|
|
@@ -59,1651 +59,1649 @@ import { ProviderTransform } from "./transform"
|
|
|
import { Installation } from "../installation"
|
|
|
import { ModelID, ProviderID } from "./schema"
|
|
|
|
|
|
-export namespace Provider {
|
|
|
- const log = Log.create({ service: "provider" })
|
|
|
+const log = Log.create({ service: "provider" })
|
|
|
|
|
|
- function shouldUseCopilotResponsesApi(modelID: string): boolean {
|
|
|
- const match = /^gpt-(\d+)/.exec(modelID)
|
|
|
- if (!match) return false
|
|
|
- return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
|
|
- }
|
|
|
+function shouldUseCopilotResponsesApi(modelID: string): boolean {
|
|
|
+ const match = /^gpt-(\d+)/.exec(modelID)
|
|
|
+ if (!match) return false
|
|
|
+ return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
|
|
+}
|
|
|
|
|
|
- function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
|
|
- if (typeof ms !== "number" || ms <= 0) return res
|
|
|
- if (!res.body) return res
|
|
|
- if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
|
|
|
-
|
|
|
- const reader = res.body.getReader()
|
|
|
- const body = new ReadableStream<Uint8Array>({
|
|
|
- async pull(ctrl) {
|
|
|
- const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
|
|
|
- const id = setTimeout(() => {
|
|
|
- const err = new Error("SSE read timed out")
|
|
|
- ctl.abort(err)
|
|
|
- void reader.cancel(err)
|
|
|
+function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
|
|
+ if (typeof ms !== "number" || ms <= 0) return res
|
|
|
+ if (!res.body) return res
|
|
|
+ if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
|
|
|
+
|
|
|
+ const reader = res.body.getReader()
|
|
|
+ const body = new ReadableStream<Uint8Array>({
|
|
|
+ async pull(ctrl) {
|
|
|
+ const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
|
|
|
+ const id = setTimeout(() => {
|
|
|
+ const err = new Error("SSE read timed out")
|
|
|
+ ctl.abort(err)
|
|
|
+ void reader.cancel(err)
|
|
|
+ reject(err)
|
|
|
+ }, ms)
|
|
|
+
|
|
|
+ reader.read().then(
|
|
|
+ (part) => {
|
|
|
+ clearTimeout(id)
|
|
|
+ resolve(part)
|
|
|
+ },
|
|
|
+ (err) => {
|
|
|
+ clearTimeout(id)
|
|
|
reject(err)
|
|
|
- }, ms)
|
|
|
-
|
|
|
- reader.read().then(
|
|
|
- (part) => {
|
|
|
- clearTimeout(id)
|
|
|
- resolve(part)
|
|
|
- },
|
|
|
- (err) => {
|
|
|
- clearTimeout(id)
|
|
|
- reject(err)
|
|
|
- },
|
|
|
- )
|
|
|
- })
|
|
|
-
|
|
|
- if (part.done) {
|
|
|
- ctrl.close()
|
|
|
- return
|
|
|
- }
|
|
|
+ },
|
|
|
+ )
|
|
|
+ })
|
|
|
|
|
|
- ctrl.enqueue(part.value)
|
|
|
- },
|
|
|
- async cancel(reason) {
|
|
|
- ctl.abort(reason)
|
|
|
- await reader.cancel(reason)
|
|
|
- },
|
|
|
- })
|
|
|
+ if (part.done) {
|
|
|
+ ctrl.close()
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- return new Response(body, {
|
|
|
- headers: new Headers(res.headers),
|
|
|
- status: res.status,
|
|
|
- statusText: res.statusText,
|
|
|
- })
|
|
|
- }
|
|
|
+ ctrl.enqueue(part.value)
|
|
|
+ },
|
|
|
+ async cancel(reason) {
|
|
|
+ ctl.abort(reason)
|
|
|
+ await reader.cancel(reason)
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ return new Response(body, {
|
|
|
+ headers: new Headers(res.headers),
|
|
|
+ status: res.status,
|
|
|
+ statusText: res.statusText,
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
- type BundledSDK = {
|
|
|
- languageModel(modelId: string): LanguageModelV3
|
|
|
- }
|
|
|
+type BundledSDK = {
|
|
|
+ languageModel(modelId: string): LanguageModelV3
|
|
|
+}
|
|
|
|
|
|
- const BUNDLED_PROVIDERS: Record<string, (options: any) => BundledSDK> = {
|
|
|
- "@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
|
|
- "@ai-sdk/anthropic": createAnthropic,
|
|
|
- "@ai-sdk/azure": createAzure,
|
|
|
- "@ai-sdk/google": createGoogleGenerativeAI,
|
|
|
- "@ai-sdk/google-vertex": createVertex,
|
|
|
- "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
|
|
|
- "@ai-sdk/openai": createOpenAI,
|
|
|
- "@ai-sdk/openai-compatible": createOpenAICompatible,
|
|
|
- "@openrouter/ai-sdk-provider": createOpenRouter,
|
|
|
- "@ai-sdk/xai": createXai,
|
|
|
- "@ai-sdk/mistral": createMistral,
|
|
|
- "@ai-sdk/groq": createGroq,
|
|
|
- "@ai-sdk/deepinfra": createDeepInfra,
|
|
|
- "@ai-sdk/cerebras": createCerebras,
|
|
|
- "@ai-sdk/cohere": createCohere,
|
|
|
- "@ai-sdk/gateway": createGateway,
|
|
|
- "@ai-sdk/togetherai": createTogetherAI,
|
|
|
- "@ai-sdk/perplexity": createPerplexity,
|
|
|
- "@ai-sdk/vercel": createVercel,
|
|
|
- "@ai-sdk/alibaba": createAlibaba,
|
|
|
- "gitlab-ai-provider": createGitLab,
|
|
|
- "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
|
|
- "venice-ai-sdk-provider": createVenice,
|
|
|
- }
|
|
|
+const BUNDLED_PROVIDERS: Record<string, (options: any) => BundledSDK> = {
|
|
|
+ "@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
|
|
+ "@ai-sdk/anthropic": createAnthropic,
|
|
|
+ "@ai-sdk/azure": createAzure,
|
|
|
+ "@ai-sdk/google": createGoogleGenerativeAI,
|
|
|
+ "@ai-sdk/google-vertex": createVertex,
|
|
|
+ "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
|
|
|
+ "@ai-sdk/openai": createOpenAI,
|
|
|
+ "@ai-sdk/openai-compatible": createOpenAICompatible,
|
|
|
+ "@openrouter/ai-sdk-provider": createOpenRouter,
|
|
|
+ "@ai-sdk/xai": createXai,
|
|
|
+ "@ai-sdk/mistral": createMistral,
|
|
|
+ "@ai-sdk/groq": createGroq,
|
|
|
+ "@ai-sdk/deepinfra": createDeepInfra,
|
|
|
+ "@ai-sdk/cerebras": createCerebras,
|
|
|
+ "@ai-sdk/cohere": createCohere,
|
|
|
+ "@ai-sdk/gateway": createGateway,
|
|
|
+ "@ai-sdk/togetherai": createTogetherAI,
|
|
|
+ "@ai-sdk/perplexity": createPerplexity,
|
|
|
+ "@ai-sdk/vercel": createVercel,
|
|
|
+ "@ai-sdk/alibaba": createAlibaba,
|
|
|
+ "gitlab-ai-provider": createGitLab,
|
|
|
+ "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
|
|
+ "venice-ai-sdk-provider": createVenice,
|
|
|
+}
|
|
|
|
|
|
- type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
|
|
- type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
|
|
|
- type CustomDiscoverModels = () => Promise<Record<string, Model>>
|
|
|
- type CustomLoader = (provider: Info) => Effect.Effect<{
|
|
|
- autoload: boolean
|
|
|
- getModel?: CustomModelLoader
|
|
|
- vars?: CustomVarsLoader
|
|
|
- options?: Record<string, any>
|
|
|
- discoverModels?: CustomDiscoverModels
|
|
|
- }>
|
|
|
-
|
|
|
- type CustomDep = {
|
|
|
- auth: (id: string) => Effect.Effect<Auth.Info | undefined>
|
|
|
- config: () => Effect.Effect<Config.Info>
|
|
|
- env: () => Effect.Effect<Record<string, string | undefined>>
|
|
|
- get: (key: string) => Effect.Effect<string | undefined>
|
|
|
- }
|
|
|
+type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
|
|
+type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
|
|
|
+type CustomDiscoverModels = () => Promise<Record<string, Model>>
|
|
|
+type CustomLoader = (provider: Info) => Effect.Effect<{
|
|
|
+ autoload: boolean
|
|
|
+ getModel?: CustomModelLoader
|
|
|
+ vars?: CustomVarsLoader
|
|
|
+ options?: Record<string, any>
|
|
|
+ discoverModels?: CustomDiscoverModels
|
|
|
+}>
|
|
|
+
|
|
|
+type CustomDep = {
|
|
|
+ auth: (id: string) => Effect.Effect<Auth.Info | undefined>
|
|
|
+ config: () => Effect.Effect<Config.Info>
|
|
|
+ env: () => Effect.Effect<Record<string, string | undefined>>
|
|
|
+ get: (key: string) => Effect.Effect<string | undefined>
|
|
|
+}
|
|
|
|
|
|
- function useLanguageModel(sdk: any) {
|
|
|
- return sdk.responses === undefined && sdk.chat === undefined
|
|
|
- }
|
|
|
+function useLanguageModel(sdk: any) {
|
|
|
+ return sdk.responses === undefined && sdk.chat === undefined
|
|
|
+}
|
|
|
|
|
|
- function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|
|
- return {
|
|
|
- anthropic: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- options: {
|
|
|
- headers: {
|
|
|
- "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
|
- },
|
|
|
+function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|
|
+ return {
|
|
|
+ anthropic: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ options: {
|
|
|
+ headers: {
|
|
|
+ "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
|
},
|
|
|
- }),
|
|
|
- opencode: Effect.fnUntraced(function* (input: Info) {
|
|
|
- const env = yield* dep.env()
|
|
|
- const hasKey = iife(() => {
|
|
|
- if (input.env.some((item) => env[item])) return true
|
|
|
- return false
|
|
|
- })
|
|
|
- const ok =
|
|
|
- hasKey ||
|
|
|
- Boolean(yield* dep.auth(input.id)) ||
|
|
|
- Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey)
|
|
|
-
|
|
|
- if (!ok) {
|
|
|
- for (const [key, value] of Object.entries(input.models)) {
|
|
|
- if (value.cost.input === 0) continue
|
|
|
- delete input.models[key]
|
|
|
- }
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ opencode: Effect.fnUntraced(function* (input: Info) {
|
|
|
+ const env = yield* dep.env()
|
|
|
+ const hasKey = iife(() => {
|
|
|
+ if (input.env.some((item) => env[item])) return true
|
|
|
+ return false
|
|
|
+ })
|
|
|
+ const ok =
|
|
|
+ hasKey ||
|
|
|
+ Boolean(yield* dep.auth(input.id)) ||
|
|
|
+ Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey)
|
|
|
+
|
|
|
+ if (!ok) {
|
|
|
+ for (const [key, value] of Object.entries(input.models)) {
|
|
|
+ if (value.cost.input === 0) continue
|
|
|
+ delete input.models[key]
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
- autoload: Object.keys(input.models).length > 0,
|
|
|
- options: ok ? {} : { apiKey: "public" },
|
|
|
- }
|
|
|
+ return {
|
|
|
+ autoload: Object.keys(input.models).length > 0,
|
|
|
+ options: ok ? {} : { apiKey: "public" },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ openai: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
+ return sdk.responses(modelID)
|
|
|
+ },
|
|
|
+ options: {},
|
|
|
}),
|
|
|
- openai: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
- return sdk.responses(modelID)
|
|
|
- },
|
|
|
- options: {},
|
|
|
- }),
|
|
|
- xai: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
- return sdk.responses(modelID)
|
|
|
- },
|
|
|
- options: {},
|
|
|
- }),
|
|
|
- "github-copilot": () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
|
- return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
|
|
- },
|
|
|
- options: {},
|
|
|
- }),
|
|
|
- azure: Effect.fnUntraced(function* (provider: Info) {
|
|
|
- const env = yield* dep.env()
|
|
|
- const resource = iife(() => {
|
|
|
- const name = provider.options?.resourceName
|
|
|
- if (typeof name === "string" && name.trim() !== "") return name
|
|
|
- return env["AZURE_RESOURCE_NAME"]
|
|
|
- })
|
|
|
-
|
|
|
- return {
|
|
|
- autoload: false,
|
|
|
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
|
- if (options?.["useCompletionUrls"]) {
|
|
|
- return sdk.chat(modelID)
|
|
|
- } else {
|
|
|
- return sdk.responses(modelID)
|
|
|
- }
|
|
|
- },
|
|
|
- options: {},
|
|
|
- vars(_options) {
|
|
|
- return {
|
|
|
- ...(resource && { AZURE_RESOURCE_NAME: resource }),
|
|
|
- }
|
|
|
- },
|
|
|
- }
|
|
|
+ xai: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
+ return sdk.responses(modelID)
|
|
|
+ },
|
|
|
+ options: {},
|
|
|
}),
|
|
|
- "azure-cognitive-services": Effect.fnUntraced(function* () {
|
|
|
- const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
|
|
- return {
|
|
|
- autoload: false,
|
|
|
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
|
- if (options?.["useCompletionUrls"]) {
|
|
|
- return sdk.chat(modelID)
|
|
|
- } else {
|
|
|
- return sdk.responses(modelID)
|
|
|
- }
|
|
|
- },
|
|
|
- options: {
|
|
|
- baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
|
|
- },
|
|
|
- }
|
|
|
+ "github-copilot": () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
|
+ return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
|
|
+ },
|
|
|
+ options: {},
|
|
|
}),
|
|
|
- "amazon-bedrock": Effect.fnUntraced(function* () {
|
|
|
- const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"]
|
|
|
- const auth = yield* dep.auth("amazon-bedrock")
|
|
|
- const env = yield* dep.env()
|
|
|
-
|
|
|
- // Region precedence: 1) config file, 2) env var, 3) default
|
|
|
- const configRegion = providerConfig?.options?.region
|
|
|
- const envRegion = env["AWS_REGION"]
|
|
|
- const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
|
|
|
-
|
|
|
- // Profile: config file takes precedence over env var
|
|
|
- const configProfile = providerConfig?.options?.profile
|
|
|
- const envProfile = env["AWS_PROFILE"]
|
|
|
- const profile = configProfile ?? envProfile
|
|
|
-
|
|
|
- const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"]
|
|
|
-
|
|
|
- // TODO: Using process.env directly because Env.set only updates a process.env shallow copy,
|
|
|
- // until the scope of the Env API is clarified (test only or runtime?)
|
|
|
- const awsBearerToken = iife(() => {
|
|
|
- const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK
|
|
|
- if (envToken) return envToken
|
|
|
- if (auth?.type === "api") {
|
|
|
- process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key
|
|
|
- return auth.key
|
|
|
+ azure: Effect.fnUntraced(function* (provider: Info) {
|
|
|
+ const env = yield* dep.env()
|
|
|
+ const resource = iife(() => {
|
|
|
+ const name = provider.options?.resourceName
|
|
|
+ if (typeof name === "string" && name.trim() !== "") return name
|
|
|
+ return env["AZURE_RESOURCE_NAME"]
|
|
|
+ })
|
|
|
+
|
|
|
+ return {
|
|
|
+ autoload: false,
|
|
|
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
|
+ if (options?.["useCompletionUrls"]) {
|
|
|
+ return sdk.chat(modelID)
|
|
|
+ } else {
|
|
|
+ return sdk.responses(modelID)
|
|
|
}
|
|
|
- return undefined
|
|
|
- })
|
|
|
+ },
|
|
|
+ options: {},
|
|
|
+ vars(_options) {
|
|
|
+ return {
|
|
|
+ ...(resource && { AZURE_RESOURCE_NAME: resource }),
|
|
|
+ }
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ "azure-cognitive-services": Effect.fnUntraced(function* () {
|
|
|
+ const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
|
|
+ return {
|
|
|
+ autoload: false,
|
|
|
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
|
+ if (options?.["useCompletionUrls"]) {
|
|
|
+ return sdk.chat(modelID)
|
|
|
+ } else {
|
|
|
+ return sdk.responses(modelID)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ "amazon-bedrock": Effect.fnUntraced(function* () {
|
|
|
+ const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"]
|
|
|
+ const auth = yield* dep.auth("amazon-bedrock")
|
|
|
+ const env = yield* dep.env()
|
|
|
+
|
|
|
+ // Region precedence: 1) config file, 2) env var, 3) default
|
|
|
+ const configRegion = providerConfig?.options?.region
|
|
|
+ const envRegion = env["AWS_REGION"]
|
|
|
+ const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
|
|
|
+
|
|
|
+ // Profile: config file takes precedence over env var
|
|
|
+ const configProfile = providerConfig?.options?.profile
|
|
|
+ const envProfile = env["AWS_PROFILE"]
|
|
|
+ const profile = configProfile ?? envProfile
|
|
|
+
|
|
|
+ const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"]
|
|
|
+
|
|
|
+ // TODO: Using process.env directly because Env.set only updates a process.env shallow copy,
|
|
|
+ // until the scope of the Env API is clarified (test only or runtime?)
|
|
|
+ const awsBearerToken = iife(() => {
|
|
|
+ const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK
|
|
|
+ if (envToken) return envToken
|
|
|
+ if (auth?.type === "api") {
|
|
|
+ process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key
|
|
|
+ return auth.key
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
|
|
|
- const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"]
|
|
|
+ const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"]
|
|
|
|
|
|
- const containerCreds = Boolean(
|
|
|
- process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
|
|
|
- )
|
|
|
+ const containerCreds = Boolean(
|
|
|
+ process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
|
|
|
+ )
|
|
|
|
|
|
- if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds)
|
|
|
- return { autoload: false }
|
|
|
+ if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds)
|
|
|
+ return { autoload: false }
|
|
|
|
|
|
- const providerOptions: AmazonBedrockProviderSettings = {
|
|
|
- region: defaultRegion,
|
|
|
- }
|
|
|
+ const providerOptions: AmazonBedrockProviderSettings = {
|
|
|
+ region: defaultRegion,
|
|
|
+ }
|
|
|
|
|
|
- // Only use credential chain if no bearer token exists
|
|
|
- // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens)
|
|
|
- if (!awsBearerToken) {
|
|
|
- // Build credential provider options (only pass profile if specified)
|
|
|
- const credentialProviderOptions = profile ? { profile } : {}
|
|
|
+ // Only use credential chain if no bearer token exists
|
|
|
+ // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens)
|
|
|
+ if (!awsBearerToken) {
|
|
|
+ // Build credential provider options (only pass profile if specified)
|
|
|
+ const credentialProviderOptions = profile ? { profile } : {}
|
|
|
|
|
|
- providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
|
|
|
- }
|
|
|
+ providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
|
|
|
+ }
|
|
|
|
|
|
- // Add custom endpoint if specified (endpoint takes precedence over baseURL)
|
|
|
- const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL
|
|
|
- if (endpoint) {
|
|
|
- providerOptions.baseURL = endpoint
|
|
|
- }
|
|
|
+ // Add custom endpoint if specified (endpoint takes precedence over baseURL)
|
|
|
+ const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL
|
|
|
+ if (endpoint) {
|
|
|
+ providerOptions.baseURL = endpoint
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
- autoload: true,
|
|
|
- options: providerOptions,
|
|
|
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
- // Skip region prefixing if model already has a cross-region inference profile prefix
|
|
|
- // Models from models.dev may already include prefixes like us., eu., global., etc.
|
|
|
- const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
|
|
- if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) {
|
|
|
- return sdk.languageModel(modelID)
|
|
|
- }
|
|
|
+ return {
|
|
|
+ autoload: true,
|
|
|
+ options: providerOptions,
|
|
|
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
|
+ // Skip region prefixing if model already has a cross-region inference profile prefix
|
|
|
+ // Models from models.dev may already include prefixes like us., eu., global., etc.
|
|
|
+ const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
|
|
+ if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) {
|
|
|
+ return sdk.languageModel(modelID)
|
|
|
+ }
|
|
|
|
|
|
- // Region resolution precedence (highest to lowest):
|
|
|
- // 1. options.region from opencode.json provider config
|
|
|
- // 2. defaultRegion from AWS_REGION environment variable
|
|
|
- // 3. Default "us-east-1" (baked into defaultRegion)
|
|
|
- const region = options?.region ?? defaultRegion
|
|
|
-
|
|
|
- let regionPrefix = region.split("-")[0]
|
|
|
-
|
|
|
- switch (regionPrefix) {
|
|
|
- case "us": {
|
|
|
- const modelRequiresPrefix = [
|
|
|
- "nova-micro",
|
|
|
- "nova-lite",
|
|
|
- "nova-pro",
|
|
|
- "nova-premier",
|
|
|
- "nova-2",
|
|
|
- "claude",
|
|
|
- "deepseek",
|
|
|
- ].some((m) => modelID.includes(m))
|
|
|
- const isGovCloud = region.startsWith("us-gov")
|
|
|
- if (modelRequiresPrefix && !isGovCloud) {
|
|
|
- modelID = `${regionPrefix}.${modelID}`
|
|
|
- }
|
|
|
- break
|
|
|
+ // Region resolution precedence (highest to lowest):
|
|
|
+ // 1. options.region from opencode.json provider config
|
|
|
+ // 2. defaultRegion from AWS_REGION environment variable
|
|
|
+ // 3. Default "us-east-1" (baked into defaultRegion)
|
|
|
+ const region = options?.region ?? defaultRegion
|
|
|
+
|
|
|
+ let regionPrefix = region.split("-")[0]
|
|
|
+
|
|
|
+ switch (regionPrefix) {
|
|
|
+ case "us": {
|
|
|
+ const modelRequiresPrefix = [
|
|
|
+ "nova-micro",
|
|
|
+ "nova-lite",
|
|
|
+ "nova-pro",
|
|
|
+ "nova-premier",
|
|
|
+ "nova-2",
|
|
|
+ "claude",
|
|
|
+ "deepseek",
|
|
|
+ ].some((m) => modelID.includes(m))
|
|
|
+ const isGovCloud = region.startsWith("us-gov")
|
|
|
+ if (modelRequiresPrefix && !isGovCloud) {
|
|
|
+ modelID = `${regionPrefix}.${modelID}`
|
|
|
+ }
|
|
|
+ break
|
|
|
+ }
|
|
|
+ case "eu": {
|
|
|
+ const regionRequiresPrefix = [
|
|
|
+ "eu-west-1",
|
|
|
+ "eu-west-2",
|
|
|
+ "eu-west-3",
|
|
|
+ "eu-north-1",
|
|
|
+ "eu-central-1",
|
|
|
+ "eu-south-1",
|
|
|
+ "eu-south-2",
|
|
|
+ ].some((r) => region.includes(r))
|
|
|
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
|
|
|
+ modelID.includes(m),
|
|
|
+ )
|
|
|
+ if (regionRequiresPrefix && modelRequiresPrefix) {
|
|
|
+ modelID = `${regionPrefix}.${modelID}`
|
|
|
}
|
|
|
- case "eu": {
|
|
|
- const regionRequiresPrefix = [
|
|
|
- "eu-west-1",
|
|
|
- "eu-west-2",
|
|
|
- "eu-west-3",
|
|
|
- "eu-north-1",
|
|
|
- "eu-central-1",
|
|
|
- "eu-south-1",
|
|
|
- "eu-south-2",
|
|
|
- ].some((r) => region.includes(r))
|
|
|
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
|
|
|
+ break
|
|
|
+ }
|
|
|
+ case "ap": {
|
|
|
+ const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
|
|
|
+ const isTokyoRegion = region === "ap-northeast-1"
|
|
|
+ if (
|
|
|
+ isAustraliaRegion &&
|
|
|
+ ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
|
|
|
+ ) {
|
|
|
+ regionPrefix = "au"
|
|
|
+ modelID = `${regionPrefix}.${modelID}`
|
|
|
+ } else if (isTokyoRegion) {
|
|
|
+ // Tokyo region uses jp. prefix for cross-region inference
|
|
|
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
|
|
modelID.includes(m),
|
|
|
)
|
|
|
- if (regionRequiresPrefix && modelRequiresPrefix) {
|
|
|
+ if (modelRequiresPrefix) {
|
|
|
+ regionPrefix = "jp"
|
|
|
modelID = `${regionPrefix}.${modelID}`
|
|
|
}
|
|
|
- break
|
|
|
- }
|
|
|
- case "ap": {
|
|
|
- const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
|
|
|
- const isTokyoRegion = region === "ap-northeast-1"
|
|
|
- if (
|
|
|
- isAustraliaRegion &&
|
|
|
- ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
|
|
|
- ) {
|
|
|
- regionPrefix = "au"
|
|
|
+ } else {
|
|
|
+ // Other APAC regions use apac. prefix
|
|
|
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
|
|
+ modelID.includes(m),
|
|
|
+ )
|
|
|
+ if (modelRequiresPrefix) {
|
|
|
+ regionPrefix = "apac"
|
|
|
modelID = `${regionPrefix}.${modelID}`
|
|
|
- } else if (isTokyoRegion) {
|
|
|
- // Tokyo region uses jp. prefix for cross-region inference
|
|
|
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
|
|
- modelID.includes(m),
|
|
|
- )
|
|
|
- if (modelRequiresPrefix) {
|
|
|
- regionPrefix = "jp"
|
|
|
- modelID = `${regionPrefix}.${modelID}`
|
|
|
- }
|
|
|
- } else {
|
|
|
- // Other APAC regions use apac. prefix
|
|
|
- const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
|
|
|
- modelID.includes(m),
|
|
|
- )
|
|
|
- if (modelRequiresPrefix) {
|
|
|
- regionPrefix = "apac"
|
|
|
- modelID = `${regionPrefix}.${modelID}`
|
|
|
- }
|
|
|
}
|
|
|
- break
|
|
|
}
|
|
|
+ break
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return sdk.languageModel(modelID)
|
|
|
- },
|
|
|
- }
|
|
|
- }),
|
|
|
- openrouter: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- options: {
|
|
|
- headers: {
|
|
|
- "HTTP-Referer": "https://opencode.ai/",
|
|
|
- "X-Title": "opencode",
|
|
|
- },
|
|
|
- },
|
|
|
- }),
|
|
|
- vercel: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- options: {
|
|
|
- headers: {
|
|
|
- "http-referer": "https://opencode.ai/",
|
|
|
- "x-title": "opencode",
|
|
|
- },
|
|
|
- },
|
|
|
- }),
|
|
|
- "google-vertex": Effect.fnUntraced(function* (provider: Info) {
|
|
|
- const env = yield* dep.env()
|
|
|
- const project =
|
|
|
- provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
|
|
-
|
|
|
- const location = String(
|
|
|
- provider.options?.location ??
|
|
|
- env["GOOGLE_VERTEX_LOCATION"] ??
|
|
|
- env["GOOGLE_CLOUD_LOCATION"] ??
|
|
|
- env["VERTEX_LOCATION"] ??
|
|
|
- "us-central1",
|
|
|
- )
|
|
|
-
|
|
|
- const autoload = Boolean(project)
|
|
|
- if (!autoload) return { autoload: false }
|
|
|
- return {
|
|
|
- autoload: true,
|
|
|
- vars(_options: Record<string, any>) {
|
|
|
- const endpoint =
|
|
|
- location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
|
|
- return {
|
|
|
- ...(project && { GOOGLE_VERTEX_PROJECT: project }),
|
|
|
- GOOGLE_VERTEX_LOCATION: location,
|
|
|
- GOOGLE_VERTEX_ENDPOINT: endpoint,
|
|
|
- }
|
|
|
- },
|
|
|
- options: {
|
|
|
- project,
|
|
|
- location,
|
|
|
- fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
- const auth = new GoogleAuth()
|
|
|
- const client = await auth.getApplicationDefault()
|
|
|
- const token = await client.credential.getAccessToken()
|
|
|
-
|
|
|
- const headers = new Headers(init?.headers)
|
|
|
- headers.set("Authorization", `Bearer ${token.token}`)
|
|
|
-
|
|
|
- return fetch(input, { ...init, headers })
|
|
|
- },
|
|
|
- },
|
|
|
- async getModel(sdk: any, modelID: string) {
|
|
|
- const id = String(modelID).trim()
|
|
|
- return sdk.languageModel(id)
|
|
|
+ return sdk.languageModel(modelID)
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ openrouter: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ options: {
|
|
|
+ headers: {
|
|
|
+ "HTTP-Referer": "https://opencode.ai/",
|
|
|
+ "X-Title": "opencode",
|
|
|
},
|
|
|
- }
|
|
|
+ },
|
|
|
}),
|
|
|
- "google-vertex-anthropic": Effect.fnUntraced(function* () {
|
|
|
- const env = yield* dep.env()
|
|
|
- const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
|
|
- const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
|
|
|
- const autoload = Boolean(project)
|
|
|
- if (!autoload) return { autoload: false }
|
|
|
- return {
|
|
|
- autoload: true,
|
|
|
- options: {
|
|
|
- project,
|
|
|
- location,
|
|
|
- },
|
|
|
- async getModel(sdk: any, modelID) {
|
|
|
- const id = String(modelID).trim()
|
|
|
- return sdk.languageModel(id)
|
|
|
+ vercel: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ options: {
|
|
|
+ headers: {
|
|
|
+ "http-referer": "https://opencode.ai/",
|
|
|
+ "x-title": "opencode",
|
|
|
},
|
|
|
- }
|
|
|
+ },
|
|
|
}),
|
|
|
- "sap-ai-core": Effect.fnUntraced(function* () {
|
|
|
- const auth = yield* dep.auth("sap-ai-core")
|
|
|
- // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env),
|
|
|
- // until the scope of the Env API is clarified (test only or runtime?)
|
|
|
- const envServiceKey = iife(() => {
|
|
|
- const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY
|
|
|
- if (envAICoreServiceKey) return envAICoreServiceKey
|
|
|
- if (auth?.type === "api") {
|
|
|
- process.env.AICORE_SERVICE_KEY = auth.key
|
|
|
- return auth.key
|
|
|
- }
|
|
|
- return undefined
|
|
|
- })
|
|
|
- const deploymentId = process.env.AICORE_DEPLOYMENT_ID
|
|
|
- const resourceGroup = process.env.AICORE_RESOURCE_GROUP
|
|
|
+ "google-vertex": Effect.fnUntraced(function* (provider: Info) {
|
|
|
+ const env = yield* dep.env()
|
|
|
+ const project =
|
|
|
+ provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
|
|
+
|
|
|
+ const location = String(
|
|
|
+ provider.options?.location ??
|
|
|
+ env["GOOGLE_VERTEX_LOCATION"] ??
|
|
|
+ env["GOOGLE_CLOUD_LOCATION"] ??
|
|
|
+ env["VERTEX_LOCATION"] ??
|
|
|
+ "us-central1",
|
|
|
+ )
|
|
|
|
|
|
- return {
|
|
|
- autoload: !!envServiceKey,
|
|
|
- options: envServiceKey ? { deploymentId, resourceGroup } : {},
|
|
|
- async getModel(sdk: any, modelID: string) {
|
|
|
- return sdk(modelID)
|
|
|
+ const autoload = Boolean(project)
|
|
|
+ if (!autoload) return { autoload: false }
|
|
|
+ return {
|
|
|
+ autoload: true,
|
|
|
+ vars(_options: Record<string, any>) {
|
|
|
+ const endpoint =
|
|
|
+ location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
|
|
+ return {
|
|
|
+ ...(project && { GOOGLE_VERTEX_PROJECT: project }),
|
|
|
+ GOOGLE_VERTEX_LOCATION: location,
|
|
|
+ GOOGLE_VERTEX_ENDPOINT: endpoint,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ project,
|
|
|
+ location,
|
|
|
+ fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
+ const auth = new GoogleAuth()
|
|
|
+ const client = await auth.getApplicationDefault()
|
|
|
+ const token = await client.credential.getAccessToken()
|
|
|
+
|
|
|
+ const headers = new Headers(init?.headers)
|
|
|
+ headers.set("Authorization", `Bearer ${token.token}`)
|
|
|
+
|
|
|
+ return fetch(input, { ...init, headers })
|
|
|
},
|
|
|
+ },
|
|
|
+ async getModel(sdk: any, modelID: string) {
|
|
|
+ const id = String(modelID).trim()
|
|
|
+ return sdk.languageModel(id)
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ "google-vertex-anthropic": Effect.fnUntraced(function* () {
|
|
|
+ const env = yield* dep.env()
|
|
|
+ const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
|
|
+ const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
|
|
|
+ const autoload = Boolean(project)
|
|
|
+ if (!autoload) return { autoload: false }
|
|
|
+ return {
|
|
|
+ autoload: true,
|
|
|
+ options: {
|
|
|
+ project,
|
|
|
+ location,
|
|
|
+ },
|
|
|
+ async getModel(sdk: any, modelID) {
|
|
|
+ const id = String(modelID).trim()
|
|
|
+ return sdk.languageModel(id)
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ "sap-ai-core": Effect.fnUntraced(function* () {
|
|
|
+ const auth = yield* dep.auth("sap-ai-core")
|
|
|
+ // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env),
|
|
|
+ // until the scope of the Env API is clarified (test only or runtime?)
|
|
|
+ const envServiceKey = iife(() => {
|
|
|
+ const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY
|
|
|
+ if (envAICoreServiceKey) return envAICoreServiceKey
|
|
|
+ if (auth?.type === "api") {
|
|
|
+ process.env.AICORE_SERVICE_KEY = auth.key
|
|
|
+ return auth.key
|
|
|
}
|
|
|
- }),
|
|
|
- zenmux: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- options: {
|
|
|
- headers: {
|
|
|
- "HTTP-Referer": "https://opencode.ai/",
|
|
|
- "X-Title": "opencode",
|
|
|
- },
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
+ const deploymentId = process.env.AICORE_DEPLOYMENT_ID
|
|
|
+ const resourceGroup = process.env.AICORE_RESOURCE_GROUP
|
|
|
+
|
|
|
+ return {
|
|
|
+ autoload: !!envServiceKey,
|
|
|
+ options: envServiceKey ? { deploymentId, resourceGroup } : {},
|
|
|
+ async getModel(sdk: any, modelID: string) {
|
|
|
+ return sdk(modelID)
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ zenmux: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ options: {
|
|
|
+ headers: {
|
|
|
+ "HTTP-Referer": "https://opencode.ai/",
|
|
|
+ "X-Title": "opencode",
|
|
|
},
|
|
|
- }),
|
|
|
- gitlab: Effect.fnUntraced(function* (input: Info) {
|
|
|
- const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ gitlab: Effect.fnUntraced(function* (input: Info) {
|
|
|
+ const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
|
|
|
|
|
|
- const auth = yield* dep.auth(input.id)
|
|
|
- const apiKey = yield* Effect.sync(() => {
|
|
|
- if (auth?.type === "oauth") return auth.access
|
|
|
- if (auth?.type === "api") return auth.key
|
|
|
- return undefined
|
|
|
- })
|
|
|
- const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
|
|
|
+ const auth = yield* dep.auth(input.id)
|
|
|
+ const apiKey = yield* Effect.sync(() => {
|
|
|
+ if (auth?.type === "oauth") return auth.access
|
|
|
+ if (auth?.type === "api") return auth.key
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
+ const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
|
|
|
|
|
|
- const providerConfig = (yield* dep.config()).provider?.["gitlab"]
|
|
|
+ const providerConfig = (yield* dep.config()).provider?.["gitlab"]
|
|
|
|
|
|
- const aiGatewayHeaders = {
|
|
|
- "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
|
- "anthropic-beta": "context-1m-2025-08-07",
|
|
|
- ...providerConfig?.options?.aiGatewayHeaders,
|
|
|
- }
|
|
|
+ const aiGatewayHeaders = {
|
|
|
+ "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
|
+ "anthropic-beta": "context-1m-2025-08-07",
|
|
|
+ ...providerConfig?.options?.aiGatewayHeaders,
|
|
|
+ }
|
|
|
|
|
|
- const featureFlags = {
|
|
|
- duo_agent_platform_agentic_chat: true,
|
|
|
- duo_agent_platform: true,
|
|
|
- ...providerConfig?.options?.featureFlags,
|
|
|
- }
|
|
|
+ const featureFlags = {
|
|
|
+ duo_agent_platform_agentic_chat: true,
|
|
|
+ duo_agent_platform: true,
|
|
|
+ ...providerConfig?.options?.featureFlags,
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
- autoload: !!token,
|
|
|
- options: {
|
|
|
- instanceUrl,
|
|
|
- apiKey: token,
|
|
|
- aiGatewayHeaders,
|
|
|
- featureFlags,
|
|
|
- },
|
|
|
- async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string, options?: Record<string, any>) {
|
|
|
- if (modelID.startsWith("duo-workflow-")) {
|
|
|
- const workflowRef = options?.workflowRef as string | undefined
|
|
|
- // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
|
|
|
- const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
|
|
|
- const model = sdk.workflowChat(sdkModelID, {
|
|
|
- featureFlags,
|
|
|
- workflowDefinition: options?.workflowDefinition as string | undefined,
|
|
|
- })
|
|
|
- if (workflowRef) {
|
|
|
- model.selectedModelRef = workflowRef
|
|
|
- }
|
|
|
- return model
|
|
|
- }
|
|
|
- return sdk.agenticChat(modelID, {
|
|
|
- aiGatewayHeaders,
|
|
|
+ return {
|
|
|
+ autoload: !!token,
|
|
|
+ options: {
|
|
|
+ instanceUrl,
|
|
|
+ apiKey: token,
|
|
|
+ aiGatewayHeaders,
|
|
|
+ featureFlags,
|
|
|
+ },
|
|
|
+ async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string, options?: Record<string, any>) {
|
|
|
+ if (modelID.startsWith("duo-workflow-")) {
|
|
|
+ const workflowRef = options?.workflowRef as string | undefined
|
|
|
+ // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
|
|
|
+ const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
|
|
|
+ const model = sdk.workflowChat(sdkModelID, {
|
|
|
featureFlags,
|
|
|
+ workflowDefinition: options?.workflowDefinition as string | undefined,
|
|
|
})
|
|
|
- },
|
|
|
- async discoverModels(): Promise<Record<string, Model>> {
|
|
|
- if (!apiKey) {
|
|
|
- log.info("gitlab model discovery skipped: no apiKey")
|
|
|
- return {}
|
|
|
+ if (workflowRef) {
|
|
|
+ model.selectedModelRef = workflowRef
|
|
|
}
|
|
|
+ return model
|
|
|
+ }
|
|
|
+ return sdk.agenticChat(modelID, {
|
|
|
+ aiGatewayHeaders,
|
|
|
+ featureFlags,
|
|
|
+ })
|
|
|
+ },
|
|
|
+ async discoverModels(): Promise<Record<string, Model>> {
|
|
|
+ if (!apiKey) {
|
|
|
+ log.info("gitlab model discovery skipped: no apiKey")
|
|
|
+ return {}
|
|
|
+ }
|
|
|
|
|
|
- try {
|
|
|
- const token = apiKey
|
|
|
- const getHeaders = (): Record<string, string> =>
|
|
|
- auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
|
|
|
-
|
|
|
- log.info("gitlab model discovery starting", { instanceUrl })
|
|
|
- const result = await discoverWorkflowModels(
|
|
|
- { instanceUrl, getHeaders },
|
|
|
- { workingDirectory: Instance.directory },
|
|
|
- )
|
|
|
+ try {
|
|
|
+ const token = apiKey
|
|
|
+ const getHeaders = (): Record<string, string> =>
|
|
|
+ auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
|
|
|
+
|
|
|
+ log.info("gitlab model discovery starting", { instanceUrl })
|
|
|
+ const result = await discoverWorkflowModels(
|
|
|
+ { instanceUrl, getHeaders },
|
|
|
+ { workingDirectory: Instance.directory },
|
|
|
+ )
|
|
|
|
|
|
- if (!result.models.length) {
|
|
|
- log.info("gitlab model discovery skipped: no models found", {
|
|
|
- project: result.project
|
|
|
- ? {
|
|
|
- id: result.project.id,
|
|
|
- path: result.project.pathWithNamespace,
|
|
|
- }
|
|
|
- : null,
|
|
|
- })
|
|
|
- return {}
|
|
|
- }
|
|
|
+ if (!result.models.length) {
|
|
|
+ log.info("gitlab model discovery skipped: no models found", {
|
|
|
+ project: result.project
|
|
|
+ ? {
|
|
|
+ id: result.project.id,
|
|
|
+ path: result.project.pathWithNamespace,
|
|
|
+ }
|
|
|
+ : null,
|
|
|
+ })
|
|
|
+ return {}
|
|
|
+ }
|
|
|
|
|
|
- const models: Record<string, Model> = {}
|
|
|
- for (const m of result.models) {
|
|
|
- if (!input.models[m.id]) {
|
|
|
- models[m.id] = {
|
|
|
- id: ModelID.make(m.id),
|
|
|
- providerID: ProviderID.make("gitlab"),
|
|
|
- name: `Agent Platform (${m.name})`,
|
|
|
- family: "",
|
|
|
- api: {
|
|
|
- id: m.id,
|
|
|
- url: instanceUrl,
|
|
|
- npm: "gitlab-ai-provider",
|
|
|
+ const models: Record<string, Model> = {}
|
|
|
+ for (const m of result.models) {
|
|
|
+ if (!input.models[m.id]) {
|
|
|
+ models[m.id] = {
|
|
|
+ id: ModelID.make(m.id),
|
|
|
+ providerID: ProviderID.make("gitlab"),
|
|
|
+ name: `Agent Platform (${m.name})`,
|
|
|
+ family: "",
|
|
|
+ api: {
|
|
|
+ id: m.id,
|
|
|
+ url: instanceUrl,
|
|
|
+ npm: "gitlab-ai-provider",
|
|
|
+ },
|
|
|
+ status: "active",
|
|
|
+ headers: {},
|
|
|
+ options: { workflowRef: m.ref },
|
|
|
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
|
+ limit: { context: m.context, output: m.output },
|
|
|
+ capabilities: {
|
|
|
+ temperature: false,
|
|
|
+ reasoning: true,
|
|
|
+ attachment: true,
|
|
|
+ toolcall: true,
|
|
|
+ input: {
|
|
|
+ text: true,
|
|
|
+ audio: false,
|
|
|
+ image: true,
|
|
|
+ video: false,
|
|
|
+ pdf: true,
|
|
|
},
|
|
|
- status: "active",
|
|
|
- headers: {},
|
|
|
- options: { workflowRef: m.ref },
|
|
|
- cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
|
- limit: { context: m.context, output: m.output },
|
|
|
- capabilities: {
|
|
|
- temperature: false,
|
|
|
- reasoning: true,
|
|
|
- attachment: true,
|
|
|
- toolcall: true,
|
|
|
- input: {
|
|
|
- text: true,
|
|
|
- audio: false,
|
|
|
- image: true,
|
|
|
- video: false,
|
|
|
- pdf: true,
|
|
|
- },
|
|
|
- output: {
|
|
|
- text: true,
|
|
|
- audio: false,
|
|
|
- image: false,
|
|
|
- video: false,
|
|
|
- pdf: false,
|
|
|
- },
|
|
|
- interleaved: false,
|
|
|
+ output: {
|
|
|
+ text: true,
|
|
|
+ audio: false,
|
|
|
+ image: false,
|
|
|
+ video: false,
|
|
|
+ pdf: false,
|
|
|
},
|
|
|
- release_date: "",
|
|
|
- variants: {},
|
|
|
- }
|
|
|
+ interleaved: false,
|
|
|
+ },
|
|
|
+ release_date: "",
|
|
|
+ variants: {},
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- log.info("gitlab model discovery complete", {
|
|
|
- count: Object.keys(models).length,
|
|
|
- models: Object.keys(models),
|
|
|
- })
|
|
|
- return models
|
|
|
- } catch (e) {
|
|
|
- log.warn("gitlab model discovery failed", { error: e })
|
|
|
- return {}
|
|
|
}
|
|
|
+
|
|
|
+ log.info("gitlab model discovery complete", {
|
|
|
+ count: Object.keys(models).length,
|
|
|
+ models: Object.keys(models),
|
|
|
+ })
|
|
|
+ return models
|
|
|
+ } catch (e) {
|
|
|
+ log.warn("gitlab model discovery failed", { error: e })
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
|
|
|
+ // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway),
|
|
|
+ // skip the account ID check because the URL is already fully specified.
|
|
|
+ if (input.options?.baseURL) return { autoload: false }
|
|
|
+
|
|
|
+ const auth = yield* dep.auth(input.id)
|
|
|
+ const env = yield* dep.env()
|
|
|
+ const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
|
|
+ if (!accountId)
|
|
|
+ return {
|
|
|
+ autoload: false,
|
|
|
+ async getModel() {
|
|
|
+ throw new Error(
|
|
|
+ "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=<your-account-id>",
|
|
|
+ )
|
|
|
},
|
|
|
}
|
|
|
- }),
|
|
|
- "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
|
|
|
- // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway),
|
|
|
- // skip the account ID check because the URL is already fully specified.
|
|
|
- if (input.options?.baseURL) return { autoload: false }
|
|
|
-
|
|
|
- const auth = yield* dep.auth(input.id)
|
|
|
- const env = yield* dep.env()
|
|
|
- const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
|
|
- if (!accountId)
|
|
|
- return {
|
|
|
- autoload: false,
|
|
|
- async getModel() {
|
|
|
- throw new Error(
|
|
|
- "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=<your-account-id>",
|
|
|
- )
|
|
|
- },
|
|
|
- }
|
|
|
|
|
|
- const apiKey = yield* Effect.gen(function* () {
|
|
|
- const envToken = env["CLOUDFLARE_API_KEY"]
|
|
|
- if (envToken) return envToken
|
|
|
- if (auth?.type === "api") return auth.key
|
|
|
- return undefined
|
|
|
- })
|
|
|
+ const apiKey = yield* Effect.gen(function* () {
|
|
|
+ const envToken = env["CLOUDFLARE_API_KEY"]
|
|
|
+ if (envToken) return envToken
|
|
|
+ if (auth?.type === "api") return auth.key
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
|
|
|
- return {
|
|
|
- autoload: !!apiKey,
|
|
|
- options: {
|
|
|
- apiKey,
|
|
|
- headers: {
|
|
|
- "User-Agent": `opencode/${Installation.VERSION} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
|
- },
|
|
|
- },
|
|
|
- async getModel(sdk: any, modelID: string) {
|
|
|
- return sdk.languageModel(modelID)
|
|
|
- },
|
|
|
- vars(_options) {
|
|
|
- return {
|
|
|
- CLOUDFLARE_ACCOUNT_ID: accountId,
|
|
|
- }
|
|
|
+ return {
|
|
|
+ autoload: !!apiKey,
|
|
|
+ options: {
|
|
|
+ apiKey,
|
|
|
+ headers: {
|
|
|
+ "User-Agent": `opencode/${Installation.VERSION} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
|
},
|
|
|
- }
|
|
|
- }),
|
|
|
- "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) {
|
|
|
- // When baseURL is already configured (e.g. corporate config), skip the ID checks.
|
|
|
- if (input.options?.baseURL) return { autoload: false }
|
|
|
-
|
|
|
- const auth = yield* dep.auth(input.id)
|
|
|
- const env = yield* dep.env()
|
|
|
- const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
|
|
- const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
|
|
|
-
|
|
|
- if (!accountId || !gateway) {
|
|
|
- const missing = [
|
|
|
- !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined,
|
|
|
- !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined,
|
|
|
- ].filter((x): x is string => Boolean(x))
|
|
|
+ },
|
|
|
+ async getModel(sdk: any, modelID: string) {
|
|
|
+ return sdk.languageModel(modelID)
|
|
|
+ },
|
|
|
+ vars(_options) {
|
|
|
return {
|
|
|
- autoload: false,
|
|
|
- async getModel() {
|
|
|
- throw new Error(
|
|
|
- `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`,
|
|
|
- )
|
|
|
- },
|
|
|
+ CLOUDFLARE_ACCOUNT_ID: accountId,
|
|
|
}
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) {
|
|
|
+ // When baseURL is already configured (e.g. corporate config), skip the ID checks.
|
|
|
+ if (input.options?.baseURL) return { autoload: false }
|
|
|
+
|
|
|
+ const auth = yield* dep.auth(input.id)
|
|
|
+ const env = yield* dep.env()
|
|
|
+ const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
|
|
+ const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
|
|
|
+
|
|
|
+ if (!accountId || !gateway) {
|
|
|
+ const missing = [
|
|
|
+ !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined,
|
|
|
+ !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined,
|
|
|
+ ].filter((x): x is string => Boolean(x))
|
|
|
+ return {
|
|
|
+ autoload: false,
|
|
|
+ async getModel() {
|
|
|
+ throw new Error(
|
|
|
+ `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`,
|
|
|
+ )
|
|
|
+ },
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // Get API token from env or auth - required for authenticated gateways
|
|
|
- const apiToken = yield* Effect.gen(function* () {
|
|
|
- const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]
|
|
|
- if (envToken) return envToken
|
|
|
- if (auth?.type === "api") return auth.key
|
|
|
- return undefined
|
|
|
- })
|
|
|
+ // Get API token from env or auth - required for authenticated gateways
|
|
|
+ const apiToken = yield* Effect.gen(function* () {
|
|
|
+ const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]
|
|
|
+ if (envToken) return envToken
|
|
|
+ if (auth?.type === "api") return auth.key
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
|
|
|
- if (!apiToken) {
|
|
|
- throw new Error(
|
|
|
- "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " +
|
|
|
- "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.",
|
|
|
- )
|
|
|
- }
|
|
|
+ if (!apiToken) {
|
|
|
+ throw new Error(
|
|
|
+ "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " +
|
|
|
+ "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.",
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility)
|
|
|
- const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider"))
|
|
|
- const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified"))
|
|
|
+ // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility)
|
|
|
+ const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider"))
|
|
|
+ const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified"))
|
|
|
|
|
|
- const metadata = iife(() => {
|
|
|
- if (input.options?.metadata) return input.options.metadata
|
|
|
- try {
|
|
|
- return JSON.parse(input.options?.headers?.["cf-aig-metadata"])
|
|
|
- } catch {
|
|
|
- return undefined
|
|
|
- }
|
|
|
- })
|
|
|
- const opts = {
|
|
|
- metadata,
|
|
|
- cacheTtl: input.options?.cacheTtl,
|
|
|
- cacheKey: input.options?.cacheKey,
|
|
|
- skipCache: input.options?.skipCache,
|
|
|
- collectLog: input.options?.collectLog,
|
|
|
- headers: {
|
|
|
- "User-Agent": `opencode/${Installation.VERSION} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
|
- },
|
|
|
+ const metadata = iife(() => {
|
|
|
+ if (input.options?.metadata) return input.options.metadata
|
|
|
+ try {
|
|
|
+ return JSON.parse(input.options?.headers?.["cf-aig-metadata"])
|
|
|
+ } catch {
|
|
|
+ return undefined
|
|
|
}
|
|
|
+ })
|
|
|
+ const opts = {
|
|
|
+ metadata,
|
|
|
+ cacheTtl: input.options?.cacheTtl,
|
|
|
+ cacheKey: input.options?.cacheKey,
|
|
|
+ skipCache: input.options?.skipCache,
|
|
|
+ collectLog: input.options?.collectLog,
|
|
|
+ headers: {
|
|
|
+ "User-Agent": `opencode/${Installation.VERSION} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
|
+ },
|
|
|
+ }
|
|
|
|
|
|
- const aigateway = createAiGateway({
|
|
|
- accountId,
|
|
|
- gateway,
|
|
|
- apiKey: apiToken,
|
|
|
- ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}),
|
|
|
- })
|
|
|
- const unified = createUnified()
|
|
|
+ const aigateway = createAiGateway({
|
|
|
+ accountId,
|
|
|
+ gateway,
|
|
|
+ apiKey: apiToken,
|
|
|
+ ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}),
|
|
|
+ })
|
|
|
+ const unified = createUnified()
|
|
|
|
|
|
- return {
|
|
|
- autoload: true,
|
|
|
- async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
- // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
|
|
|
- return aigateway(unified(modelID))
|
|
|
+ return {
|
|
|
+ autoload: true,
|
|
|
+ async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
|
|
|
+ // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
|
|
|
+ return aigateway(unified(modelID))
|
|
|
+ },
|
|
|
+ options: {},
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ cerebras: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ options: {
|
|
|
+ headers: {
|
|
|
+ "X-Cerebras-3rd-Party-Integration": "opencode",
|
|
|
},
|
|
|
- options: {},
|
|
|
- }
|
|
|
+ },
|
|
|
}),
|
|
|
- cerebras: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- options: {
|
|
|
- headers: {
|
|
|
- "X-Cerebras-3rd-Party-Integration": "opencode",
|
|
|
- },
|
|
|
- },
|
|
|
- }),
|
|
|
- kilo: () =>
|
|
|
- Effect.succeed({
|
|
|
- autoload: false,
|
|
|
- options: {
|
|
|
- headers: {
|
|
|
- "HTTP-Referer": "https://opencode.ai/",
|
|
|
- "X-Title": "opencode",
|
|
|
- },
|
|
|
+ kilo: () =>
|
|
|
+ Effect.succeed({
|
|
|
+ autoload: false,
|
|
|
+ options: {
|
|
|
+ headers: {
|
|
|
+ "HTTP-Referer": "https://opencode.ai/",
|
|
|
+ "X-Title": "opencode",
|
|
|
},
|
|
|
- }),
|
|
|
- }
|
|
|
+ },
|
|
|
+ }),
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- export const Model = z
|
|
|
- .object({
|
|
|
- id: ModelID.zod,
|
|
|
- providerID: ProviderID.zod,
|
|
|
- api: z.object({
|
|
|
- id: z.string(),
|
|
|
- url: z.string(),
|
|
|
- npm: z.string(),
|
|
|
+export const Model = z
|
|
|
+ .object({
|
|
|
+ id: ModelID.zod,
|
|
|
+ providerID: ProviderID.zod,
|
|
|
+ api: z.object({
|
|
|
+ id: z.string(),
|
|
|
+ url: z.string(),
|
|
|
+ npm: z.string(),
|
|
|
+ }),
|
|
|
+ name: z.string(),
|
|
|
+ family: z.string().optional(),
|
|
|
+ capabilities: z.object({
|
|
|
+ temperature: z.boolean(),
|
|
|
+ reasoning: z.boolean(),
|
|
|
+ attachment: z.boolean(),
|
|
|
+ toolcall: z.boolean(),
|
|
|
+ input: z.object({
|
|
|
+ text: z.boolean(),
|
|
|
+ audio: z.boolean(),
|
|
|
+ image: z.boolean(),
|
|
|
+ video: z.boolean(),
|
|
|
+ pdf: z.boolean(),
|
|
|
}),
|
|
|
- name: z.string(),
|
|
|
- family: z.string().optional(),
|
|
|
- capabilities: z.object({
|
|
|
- temperature: z.boolean(),
|
|
|
- reasoning: z.boolean(),
|
|
|
- attachment: z.boolean(),
|
|
|
- toolcall: z.boolean(),
|
|
|
- input: z.object({
|
|
|
- text: z.boolean(),
|
|
|
- audio: z.boolean(),
|
|
|
- image: z.boolean(),
|
|
|
- video: z.boolean(),
|
|
|
- pdf: z.boolean(),
|
|
|
- }),
|
|
|
- output: z.object({
|
|
|
- text: z.boolean(),
|
|
|
- audio: z.boolean(),
|
|
|
- image: z.boolean(),
|
|
|
- video: z.boolean(),
|
|
|
- pdf: z.boolean(),
|
|
|
- }),
|
|
|
- interleaved: z.union([
|
|
|
- z.boolean(),
|
|
|
- z.object({
|
|
|
- field: z.enum(["reasoning_content", "reasoning_details"]),
|
|
|
- }),
|
|
|
- ]),
|
|
|
+ output: z.object({
|
|
|
+ text: z.boolean(),
|
|
|
+ audio: z.boolean(),
|
|
|
+ image: z.boolean(),
|
|
|
+ video: z.boolean(),
|
|
|
+ pdf: z.boolean(),
|
|
|
}),
|
|
|
- cost: z.object({
|
|
|
- input: z.number(),
|
|
|
- output: z.number(),
|
|
|
- cache: z.object({
|
|
|
- read: z.number(),
|
|
|
- write: z.number(),
|
|
|
+ interleaved: z.union([
|
|
|
+ z.boolean(),
|
|
|
+ z.object({
|
|
|
+ field: z.enum(["reasoning_content", "reasoning_details"]),
|
|
|
}),
|
|
|
- experimentalOver200K: z
|
|
|
- .object({
|
|
|
- input: z.number(),
|
|
|
- output: z.number(),
|
|
|
- cache: z.object({
|
|
|
- read: z.number(),
|
|
|
- write: z.number(),
|
|
|
- }),
|
|
|
- })
|
|
|
- .optional(),
|
|
|
- }),
|
|
|
- limit: z.object({
|
|
|
- context: z.number(),
|
|
|
- input: z.number().optional(),
|
|
|
- output: z.number(),
|
|
|
+ ]),
|
|
|
+ }),
|
|
|
+ cost: z.object({
|
|
|
+ input: z.number(),
|
|
|
+ output: z.number(),
|
|
|
+ cache: z.object({
|
|
|
+ read: z.number(),
|
|
|
+ write: z.number(),
|
|
|
}),
|
|
|
- status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
|
|
- options: z.record(z.string(), z.any()),
|
|
|
- headers: z.record(z.string(), z.string()),
|
|
|
- release_date: z.string(),
|
|
|
- variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
|
|
- })
|
|
|
- .meta({
|
|
|
- ref: "Model",
|
|
|
- })
|
|
|
- export type Model = z.infer<typeof Model>
|
|
|
-
|
|
|
- export const Info = z
|
|
|
- .object({
|
|
|
- id: ProviderID.zod,
|
|
|
- name: z.string(),
|
|
|
- source: z.enum(["env", "config", "custom", "api"]),
|
|
|
- env: z.string().array(),
|
|
|
- key: z.string().optional(),
|
|
|
- options: z.record(z.string(), z.any()),
|
|
|
- models: z.record(z.string(), Model),
|
|
|
- })
|
|
|
- .meta({
|
|
|
- ref: "Provider",
|
|
|
- })
|
|
|
- export type Info = z.infer<typeof Info>
|
|
|
-
|
|
|
- export interface Interface {
|
|
|
- readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
|
|
- readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
|
|
|
- readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model>
|
|
|
- readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
|
|
|
- readonly closest: (
|
|
|
- providerID: ProviderID,
|
|
|
- query: string[],
|
|
|
- ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
|
|
|
- readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
|
|
|
- readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }>
|
|
|
- }
|
|
|
+ experimentalOver200K: z
|
|
|
+ .object({
|
|
|
+ input: z.number(),
|
|
|
+ output: z.number(),
|
|
|
+ cache: z.object({
|
|
|
+ read: z.number(),
|
|
|
+ write: z.number(),
|
|
|
+ }),
|
|
|
+ })
|
|
|
+ .optional(),
|
|
|
+ }),
|
|
|
+ limit: z.object({
|
|
|
+ context: z.number(),
|
|
|
+ input: z.number().optional(),
|
|
|
+ output: z.number(),
|
|
|
+ }),
|
|
|
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
|
|
+ options: z.record(z.string(), z.any()),
|
|
|
+ headers: z.record(z.string(), z.string()),
|
|
|
+ release_date: z.string(),
|
|
|
+ variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
|
|
+ })
|
|
|
+ .meta({
|
|
|
+ ref: "Model",
|
|
|
+ })
|
|
|
+export type Model = z.infer<typeof Model>
|
|
|
+
|
|
|
+export const Info = z
|
|
|
+ .object({
|
|
|
+ id: ProviderID.zod,
|
|
|
+ name: z.string(),
|
|
|
+ source: z.enum(["env", "config", "custom", "api"]),
|
|
|
+ env: z.string().array(),
|
|
|
+ key: z.string().optional(),
|
|
|
+ options: z.record(z.string(), z.any()),
|
|
|
+ models: z.record(z.string(), Model),
|
|
|
+ })
|
|
|
+ .meta({
|
|
|
+ ref: "Provider",
|
|
|
+ })
|
|
|
+export type Info = z.infer<typeof Info>
|
|
|
+
|
|
|
+export interface Interface {
|
|
|
+ readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
|
|
+ readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
|
|
|
+ readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model>
|
|
|
+ readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
|
|
|
+ readonly closest: (
|
|
|
+ providerID: ProviderID,
|
|
|
+ query: string[],
|
|
|
+ ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
|
|
|
+ readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
|
|
|
+ readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }>
|
|
|
+}
|
|
|
|
|
|
- interface State {
|
|
|
- models: Map<string, LanguageModelV3>
|
|
|
- providers: Record<ProviderID, Info>
|
|
|
- sdk: Map<string, BundledSDK>
|
|
|
- modelLoaders: Record<string, CustomModelLoader>
|
|
|
- varsLoaders: Record<string, CustomVarsLoader>
|
|
|
- }
|
|
|
+interface State {
|
|
|
+ models: Map<string, LanguageModelV3>
|
|
|
+ providers: Record<ProviderID, Info>
|
|
|
+ sdk: Map<string, BundledSDK>
|
|
|
+ modelLoaders: Record<string, CustomModelLoader>
|
|
|
+ varsLoaders: Record<string, CustomVarsLoader>
|
|
|
+}
|
|
|
|
|
|
- export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
|
|
|
+export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
|
|
|
|
|
|
- function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|
|
- const result: Model["cost"] = {
|
|
|
- input: c?.input ?? 0,
|
|
|
- output: c?.output ?? 0,
|
|
|
+function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|
|
+ const result: Model["cost"] = {
|
|
|
+ input: c?.input ?? 0,
|
|
|
+ output: c?.output ?? 0,
|
|
|
+ cache: {
|
|
|
+ read: c?.cache_read ?? 0,
|
|
|
+ write: c?.cache_write ?? 0,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ if (c?.context_over_200k) {
|
|
|
+ result.experimentalOver200K = {
|
|
|
cache: {
|
|
|
- read: c?.cache_read ?? 0,
|
|
|
- write: c?.cache_write ?? 0,
|
|
|
+ read: c.context_over_200k.cache_read ?? 0,
|
|
|
+ write: c.context_over_200k.cache_write ?? 0,
|
|
|
},
|
|
|
+ input: c.context_over_200k.input,
|
|
|
+ output: c.context_over_200k.output,
|
|
|
}
|
|
|
- if (c?.context_over_200k) {
|
|
|
- result.experimentalOver200K = {
|
|
|
- cache: {
|
|
|
- read: c.context_over_200k.cache_read ?? 0,
|
|
|
- write: c.context_over_200k.cache_write ?? 0,
|
|
|
- },
|
|
|
- input: c.context_over_200k.input,
|
|
|
- output: c.context_over_200k.output,
|
|
|
- }
|
|
|
- }
|
|
|
- return result
|
|
|
}
|
|
|
+ return result
|
|
|
+}
|
|
|
|
|
|
- function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
|
|
- const m: Model = {
|
|
|
- id: ModelID.make(model.id),
|
|
|
- providerID: ProviderID.make(provider.id),
|
|
|
- name: model.name,
|
|
|
- family: model.family,
|
|
|
- api: {
|
|
|
- id: model.id,
|
|
|
- url: model.provider?.api ?? provider.api!,
|
|
|
- npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
|
|
+function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
|
|
+ const m: Model = {
|
|
|
+ id: ModelID.make(model.id),
|
|
|
+ providerID: ProviderID.make(provider.id),
|
|
|
+ name: model.name,
|
|
|
+ family: model.family,
|
|
|
+ api: {
|
|
|
+ id: model.id,
|
|
|
+ url: model.provider?.api ?? provider.api!,
|
|
|
+ npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
|
|
+ },
|
|
|
+ status: model.status ?? "active",
|
|
|
+ headers: {},
|
|
|
+ options: {},
|
|
|
+ cost: cost(model.cost),
|
|
|
+ limit: {
|
|
|
+ context: model.limit.context,
|
|
|
+ input: model.limit.input,
|
|
|
+ output: model.limit.output,
|
|
|
+ },
|
|
|
+ capabilities: {
|
|
|
+ temperature: model.temperature,
|
|
|
+ reasoning: model.reasoning,
|
|
|
+ attachment: model.attachment,
|
|
|
+ toolcall: model.tool_call,
|
|
|
+ input: {
|
|
|
+ text: model.modalities?.input?.includes("text") ?? false,
|
|
|
+ audio: model.modalities?.input?.includes("audio") ?? false,
|
|
|
+ image: model.modalities?.input?.includes("image") ?? false,
|
|
|
+ video: model.modalities?.input?.includes("video") ?? false,
|
|
|
+ pdf: model.modalities?.input?.includes("pdf") ?? false,
|
|
|
},
|
|
|
- status: model.status ?? "active",
|
|
|
- headers: {},
|
|
|
- options: {},
|
|
|
- cost: cost(model.cost),
|
|
|
- limit: {
|
|
|
- context: model.limit.context,
|
|
|
- input: model.limit.input,
|
|
|
- output: model.limit.output,
|
|
|
- },
|
|
|
- capabilities: {
|
|
|
- temperature: model.temperature,
|
|
|
- reasoning: model.reasoning,
|
|
|
- attachment: model.attachment,
|
|
|
- toolcall: model.tool_call,
|
|
|
- input: {
|
|
|
- text: model.modalities?.input?.includes("text") ?? false,
|
|
|
- audio: model.modalities?.input?.includes("audio") ?? false,
|
|
|
- image: model.modalities?.input?.includes("image") ?? false,
|
|
|
- video: model.modalities?.input?.includes("video") ?? false,
|
|
|
- pdf: model.modalities?.input?.includes("pdf") ?? false,
|
|
|
- },
|
|
|
- output: {
|
|
|
- text: model.modalities?.output?.includes("text") ?? false,
|
|
|
- audio: model.modalities?.output?.includes("audio") ?? false,
|
|
|
- image: model.modalities?.output?.includes("image") ?? false,
|
|
|
- video: model.modalities?.output?.includes("video") ?? false,
|
|
|
- pdf: model.modalities?.output?.includes("pdf") ?? false,
|
|
|
- },
|
|
|
- interleaved: model.interleaved ?? false,
|
|
|
+ output: {
|
|
|
+ text: model.modalities?.output?.includes("text") ?? false,
|
|
|
+ audio: model.modalities?.output?.includes("audio") ?? false,
|
|
|
+ image: model.modalities?.output?.includes("image") ?? false,
|
|
|
+ video: model.modalities?.output?.includes("video") ?? false,
|
|
|
+ pdf: model.modalities?.output?.includes("pdf") ?? false,
|
|
|
},
|
|
|
- release_date: model.release_date,
|
|
|
- variants: {},
|
|
|
- }
|
|
|
+ interleaved: model.interleaved ?? false,
|
|
|
+ },
|
|
|
+ release_date: model.release_date,
|
|
|
+ variants: {},
|
|
|
+ }
|
|
|
|
|
|
- m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
|
|
|
+ m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
|
|
|
|
|
|
- return m
|
|
|
- }
|
|
|
+ return m
|
|
|
+}
|
|
|
|
|
|
- export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
- const models: Record<string, Model> = {}
|
|
|
- for (const [key, model] of Object.entries(provider.models)) {
|
|
|
- models[key] = fromModelsDevModel(provider, model)
|
|
|
- for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
|
|
|
- const id = `${model.id}-${mode}`
|
|
|
- const m = fromModelsDevModel(provider, model)
|
|
|
- m.id = ModelID.make(id)
|
|
|
- m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
|
|
|
- if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
|
|
|
- // convert body params to camelCase for ai sdk compatibility
|
|
|
- if (opts.provider?.body)
|
|
|
- m.options = Object.fromEntries(
|
|
|
- Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
|
|
|
- )
|
|
|
- if (opts.provider?.headers) m.headers = opts.provider.headers
|
|
|
- models[id] = m
|
|
|
- }
|
|
|
- }
|
|
|
- return {
|
|
|
- id: ProviderID.make(provider.id),
|
|
|
- source: "custom",
|
|
|
- name: provider.name,
|
|
|
- env: provider.env ?? [],
|
|
|
- options: {},
|
|
|
- models,
|
|
|
+export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|
|
+ const models: Record<string, Model> = {}
|
|
|
+ for (const [key, model] of Object.entries(provider.models)) {
|
|
|
+ models[key] = fromModelsDevModel(provider, model)
|
|
|
+ for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
|
|
|
+ const id = `${model.id}-${mode}`
|
|
|
+ const m = fromModelsDevModel(provider, model)
|
|
|
+ m.id = ModelID.make(id)
|
|
|
+ m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
|
|
|
+ if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
|
|
|
+ // convert body params to camelCase for ai sdk compatibility
|
|
|
+ if (opts.provider?.body)
|
|
|
+ m.options = Object.fromEntries(
|
|
|
+ Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
|
|
|
+ )
|
|
|
+ if (opts.provider?.headers) m.headers = opts.provider.headers
|
|
|
+ models[id] = m
|
|
|
}
|
|
|
}
|
|
|
+ return {
|
|
|
+ id: ProviderID.make(provider.id),
|
|
|
+ source: "custom",
|
|
|
+ name: provider.name,
|
|
|
+ env: provider.env ?? [],
|
|
|
+ options: {},
|
|
|
+ models,
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- const layer: Layer.Layer<
|
|
|
- Service,
|
|
|
- never,
|
|
|
- Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service
|
|
|
- > = Layer.effect(
|
|
|
- Service,
|
|
|
- Effect.gen(function* () {
|
|
|
- const fs = yield* AppFileSystem.Service
|
|
|
- const config = yield* Config.Service
|
|
|
- const auth = yield* Auth.Service
|
|
|
- const env = yield* Env.Service
|
|
|
- const plugin = yield* Plugin.Service
|
|
|
-
|
|
|
- const state = yield* InstanceState.make<State>(() =>
|
|
|
- Effect.gen(function* () {
|
|
|
- using _ = log.time("state")
|
|
|
- const bridge = yield* EffectBridge.make()
|
|
|
- const cfg = yield* config.get()
|
|
|
- const modelsDev = yield* Effect.promise(() => ModelsDev.get())
|
|
|
- const database = mapValues(modelsDev, fromModelsDevProvider)
|
|
|
-
|
|
|
- const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
|
|
|
- const languages = new Map<string, LanguageModelV3>()
|
|
|
- const modelLoaders: {
|
|
|
- [providerID: string]: CustomModelLoader
|
|
|
- } = {}
|
|
|
- const varsLoaders: {
|
|
|
- [providerID: string]: CustomVarsLoader
|
|
|
- } = {}
|
|
|
- const sdk = new Map<string, BundledSDK>()
|
|
|
- const discoveryLoaders: {
|
|
|
- [providerID: string]: CustomDiscoverModels
|
|
|
- } = {}
|
|
|
- const dep = {
|
|
|
- auth: (id: string) => auth.get(id).pipe(Effect.orDie),
|
|
|
- config: () => config.get(),
|
|
|
- env: () => env.all(),
|
|
|
- get: (key: string) => env.get(key),
|
|
|
- }
|
|
|
+const layer: Layer.Layer<
|
|
|
+ Service,
|
|
|
+ never,
|
|
|
+ Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service
|
|
|
+> = Layer.effect(
|
|
|
+ Service,
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const fs = yield* AppFileSystem.Service
|
|
|
+ const config = yield* Config.Service
|
|
|
+ const auth = yield* Auth.Service
|
|
|
+ const env = yield* Env.Service
|
|
|
+ const plugin = yield* Plugin.Service
|
|
|
+
|
|
|
+ const state = yield* InstanceState.make<State>(() =>
|
|
|
+ Effect.gen(function* () {
|
|
|
+ using _ = log.time("state")
|
|
|
+ const bridge = yield* EffectBridge.make()
|
|
|
+ const cfg = yield* config.get()
|
|
|
+ const modelsDev = yield* Effect.promise(() => ModelsDev.get())
|
|
|
+ const database = mapValues(modelsDev, fromModelsDevProvider)
|
|
|
+
|
|
|
+ const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
|
|
|
+ const languages = new Map<string, LanguageModelV3>()
|
|
|
+ const modelLoaders: {
|
|
|
+ [providerID: string]: CustomModelLoader
|
|
|
+ } = {}
|
|
|
+ const varsLoaders: {
|
|
|
+ [providerID: string]: CustomVarsLoader
|
|
|
+ } = {}
|
|
|
+ const sdk = new Map<string, BundledSDK>()
|
|
|
+ const discoveryLoaders: {
|
|
|
+ [providerID: string]: CustomDiscoverModels
|
|
|
+ } = {}
|
|
|
+ const dep = {
|
|
|
+ auth: (id: string) => auth.get(id).pipe(Effect.orDie),
|
|
|
+ config: () => config.get(),
|
|
|
+ env: () => env.all(),
|
|
|
+ get: (key: string) => env.get(key),
|
|
|
+ }
|
|
|
|
|
|
- log.info("init")
|
|
|
+ log.info("init")
|
|
|
|
|
|
- function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
|
|
- const existing = providers[providerID]
|
|
|
- if (existing) {
|
|
|
- // @ts-expect-error
|
|
|
- providers[providerID] = mergeDeep(existing, provider)
|
|
|
- return
|
|
|
- }
|
|
|
- const match = database[providerID]
|
|
|
- if (!match) return
|
|
|
+ function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
|
|
+ const existing = providers[providerID]
|
|
|
+ if (existing) {
|
|
|
// @ts-expect-error
|
|
|
- providers[providerID] = mergeDeep(match, provider)
|
|
|
+ providers[providerID] = mergeDeep(existing, provider)
|
|
|
+ return
|
|
|
}
|
|
|
+ const match = database[providerID]
|
|
|
+ if (!match) return
|
|
|
+ // @ts-expect-error
|
|
|
+ providers[providerID] = mergeDeep(match, provider)
|
|
|
+ }
|
|
|
|
|
|
- // load plugins first so config() hook runs before reading cfg.provider
|
|
|
- const plugins = yield* plugin.list()
|
|
|
+ // load plugins first so config() hook runs before reading cfg.provider
|
|
|
+ const plugins = yield* plugin.list()
|
|
|
|
|
|
- // now read config providers - includes any modifications from plugin config() hook
|
|
|
- const configProviders = Object.entries(cfg.provider ?? {})
|
|
|
- const disabled = new Set(cfg.disabled_providers ?? [])
|
|
|
- const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
|
|
|
+ // now read config providers - includes any modifications from plugin config() hook
|
|
|
+ const configProviders = Object.entries(cfg.provider ?? {})
|
|
|
+ const disabled = new Set(cfg.disabled_providers ?? [])
|
|
|
+ const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
|
|
|
|
|
|
- function isProviderAllowed(providerID: ProviderID): boolean {
|
|
|
- if (enabled && !enabled.has(providerID)) return false
|
|
|
- if (disabled.has(providerID)) return false
|
|
|
- return true
|
|
|
- }
|
|
|
+ function isProviderAllowed(providerID: ProviderID): boolean {
|
|
|
+ if (enabled && !enabled.has(providerID)) return false
|
|
|
+ if (disabled.has(providerID)) return false
|
|
|
+ return true
|
|
|
+ }
|
|
|
|
|
|
- // extend database from config
|
|
|
- for (const [providerID, provider] of configProviders) {
|
|
|
- const existing = database[providerID]
|
|
|
- const parsed: Info = {
|
|
|
- id: ProviderID.make(providerID),
|
|
|
- name: provider.name ?? existing?.name ?? providerID,
|
|
|
- env: provider.env ?? existing?.env ?? [],
|
|
|
- options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
|
|
|
- source: "config",
|
|
|
- models: existing?.models ?? {},
|
|
|
- }
|
|
|
+ // extend database from config
|
|
|
+ for (const [providerID, provider] of configProviders) {
|
|
|
+ const existing = database[providerID]
|
|
|
+ const parsed: Info = {
|
|
|
+ id: ProviderID.make(providerID),
|
|
|
+ name: provider.name ?? existing?.name ?? providerID,
|
|
|
+ env: provider.env ?? existing?.env ?? [],
|
|
|
+ options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
|
|
|
+ source: "config",
|
|
|
+ models: existing?.models ?? {},
|
|
|
+ }
|
|
|
|
|
|
- for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
|
|
- const existingModel = parsed.models[model.id ?? modelID]
|
|
|
- const name = iife(() => {
|
|
|
- if (model.name) return model.name
|
|
|
- if (model.id && model.id !== modelID) return modelID
|
|
|
- return existingModel?.name ?? modelID
|
|
|
- })
|
|
|
- const parsedModel: Model = {
|
|
|
- id: ModelID.make(modelID),
|
|
|
- api: {
|
|
|
- id: model.id ?? existingModel?.api.id ?? modelID,
|
|
|
- npm:
|
|
|
- model.provider?.npm ??
|
|
|
- provider.npm ??
|
|
|
- existingModel?.api.npm ??
|
|
|
- modelsDev[providerID]?.npm ??
|
|
|
- "@ai-sdk/openai-compatible",
|
|
|
- url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
|
|
|
- },
|
|
|
- status: model.status ?? existingModel?.status ?? "active",
|
|
|
- name,
|
|
|
- providerID: ProviderID.make(providerID),
|
|
|
- capabilities: {
|
|
|
- temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
|
|
- reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
|
|
- attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
|
|
- toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
|
|
- input: {
|
|
|
- text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
|
|
|
- audio:
|
|
|
- model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
|
|
|
- image:
|
|
|
- model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
|
|
|
- video:
|
|
|
- model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
|
|
|
- pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
|
|
|
- },
|
|
|
- output: {
|
|
|
- text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
|
|
|
- audio:
|
|
|
- model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
|
|
|
- image:
|
|
|
- model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
|
|
|
- video:
|
|
|
- model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
|
|
|
- pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
|
|
|
- },
|
|
|
- interleaved: model.interleaved ?? false,
|
|
|
+ for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
|
|
+ const existingModel = parsed.models[model.id ?? modelID]
|
|
|
+ const name = iife(() => {
|
|
|
+ if (model.name) return model.name
|
|
|
+ if (model.id && model.id !== modelID) return modelID
|
|
|
+ return existingModel?.name ?? modelID
|
|
|
+ })
|
|
|
+ const parsedModel: Model = {
|
|
|
+ id: ModelID.make(modelID),
|
|
|
+ api: {
|
|
|
+ id: model.id ?? existingModel?.api.id ?? modelID,
|
|
|
+ npm:
|
|
|
+ model.provider?.npm ??
|
|
|
+ provider.npm ??
|
|
|
+ existingModel?.api.npm ??
|
|
|
+ modelsDev[providerID]?.npm ??
|
|
|
+ "@ai-sdk/openai-compatible",
|
|
|
+ url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
|
|
|
+ },
|
|
|
+ status: model.status ?? existingModel?.status ?? "active",
|
|
|
+ name,
|
|
|
+ providerID: ProviderID.make(providerID),
|
|
|
+ capabilities: {
|
|
|
+ temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
|
|
+ reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
|
|
+ attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
|
|
+ toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
|
|
+ input: {
|
|
|
+ text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
|
|
|
+ audio:
|
|
|
+ model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
|
|
|
+ image:
|
|
|
+ model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
|
|
|
+ video:
|
|
|
+ model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
|
|
|
+ pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
|
|
|
},
|
|
|
- cost: {
|
|
|
- input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
|
|
|
- output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
|
|
|
- cache: {
|
|
|
- read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
|
|
|
- write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
|
|
|
- },
|
|
|
+ output: {
|
|
|
+ text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
|
|
|
+ audio:
|
|
|
+ model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
|
|
|
+ image:
|
|
|
+ model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
|
|
|
+ video:
|
|
|
+ model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
|
|
|
+ pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
|
|
|
},
|
|
|
- options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
|
|
|
- limit: {
|
|
|
- context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
|
|
|
- input: model.limit?.input ?? existingModel?.limit?.input,
|
|
|
- output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
|
|
|
+ interleaved: model.interleaved ?? false,
|
|
|
+ },
|
|
|
+ cost: {
|
|
|
+ input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
|
|
|
+ output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
|
|
|
+ cache: {
|
|
|
+ read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
|
|
|
+ write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
|
|
|
},
|
|
|
- headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
|
|
|
- family: model.family ?? existingModel?.family ?? "",
|
|
|
- release_date: model.release_date ?? existingModel?.release_date ?? "",
|
|
|
- variants: {},
|
|
|
- }
|
|
|
- const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
|
|
|
- parsedModel.variants = mapValues(
|
|
|
- pickBy(merged, (v) => !v.disabled),
|
|
|
- (v) => omit(v, ["disabled"]),
|
|
|
- )
|
|
|
- parsed.models[modelID] = parsedModel
|
|
|
+ },
|
|
|
+ options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
|
|
|
+ limit: {
|
|
|
+ context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
|
|
|
+ input: model.limit?.input ?? existingModel?.limit?.input,
|
|
|
+ output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
|
|
|
+ },
|
|
|
+ headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
|
|
|
+ family: model.family ?? existingModel?.family ?? "",
|
|
|
+ release_date: model.release_date ?? existingModel?.release_date ?? "",
|
|
|
+ variants: {},
|
|
|
}
|
|
|
- database[providerID] = parsed
|
|
|
+ const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
|
|
|
+ parsedModel.variants = mapValues(
|
|
|
+ pickBy(merged, (v) => !v.disabled),
|
|
|
+ (v) => omit(v, ["disabled"]),
|
|
|
+ )
|
|
|
+ parsed.models[modelID] = parsedModel
|
|
|
}
|
|
|
+ database[providerID] = parsed
|
|
|
+ }
|
|
|
|
|
|
- // load env
|
|
|
- const envs = yield* env.all()
|
|
|
- for (const [id, provider] of Object.entries(database)) {
|
|
|
- const providerID = ProviderID.make(id)
|
|
|
- if (disabled.has(providerID)) continue
|
|
|
- const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
|
|
|
- if (!apiKey) continue
|
|
|
+ // load env
|
|
|
+ const envs = yield* env.all()
|
|
|
+ for (const [id, provider] of Object.entries(database)) {
|
|
|
+ const providerID = ProviderID.make(id)
|
|
|
+ if (disabled.has(providerID)) continue
|
|
|
+ const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
|
|
|
+ if (!apiKey) continue
|
|
|
+ mergeProvider(providerID, {
|
|
|
+ source: "env",
|
|
|
+ key: provider.env.length === 1 ? apiKey : undefined,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // load apikeys
|
|
|
+ const auths = yield* auth.all().pipe(Effect.orDie)
|
|
|
+ for (const [id, provider] of Object.entries(auths)) {
|
|
|
+ const providerID = ProviderID.make(id)
|
|
|
+ if (disabled.has(providerID)) continue
|
|
|
+ if (provider.type === "api") {
|
|
|
mergeProvider(providerID, {
|
|
|
- source: "env",
|
|
|
- key: provider.env.length === 1 ? apiKey : undefined,
|
|
|
+ source: "api",
|
|
|
+ key: provider.key,
|
|
|
})
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // load apikeys
|
|
|
- const auths = yield* auth.all().pipe(Effect.orDie)
|
|
|
- for (const [id, provider] of Object.entries(auths)) {
|
|
|
- const providerID = ProviderID.make(id)
|
|
|
- if (disabled.has(providerID)) continue
|
|
|
- if (provider.type === "api") {
|
|
|
- mergeProvider(providerID, {
|
|
|
- source: "api",
|
|
|
- key: provider.key,
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
+ // plugin auth loader - database now has entries for config providers
|
|
|
+ for (const plugin of plugins) {
|
|
|
+ if (!plugin.auth) continue
|
|
|
+ const providerID = ProviderID.make(plugin.auth.provider)
|
|
|
+ if (disabled.has(providerID)) continue
|
|
|
+
|
|
|
+ const stored = yield* auth.get(providerID).pipe(Effect.orDie)
|
|
|
+ if (!stored) continue
|
|
|
+ if (!plugin.auth.loader) continue
|
|
|
+
|
|
|
+ const options = yield* Effect.promise(() =>
|
|
|
+ plugin.auth!.loader!(
|
|
|
+ () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
|
|
|
+ database[plugin.auth!.provider],
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ const opts = options ?? {}
|
|
|
+ const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
|
|
|
+ mergeProvider(providerID, patch)
|
|
|
+ }
|
|
|
|
|
|
- // plugin auth loader - database now has entries for config providers
|
|
|
- for (const plugin of plugins) {
|
|
|
- if (!plugin.auth) continue
|
|
|
- const providerID = ProviderID.make(plugin.auth.provider)
|
|
|
- if (disabled.has(providerID)) continue
|
|
|
-
|
|
|
- const stored = yield* auth.get(providerID).pipe(Effect.orDie)
|
|
|
- if (!stored) continue
|
|
|
- if (!plugin.auth.loader) continue
|
|
|
-
|
|
|
- const options = yield* Effect.promise(() =>
|
|
|
- plugin.auth!.loader!(
|
|
|
- () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
|
|
|
- database[plugin.auth!.provider],
|
|
|
- ),
|
|
|
- )
|
|
|
- const opts = options ?? {}
|
|
|
- const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
|
|
|
- mergeProvider(providerID, patch)
|
|
|
+ for (const [id, fn] of Object.entries(custom(dep))) {
|
|
|
+ const providerID = ProviderID.make(id)
|
|
|
+ if (disabled.has(providerID)) continue
|
|
|
+ const data = database[providerID]
|
|
|
+ if (!data) {
|
|
|
+ log.error("Provider does not exist in model list " + providerID)
|
|
|
+ continue
|
|
|
}
|
|
|
-
|
|
|
- for (const [id, fn] of Object.entries(custom(dep))) {
|
|
|
- const providerID = ProviderID.make(id)
|
|
|
- if (disabled.has(providerID)) continue
|
|
|
- const data = database[providerID]
|
|
|
- if (!data) {
|
|
|
- log.error("Provider does not exist in model list " + providerID)
|
|
|
- continue
|
|
|
- }
|
|
|
- const result = yield* fn(data)
|
|
|
- if (result && (result.autoload || providers[providerID])) {
|
|
|
- if (result.getModel) modelLoaders[providerID] = result.getModel
|
|
|
- if (result.vars) varsLoaders[providerID] = result.vars
|
|
|
- if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
|
|
|
- const opts = result.options ?? {}
|
|
|
- const patch: Partial<Info> = providers[providerID]
|
|
|
- ? { options: opts }
|
|
|
- : { source: "custom", options: opts }
|
|
|
- mergeProvider(providerID, patch)
|
|
|
- }
|
|
|
+ const result = yield* fn(data)
|
|
|
+ if (result && (result.autoload || providers[providerID])) {
|
|
|
+ if (result.getModel) modelLoaders[providerID] = result.getModel
|
|
|
+ if (result.vars) varsLoaders[providerID] = result.vars
|
|
|
+ if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
|
|
|
+ const opts = result.options ?? {}
|
|
|
+ const patch: Partial<Info> = providers[providerID]
|
|
|
+ ? { options: opts }
|
|
|
+ : { source: "custom", options: opts }
|
|
|
+ mergeProvider(providerID, patch)
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // load config - re-apply with updated data
|
|
|
- for (const [id, provider] of configProviders) {
|
|
|
- const providerID = ProviderID.make(id)
|
|
|
- const partial: Partial<Info> = { source: "config" }
|
|
|
- if (provider.env) partial.env = provider.env
|
|
|
- if (provider.name) partial.name = provider.name
|
|
|
- if (provider.options) partial.options = provider.options
|
|
|
- mergeProvider(providerID, partial)
|
|
|
- }
|
|
|
+ // load config - re-apply with updated data
|
|
|
+ for (const [id, provider] of configProviders) {
|
|
|
+ const providerID = ProviderID.make(id)
|
|
|
+ const partial: Partial<Info> = { source: "config" }
|
|
|
+ if (provider.env) partial.env = provider.env
|
|
|
+ if (provider.name) partial.name = provider.name
|
|
|
+ if (provider.options) partial.options = provider.options
|
|
|
+ mergeProvider(providerID, partial)
|
|
|
+ }
|
|
|
|
|
|
- const gitlab = ProviderID.make("gitlab")
|
|
|
- if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
|
|
|
- yield* Effect.promise(async () => {
|
|
|
- try {
|
|
|
- const discovered = await discoveryLoaders[gitlab]()
|
|
|
- for (const [modelID, model] of Object.entries(discovered)) {
|
|
|
- if (!providers[gitlab].models[modelID]) {
|
|
|
- providers[gitlab].models[modelID] = model
|
|
|
- }
|
|
|
+ const gitlab = ProviderID.make("gitlab")
|
|
|
+ if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
|
|
|
+ yield* Effect.promise(async () => {
|
|
|
+ try {
|
|
|
+ const discovered = await discoveryLoaders[gitlab]()
|
|
|
+ for (const [modelID, model] of Object.entries(discovered)) {
|
|
|
+ if (!providers[gitlab].models[modelID]) {
|
|
|
+ providers[gitlab].models[modelID] = model
|
|
|
}
|
|
|
- } catch (e) {
|
|
|
- log.warn("state discovery error", { id: "gitlab", error: e })
|
|
|
}
|
|
|
- })
|
|
|
- }
|
|
|
+ } catch (e) {
|
|
|
+ log.warn("state discovery error", { id: "gitlab", error: e })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- for (const hook of plugins) {
|
|
|
- const p = hook.provider
|
|
|
- const models = p?.models
|
|
|
- if (!p || !models) continue
|
|
|
-
|
|
|
- const providerID = ProviderID.make(p.id)
|
|
|
- if (disabled.has(providerID)) continue
|
|
|
-
|
|
|
- const provider = providers[providerID]
|
|
|
- if (!provider) continue
|
|
|
- const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
|
|
|
-
|
|
|
- provider.models = yield* Effect.promise(async () => {
|
|
|
- const next = await models(provider, { auth: pluginAuth })
|
|
|
- return Object.fromEntries(
|
|
|
- Object.entries(next).map(([id, model]) => [
|
|
|
- id,
|
|
|
- {
|
|
|
- ...model,
|
|
|
- id: ModelID.make(id),
|
|
|
- providerID,
|
|
|
- },
|
|
|
- ]),
|
|
|
- )
|
|
|
- })
|
|
|
- }
|
|
|
+ for (const hook of plugins) {
|
|
|
+ const p = hook.provider
|
|
|
+ const models = p?.models
|
|
|
+ if (!p || !models) continue
|
|
|
|
|
|
- for (const [id, provider] of Object.entries(providers)) {
|
|
|
- const providerID = ProviderID.make(id)
|
|
|
- if (!isProviderAllowed(providerID)) {
|
|
|
- delete providers[providerID]
|
|
|
- continue
|
|
|
- }
|
|
|
+ const providerID = ProviderID.make(p.id)
|
|
|
+ if (disabled.has(providerID)) continue
|
|
|
|
|
|
- const configProvider = cfg.provider?.[providerID]
|
|
|
+ const provider = providers[providerID]
|
|
|
+ if (!provider) continue
|
|
|
+ const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
|
|
|
|
|
|
- for (const [modelID, model] of Object.entries(provider.models)) {
|
|
|
- model.api.id = model.api.id ?? model.id ?? modelID
|
|
|
- if (
|
|
|
- modelID === "gpt-5-chat-latest" ||
|
|
|
- (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
|
|
|
- )
|
|
|
- delete provider.models[modelID]
|
|
|
- if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
|
|
|
- if (model.status === "deprecated") delete provider.models[modelID]
|
|
|
- if (
|
|
|
- (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
|
|
|
- (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
|
|
|
- )
|
|
|
- delete provider.models[modelID]
|
|
|
+ provider.models = yield* Effect.promise(async () => {
|
|
|
+ const next = await models(provider, { auth: pluginAuth })
|
|
|
+ return Object.fromEntries(
|
|
|
+ Object.entries(next).map(([id, model]) => [
|
|
|
+ id,
|
|
|
+ {
|
|
|
+ ...model,
|
|
|
+ id: ModelID.make(id),
|
|
|
+ providerID,
|
|
|
+ },
|
|
|
+ ]),
|
|
|
+ )
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
|
|
|
+ for (const [id, provider] of Object.entries(providers)) {
|
|
|
+ const providerID = ProviderID.make(id)
|
|
|
+ if (!isProviderAllowed(providerID)) {
|
|
|
+ delete providers[providerID]
|
|
|
+ continue
|
|
|
+ }
|
|
|
|
|
|
- const configVariants = configProvider?.models?.[modelID]?.variants
|
|
|
- if (configVariants && model.variants) {
|
|
|
- const merged = mergeDeep(model.variants, configVariants)
|
|
|
- model.variants = mapValues(
|
|
|
- pickBy(merged, (v) => !v.disabled),
|
|
|
- (v) => omit(v, ["disabled"]),
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
+ const configProvider = cfg.provider?.[providerID]
|
|
|
|
|
|
- if (Object.keys(provider.models).length === 0) {
|
|
|
- delete providers[providerID]
|
|
|
- continue
|
|
|
- }
|
|
|
+ for (const [modelID, model] of Object.entries(provider.models)) {
|
|
|
+ model.api.id = model.api.id ?? model.id ?? modelID
|
|
|
+ if (
|
|
|
+ modelID === "gpt-5-chat-latest" ||
|
|
|
+ (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
|
|
|
+ )
|
|
|
+ delete provider.models[modelID]
|
|
|
+ if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
|
|
|
+ if (model.status === "deprecated") delete provider.models[modelID]
|
|
|
+ if (
|
|
|
+ (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
|
|
|
+ (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
|
|
|
+ )
|
|
|
+ delete provider.models[modelID]
|
|
|
|
|
|
- log.info("found", { providerID })
|
|
|
+ model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
|
|
|
+
|
|
|
+ const configVariants = configProvider?.models?.[modelID]?.variants
|
|
|
+ if (configVariants && model.variants) {
|
|
|
+ const merged = mergeDeep(model.variants, configVariants)
|
|
|
+ model.variants = mapValues(
|
|
|
+ pickBy(merged, (v) => !v.disabled),
|
|
|
+ (v) => omit(v, ["disabled"]),
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return {
|
|
|
- models: languages,
|
|
|
- providers,
|
|
|
- sdk,
|
|
|
- modelLoaders,
|
|
|
- varsLoaders,
|
|
|
+ if (Object.keys(provider.models).length === 0) {
|
|
|
+ delete providers[providerID]
|
|
|
+ continue
|
|
|
}
|
|
|
- }),
|
|
|
- )
|
|
|
|
|
|
- const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
|
|
|
+ log.info("found", { providerID })
|
|
|
+ }
|
|
|
|
|
|
- async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
|
|
|
- try {
|
|
|
- using _ = log.time("getSDK", {
|
|
|
- providerID: model.providerID,
|
|
|
- })
|
|
|
- const provider = s.providers[model.providerID]
|
|
|
- const options = { ...provider.options }
|
|
|
+ return {
|
|
|
+ models: languages,
|
|
|
+ providers,
|
|
|
+ sdk,
|
|
|
+ modelLoaders,
|
|
|
+ varsLoaders,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ )
|
|
|
|
|
|
- if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
|
|
|
- delete options.fetch
|
|
|
- }
|
|
|
+ const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
|
|
|
|
|
|
- if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
|
|
|
- options["includeUsage"] = true
|
|
|
- }
|
|
|
+ async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
|
|
|
+ try {
|
|
|
+ using _ = log.time("getSDK", {
|
|
|
+ providerID: model.providerID,
|
|
|
+ })
|
|
|
+ const provider = s.providers[model.providerID]
|
|
|
+ const options = { ...provider.options }
|
|
|
|
|
|
- const baseURL = iife(() => {
|
|
|
- let url =
|
|
|
- typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
|
|
|
- if (!url) return
|
|
|
-
|
|
|
- const loader = s.varsLoaders[model.providerID]
|
|
|
- if (loader) {
|
|
|
- const vars = loader(options)
|
|
|
- for (const [key, value] of Object.entries(vars)) {
|
|
|
- const field = "${" + key + "}"
|
|
|
- url = url.replaceAll(field, value)
|
|
|
- }
|
|
|
+ if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
|
|
|
+ delete options.fetch
|
|
|
+ }
|
|
|
+
|
|
|
+ if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
|
|
|
+ options["includeUsage"] = true
|
|
|
+ }
|
|
|
+
|
|
|
+ const baseURL = iife(() => {
|
|
|
+ let url =
|
|
|
+ typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
|
|
|
+ if (!url) return
|
|
|
+
|
|
|
+ const loader = s.varsLoaders[model.providerID]
|
|
|
+ if (loader) {
|
|
|
+ const vars = loader(options)
|
|
|
+ for (const [key, value] of Object.entries(vars)) {
|
|
|
+ const field = "${" + key + "}"
|
|
|
+ url = url.replaceAll(field, value)
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
|
|
|
- const val = envs[String(key)]
|
|
|
- return val ?? item
|
|
|
- })
|
|
|
- return url
|
|
|
+ url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
|
|
|
+ const val = envs[String(key)]
|
|
|
+ return val ?? item
|
|
|
})
|
|
|
+ return url
|
|
|
+ })
|
|
|
|
|
|
- if (baseURL !== undefined) options["baseURL"] = baseURL
|
|
|
- if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
|
|
|
- if (model.headers)
|
|
|
- options["headers"] = {
|
|
|
- ...options["headers"],
|
|
|
- ...model.headers,
|
|
|
- }
|
|
|
+ if (baseURL !== undefined) options["baseURL"] = baseURL
|
|
|
+ if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
|
|
|
+ if (model.headers)
|
|
|
+ options["headers"] = {
|
|
|
+ ...options["headers"],
|
|
|
+ ...model.headers,
|
|
|
+ }
|
|
|
|
|
|
- const key = Hash.fast(
|
|
|
- JSON.stringify({
|
|
|
- providerID: model.providerID,
|
|
|
- npm: model.api.npm,
|
|
|
- options,
|
|
|
- }),
|
|
|
- )
|
|
|
- const existing = s.sdk.get(key)
|
|
|
- if (existing) return existing
|
|
|
-
|
|
|
- const customFetch = options["fetch"]
|
|
|
- const chunkTimeout = options["chunkTimeout"]
|
|
|
- delete options["chunkTimeout"]
|
|
|
-
|
|
|
- options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
|
|
- const fetchFn = customFetch ?? fetch
|
|
|
- const opts = init ?? {}
|
|
|
- const chunkAbortCtl =
|
|
|
- typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
|
|
|
- const signals: AbortSignal[] = []
|
|
|
-
|
|
|
- if (opts.signal) signals.push(opts.signal)
|
|
|
- if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
|
|
|
- if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
|
|
|
- signals.push(AbortSignal.timeout(options["timeout"]))
|
|
|
-
|
|
|
- const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
|
|
|
- if (combined) opts.signal = combined
|
|
|
-
|
|
|
- // Strip openai itemId metadata following what codex does
|
|
|
- if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
|
|
|
- const body = JSON.parse(opts.body as string)
|
|
|
- const isAzure = model.providerID.includes("azure")
|
|
|
- const keepIds = isAzure && body.store === true
|
|
|
- if (!keepIds && Array.isArray(body.input)) {
|
|
|
- for (const item of body.input) {
|
|
|
- if ("id" in item) {
|
|
|
- delete item.id
|
|
|
- }
|
|
|
+ const key = Hash.fast(
|
|
|
+ JSON.stringify({
|
|
|
+ providerID: model.providerID,
|
|
|
+ npm: model.api.npm,
|
|
|
+ options,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ const existing = s.sdk.get(key)
|
|
|
+ if (existing) return existing
|
|
|
+
|
|
|
+ const customFetch = options["fetch"]
|
|
|
+ const chunkTimeout = options["chunkTimeout"]
|
|
|
+ delete options["chunkTimeout"]
|
|
|
+
|
|
|
+ options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
|
|
+ const fetchFn = customFetch ?? fetch
|
|
|
+ const opts = init ?? {}
|
|
|
+ const chunkAbortCtl =
|
|
|
+ typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
|
|
|
+ const signals: AbortSignal[] = []
|
|
|
+
|
|
|
+ if (opts.signal) signals.push(opts.signal)
|
|
|
+ if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
|
|
|
+ if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
|
|
|
+ signals.push(AbortSignal.timeout(options["timeout"]))
|
|
|
+
|
|
|
+ const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
|
|
|
+ if (combined) opts.signal = combined
|
|
|
+
|
|
|
+ // Strip openai itemId metadata following what codex does
|
|
|
+ if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
|
|
|
+ const body = JSON.parse(opts.body as string)
|
|
|
+ const isAzure = model.providerID.includes("azure")
|
|
|
+ const keepIds = isAzure && body.store === true
|
|
|
+ if (!keepIds && Array.isArray(body.input)) {
|
|
|
+ for (const item of body.input) {
|
|
|
+ if ("id" in item) {
|
|
|
+ delete item.id
|
|
|
}
|
|
|
- opts.body = JSON.stringify(body)
|
|
|
}
|
|
|
+ opts.body = JSON.stringify(body)
|
|
|
}
|
|
|
-
|
|
|
- const res = await fetchFn(input, {
|
|
|
- ...opts,
|
|
|
- // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
|
- timeout: false,
|
|
|
- })
|
|
|
-
|
|
|
- if (!chunkAbortCtl) return res
|
|
|
- return wrapSSE(res, chunkTimeout, chunkAbortCtl)
|
|
|
- }
|
|
|
-
|
|
|
- const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
|
|
|
- if (bundledFn) {
|
|
|
- log.info("using bundled provider", {
|
|
|
- providerID: model.providerID,
|
|
|
- pkg: model.api.npm,
|
|
|
- })
|
|
|
- const loaded = bundledFn({
|
|
|
- name: model.providerID,
|
|
|
- ...options,
|
|
|
- })
|
|
|
- s.sdk.set(key, loaded)
|
|
|
- return loaded as SDK
|
|
|
}
|
|
|
|
|
|
- let installedPath: string
|
|
|
- if (!model.api.npm.startsWith("file://")) {
|
|
|
- const item = await Npm.add(model.api.npm)
|
|
|
- if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
|
|
|
- installedPath = item.entrypoint
|
|
|
- } else {
|
|
|
- log.info("loading local provider", { pkg: model.api.npm })
|
|
|
- installedPath = model.api.npm
|
|
|
- }
|
|
|
+ const res = await fetchFn(input, {
|
|
|
+ ...opts,
|
|
|
+ // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
|
+ timeout: false,
|
|
|
+ })
|
|
|
|
|
|
- const mod = await import(installedPath)
|
|
|
+ if (!chunkAbortCtl) return res
|
|
|
+ return wrapSSE(res, chunkTimeout, chunkAbortCtl)
|
|
|
+ }
|
|
|
|
|
|
- const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
|
|
- const loaded = fn({
|
|
|
+ const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
|
|
|
+ if (bundledFn) {
|
|
|
+ log.info("using bundled provider", {
|
|
|
+ providerID: model.providerID,
|
|
|
+ pkg: model.api.npm,
|
|
|
+ })
|
|
|
+ const loaded = bundledFn({
|
|
|
name: model.providerID,
|
|
|
...options,
|
|
|
})
|
|
|
s.sdk.set(key, loaded)
|
|
|
return loaded as SDK
|
|
|
- } catch (e) {
|
|
|
- throw new InitError({ providerID: model.providerID }, { cause: e })
|
|
|
}
|
|
|
+
|
|
|
+ let installedPath: string
|
|
|
+ if (!model.api.npm.startsWith("file://")) {
|
|
|
+ const item = await Npm.add(model.api.npm)
|
|
|
+ if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
|
|
|
+ installedPath = item.entrypoint
|
|
|
+ } else {
|
|
|
+ log.info("loading local provider", { pkg: model.api.npm })
|
|
|
+ installedPath = model.api.npm
|
|
|
+ }
|
|
|
+
|
|
|
+ const mod = await import(installedPath)
|
|
|
+
|
|
|
+ const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
|
|
+ const loaded = fn({
|
|
|
+ name: model.providerID,
|
|
|
+ ...options,
|
|
|
+ })
|
|
|
+ s.sdk.set(key, loaded)
|
|
|
+ return loaded as SDK
|
|
|
+ } catch (e) {
|
|
|
+ throw new InitError({ providerID: model.providerID }, { cause: e })
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
|
|
|
- InstanceState.use(state, (s) => s.providers[providerID]),
|
|
|
- )
|
|
|
+ const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
|
|
|
+ InstanceState.use(state, (s) => s.providers[providerID]),
|
|
|
+ )
|
|
|
|
|
|
- const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
|
|
|
- const s = yield* InstanceState.get(state)
|
|
|
- const provider = s.providers[providerID]
|
|
|
- if (!provider) {
|
|
|
- const available = Object.keys(s.providers)
|
|
|
- const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
|
|
|
- throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
|
|
|
- }
|
|
|
+ const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
|
|
|
+ const s = yield* InstanceState.get(state)
|
|
|
+ const provider = s.providers[providerID]
|
|
|
+ if (!provider) {
|
|
|
+ const available = Object.keys(s.providers)
|
|
|
+ const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
|
|
|
+ throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
|
|
|
+ }
|
|
|
|
|
|
- const info = provider.models[modelID]
|
|
|
- if (!info) {
|
|
|
- const available = Object.keys(provider.models)
|
|
|
- const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
|
|
|
- throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
|
|
|
- }
|
|
|
- return info
|
|
|
- })
|
|
|
+ const info = provider.models[modelID]
|
|
|
+ if (!info) {
|
|
|
+ const available = Object.keys(provider.models)
|
|
|
+ const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
|
|
|
+ throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
|
|
|
+ }
|
|
|
+ return info
|
|
|
+ })
|
|
|
|
|
|
- const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
|
|
|
- const s = yield* InstanceState.get(state)
|
|
|
- const envs = yield* env.all()
|
|
|
- const key = `${model.providerID}/${model.id}`
|
|
|
- if (s.models.has(key)) return s.models.get(key)!
|
|
|
+ const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
|
|
|
+ const s = yield* InstanceState.get(state)
|
|
|
+ const envs = yield* env.all()
|
|
|
+ const key = `${model.providerID}/${model.id}`
|
|
|
+ if (s.models.has(key)) return s.models.get(key)!
|
|
|
|
|
|
- return yield* Effect.promise(async () => {
|
|
|
- const provider = s.providers[model.providerID]
|
|
|
- const sdk = await resolveSDK(model, s, envs)
|
|
|
+ return yield* Effect.promise(async () => {
|
|
|
+ const provider = s.providers[model.providerID]
|
|
|
+ const sdk = await resolveSDK(model, s, envs)
|
|
|
|
|
|
- try {
|
|
|
- const language = s.modelLoaders[model.providerID]
|
|
|
- ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
|
|
|
- ...provider.options,
|
|
|
- ...model.options,
|
|
|
- })
|
|
|
- : sdk.languageModel(model.api.id)
|
|
|
- s.models.set(key, language)
|
|
|
- return language
|
|
|
- } catch (e) {
|
|
|
- if (e instanceof NoSuchModelError)
|
|
|
- throw new ModelNotFoundError(
|
|
|
- {
|
|
|
- modelID: model.id,
|
|
|
- providerID: model.providerID,
|
|
|
- },
|
|
|
- { cause: e },
|
|
|
- )
|
|
|
- throw e
|
|
|
- }
|
|
|
- })
|
|
|
+ try {
|
|
|
+ const language = s.modelLoaders[model.providerID]
|
|
|
+ ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
|
|
|
+ ...provider.options,
|
|
|
+ ...model.options,
|
|
|
+ })
|
|
|
+ : sdk.languageModel(model.api.id)
|
|
|
+ s.models.set(key, language)
|
|
|
+ return language
|
|
|
+ } catch (e) {
|
|
|
+ if (e instanceof NoSuchModelError)
|
|
|
+ throw new ModelNotFoundError(
|
|
|
+ {
|
|
|
+ modelID: model.id,
|
|
|
+ providerID: model.providerID,
|
|
|
+ },
|
|
|
+ { cause: e },
|
|
|
+ )
|
|
|
+ throw e
|
|
|
+ }
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
|
|
|
- const s = yield* InstanceState.get(state)
|
|
|
- const provider = s.providers[providerID]
|
|
|
- if (!provider) return undefined
|
|
|
- for (const item of query) {
|
|
|
- for (const modelID of Object.keys(provider.models)) {
|
|
|
- if (modelID.includes(item)) return { providerID, modelID }
|
|
|
- }
|
|
|
+ const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
|
|
|
+ const s = yield* InstanceState.get(state)
|
|
|
+ const provider = s.providers[providerID]
|
|
|
+ if (!provider) return undefined
|
|
|
+ for (const item of query) {
|
|
|
+ for (const modelID of Object.keys(provider.models)) {
|
|
|
+ if (modelID.includes(item)) return { providerID, modelID }
|
|
|
}
|
|
|
- return undefined
|
|
|
- })
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
|
|
|
- const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
|
|
|
- const cfg = yield* config.get()
|
|
|
+ const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
|
|
|
+ const cfg = yield* config.get()
|
|
|
|
|
|
- if (cfg.small_model) {
|
|
|
- const parsed = parseModel(cfg.small_model)
|
|
|
- return yield* getModel(parsed.providerID, parsed.modelID)
|
|
|
- }
|
|
|
+ if (cfg.small_model) {
|
|
|
+ const parsed = parseModel(cfg.small_model)
|
|
|
+ return yield* getModel(parsed.providerID, parsed.modelID)
|
|
|
+ }
|
|
|
|
|
|
- const s = yield* InstanceState.get(state)
|
|
|
- const provider = s.providers[providerID]
|
|
|
- if (!provider) return undefined
|
|
|
-
|
|
|
- let priority = [
|
|
|
- "claude-haiku-4-5",
|
|
|
- "claude-haiku-4.5",
|
|
|
- "3-5-haiku",
|
|
|
- "3.5-haiku",
|
|
|
- "gemini-3-flash",
|
|
|
- "gemini-2.5-flash",
|
|
|
- "gpt-5-nano",
|
|
|
- ]
|
|
|
- if (providerID.startsWith("opencode")) {
|
|
|
- priority = ["gpt-5-nano"]
|
|
|
- }
|
|
|
- if (providerID.startsWith("github-copilot")) {
|
|
|
- priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
|
|
|
- }
|
|
|
- for (const item of priority) {
|
|
|
- if (providerID === ProviderID.amazonBedrock) {
|
|
|
- const crossRegionPrefixes = ["global.", "us.", "eu."]
|
|
|
- const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
|
|
-
|
|
|
- const globalMatch = candidates.find((m) => m.startsWith("global."))
|
|
|
- if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
|
|
|
-
|
|
|
- const region = provider.options?.region
|
|
|
- if (region) {
|
|
|
- const regionPrefix = region.split("-")[0]
|
|
|
- if (regionPrefix === "us" || regionPrefix === "eu") {
|
|
|
- const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
|
|
|
- if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
|
|
|
- }
|
|
|
+ const s = yield* InstanceState.get(state)
|
|
|
+ const provider = s.providers[providerID]
|
|
|
+ if (!provider) return undefined
|
|
|
+
|
|
|
+ let priority = [
|
|
|
+ "claude-haiku-4-5",
|
|
|
+ "claude-haiku-4.5",
|
|
|
+ "3-5-haiku",
|
|
|
+ "3.5-haiku",
|
|
|
+ "gemini-3-flash",
|
|
|
+ "gemini-2.5-flash",
|
|
|
+ "gpt-5-nano",
|
|
|
+ ]
|
|
|
+ if (providerID.startsWith("opencode")) {
|
|
|
+ priority = ["gpt-5-nano"]
|
|
|
+ }
|
|
|
+ if (providerID.startsWith("github-copilot")) {
|
|
|
+ priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
|
|
|
+ }
|
|
|
+ for (const item of priority) {
|
|
|
+ if (providerID === ProviderID.amazonBedrock) {
|
|
|
+ const crossRegionPrefixes = ["global.", "us.", "eu."]
|
|
|
+ const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
|
|
+
|
|
|
+ const globalMatch = candidates.find((m) => m.startsWith("global."))
|
|
|
+ if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
|
|
|
+
|
|
|
+ const region = provider.options?.region
|
|
|
+ if (region) {
|
|
|
+ const regionPrefix = region.split("-")[0]
|
|
|
+ if (regionPrefix === "us" || regionPrefix === "eu") {
|
|
|
+ const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
|
|
|
+ if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
|
|
|
- if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
|
|
|
- } else {
|
|
|
- for (const model of Object.keys(provider.models)) {
|
|
|
- if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
|
|
|
- }
|
|
|
+ const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
|
|
|
+ if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
|
|
|
+ } else {
|
|
|
+ for (const model of Object.keys(provider.models)) {
|
|
|
+ if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return undefined
|
|
|
- })
|
|
|
-
|
|
|
- const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
|
|
|
- const cfg = yield* config.get()
|
|
|
- if (cfg.model) return parseModel(cfg.model)
|
|
|
-
|
|
|
- const s = yield* InstanceState.get(state)
|
|
|
- const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
|
|
|
- Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
|
|
|
- if (!isRecord(x) || !Array.isArray(x.recent)) return []
|
|
|
- return x.recent.flatMap((item) => {
|
|
|
- if (!isRecord(item)) return []
|
|
|
- if (typeof item.providerID !== "string") return []
|
|
|
- if (typeof item.modelID !== "string") return []
|
|
|
- return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
|
|
|
- })
|
|
|
- }),
|
|
|
- Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
|
|
|
- )
|
|
|
- for (const entry of recent) {
|
|
|
- const provider = s.providers[entry.providerID]
|
|
|
- if (!provider) continue
|
|
|
- if (!provider.models[entry.modelID]) continue
|
|
|
- return { providerID: entry.providerID, modelID: entry.modelID }
|
|
|
- }
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
|
|
|
- const provider = Object.values(s.providers).find(
|
|
|
- (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id),
|
|
|
- )
|
|
|
- if (!provider) throw new Error("no providers found")
|
|
|
- const [model] = sort(Object.values(provider.models))
|
|
|
- if (!model) throw new Error("no models found")
|
|
|
- return {
|
|
|
- providerID: provider.id,
|
|
|
- modelID: model.id,
|
|
|
- }
|
|
|
- })
|
|
|
+ const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
|
|
|
+ const cfg = yield* config.get()
|
|
|
+ if (cfg.model) return parseModel(cfg.model)
|
|
|
+
|
|
|
+ const s = yield* InstanceState.get(state)
|
|
|
+ const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
|
|
|
+ Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
|
|
|
+ if (!isRecord(x) || !Array.isArray(x.recent)) return []
|
|
|
+ return x.recent.flatMap((item) => {
|
|
|
+ if (!isRecord(item)) return []
|
|
|
+ if (typeof item.providerID !== "string") return []
|
|
|
+ if (typeof item.modelID !== "string") return []
|
|
|
+ return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
|
|
|
+ })
|
|
|
+ }),
|
|
|
+ Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
|
|
|
+ )
|
|
|
+ for (const entry of recent) {
|
|
|
+ const provider = s.providers[entry.providerID]
|
|
|
+ if (!provider) continue
|
|
|
+ if (!provider.models[entry.modelID]) continue
|
|
|
+ return { providerID: entry.providerID, modelID: entry.modelID }
|
|
|
+ }
|
|
|
|
|
|
- return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
|
|
|
- }),
|
|
|
- )
|
|
|
+ const provider = Object.values(s.providers).find(
|
|
|
+ (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id),
|
|
|
+ )
|
|
|
+ if (!provider) throw new Error("no providers found")
|
|
|
+ const [model] = sort(Object.values(provider.models))
|
|
|
+ if (!model) throw new Error("no models found")
|
|
|
+ return {
|
|
|
+ providerID: provider.id,
|
|
|
+ modelID: model.id,
|
|
|
+ }
|
|
|
+ })
|
|
|
|
|
|
- export const defaultLayer = Layer.suspend(() =>
|
|
|
- layer.pipe(
|
|
|
- Layer.provide(AppFileSystem.defaultLayer),
|
|
|
- Layer.provide(Env.defaultLayer),
|
|
|
- Layer.provide(Config.defaultLayer),
|
|
|
- Layer.provide(Auth.defaultLayer),
|
|
|
- Layer.provide(Plugin.defaultLayer),
|
|
|
- ),
|
|
|
+ return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
+export const defaultLayer = Layer.suspend(() =>
|
|
|
+ layer.pipe(
|
|
|
+ Layer.provide(AppFileSystem.defaultLayer),
|
|
|
+ Layer.provide(Env.defaultLayer),
|
|
|
+ Layer.provide(Config.defaultLayer),
|
|
|
+ Layer.provide(Auth.defaultLayer),
|
|
|
+ Layer.provide(Plugin.defaultLayer),
|
|
|
+ ),
|
|
|
+)
|
|
|
+
|
|
|
+const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
|
|
+export function sort<T extends { id: string }>(models: T[]) {
|
|
|
+ return sortBy(
|
|
|
+ models,
|
|
|
+ [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
|
|
+ [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
|
|
|
+ [(model) => model.id, "desc"],
|
|
|
)
|
|
|
+}
|
|
|
|
|
|
- const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
|
|
- export function sort<T extends { id: string }>(models: T[]) {
|
|
|
- return sortBy(
|
|
|
- models,
|
|
|
- [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
|
|
- [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
|
|
|
- [(model) => model.id, "desc"],
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- export function parseModel(model: string) {
|
|
|
- const [providerID, ...rest] = model.split("/")
|
|
|
- return {
|
|
|
- providerID: ProviderID.make(providerID),
|
|
|
- modelID: ModelID.make(rest.join("/")),
|
|
|
- }
|
|
|
+export function parseModel(model: string) {
|
|
|
+ const [providerID, ...rest] = model.split("/")
|
|
|
+ return {
|
|
|
+ providerID: ProviderID.make(providerID),
|
|
|
+ modelID: ModelID.make(rest.join("/")),
|
|
|
}
|
|
|
-
|
|
|
- export const ModelNotFoundError = NamedError.create(
|
|
|
- "ProviderModelNotFoundError",
|
|
|
- z.object({
|
|
|
- providerID: ProviderID.zod,
|
|
|
- modelID: ModelID.zod,
|
|
|
- suggestions: z.array(z.string()).optional(),
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- export const InitError = NamedError.create(
|
|
|
- "ProviderInitError",
|
|
|
- z.object({
|
|
|
- providerID: ProviderID.zod,
|
|
|
- }),
|
|
|
- )
|
|
|
}
|
|
|
+
|
|
|
+export const ModelNotFoundError = NamedError.create(
|
|
|
+ "ProviderModelNotFoundError",
|
|
|
+ z.object({
|
|
|
+ providerID: ProviderID.zod,
|
|
|
+ modelID: ModelID.zod,
|
|
|
+ suggestions: z.array(z.string()).optional(),
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
+export const InitError = NamedError.create(
|
|
|
+ "ProviderInitError",
|
|
|
+ z.object({
|
|
|
+ providerID: ProviderID.zod,
|
|
|
+ }),
|
|
|
+)
|