diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 5d2dec625c6..f109d42028e 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -9,6 +9,11 @@ "opencode": { "options": {}, }, + "azure": { + "options": { + "resourceName": "alice-mi7mfgew-eastus2", + }, + }, }, "mcp": { "context7": { diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index c5465f9880e..f60089d940e 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -60,7 +60,12 @@ export namespace ModelsDev { status: z.enum(["alpha", "beta", "deprecated"]).optional(), options: z.record(z.string(), z.any()), headers: z.record(z.string(), z.string()).optional(), - provider: z.object({ npm: z.string() }).optional(), + provider: z + .object({ + npm: z.string().optional(), + api: z.string().optional(), + }) + .optional(), variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) export type Model = z.infer diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index bcb115edf41..98de0babf5f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -66,7 +66,7 @@ export namespace Provider { "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } - type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise + type CustomModelLoader = (sdk: any, model: Model, options?: Record) => Promise type CustomLoader = (provider: Info) => Promise<{ autoload: boolean getModel?: CustomModelLoader @@ -110,8 +110,8 @@ export namespace Provider { openai: async () => { return { autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - return sdk.responses(modelID) + async getModel(sdk: any, model: Model, _options?: Record) { + return sdk.responses(model.api.id) }, options: {}, } @@ -119,11 +119,11 @@ export namespace Provider { "github-copilot": async () => { return { autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { - return sdk.responses(modelID) + async getModel(sdk: any, model: Model, _options?: Record) { + if (model.api.id.includes("codex")) { + return sdk.responses(model.api.id) } - return sdk.chat(modelID) + return sdk.chat(model.api.id) }, options: {}, } @@ -131,42 +131,44 @@ export namespace Provider { "github-copilot-enterprise": async () => { return { autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { - return sdk.responses(modelID) + async getModel(sdk: any, model: Model, _options?: Record) { + if (model.api.id.includes("codex")) { + return sdk.responses(model.api.id) } - return sdk.chat(modelID) + return sdk.chat(model.api.id) }, options: {}, } }, + // TODO: handle the openai and anthropic deployments azure: async () => { return { autoload: false, - async getModel(sdk: any, modelID: string, options?: Record) { + async getModel(sdk: any, model: Model, options?: Record) { + if (model && model.api.npm !== "@ai-sdk/azure") { + return sdk.languageModel(model.api.id) + } if (options?.["useCompletionUrls"]) { - return sdk.chat(modelID) - } else { - return sdk.responses(modelID) + return sdk.chat(model.api.id) } + return sdk.responses(model.api.id) }, options: {}, } }, "azure-cognitive-services": async () => { - const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") return { autoload: false, - async getModel(sdk: any, modelID: string, options?: Record) { + async getModel(sdk: any, model: Model, options?: Record) { + if (model && model.api.npm !== "@ai-sdk/azure") { + return sdk.languageModel(model.api.id) + } if (options?.["useCompletionUrls"]) { - return sdk.chat(modelID) - } else { - return sdk.responses(modelID) + return sdk.chat(model.api.id) } + return sdk.responses(model.api.id) }, - options: { - baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, - }, + options: {}, } }, "amazon-bedrock": async () => { @@ -225,7 +227,8 @@ export namespace Provider { return { autoload: true, options: providerOptions, - async getModel(sdk: any, modelID: string, options?: Record) { + async getModel(sdk: any, model: Model, options?: Record) { + let modelID = model.api.id // Skip region prefixing if model already has a cross-region inference profile prefix if (modelID.startsWith("global.") || modelID.startsWith("jp.")) { return sdk.languageModel(modelID) @@ -343,8 +346,8 @@ export namespace Provider { project, location, }, - async getModel(sdk: any, modelID: string) { - const id = String(modelID).trim() + async getModel(sdk: any, model: Model) { + const id = String(model.api.id).trim() return sdk.languageModel(id) }, } @@ -360,8 +363,8 @@ export namespace Provider { project, location, }, - async getModel(sdk: any, modelID) { - const id = String(modelID).trim() + async getModel(sdk: any, model: Model) { + const id = String(model.api.id).trim() return sdk.languageModel(id) }, } @@ -383,8 +386,8 @@ export namespace Provider { return { autoload: !!envServiceKey, options: envServiceKey ? { deploymentId, resourceGroup } : {}, - async getModel(sdk: any, modelID: string) { - return sdk(modelID) + async getModel(sdk: any, model: Model) { + return sdk(model.api.id) }, } }, @@ -423,8 +426,8 @@ export namespace Provider { ...(providerConfig?.options?.featureFlags || {}), }, }, - async getModel(sdk: ReturnType, modelID: string) { - return sdk.agenticChat(modelID, { + async getModel(sdk: ReturnType, model: Model) { + return sdk.agenticChat(model.api.id, { featureFlags: { duo_agent_platform_agentic_chat: true, duo_agent_platform: true, @@ -451,8 +454,8 @@ export namespace Provider { return { autoload: true, - async getModel(sdk: any, modelID: string, _options?: Record) { - return sdk.languageModel(modelID) + async getModel(sdk: any, model: Model, _options?: Record) { + return sdk.languageModel(model.api.id) }, options: { baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gateway}/compat`, @@ -594,7 +597,7 @@ export namespace Provider { family: model.family, api: { id: model.id, - url: provider.api!, + url: model.provider?.api ?? provider.api!, npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, status: model.status ?? "active", @@ -957,6 +960,21 @@ export namespace Provider { return state().then((state) => state.providers) } + function resolveModelBaseURL(model: Model, options: Record): string { + const template = model.api?.url ?? "" + if (!template) return "" + const matches = [...template.matchAll(/{{([^}]+)}}/g)] + if (matches.length === 0) return template + return matches.reduce((url, match) => { + const keys = match[1].split("|").map((item) => item.trim()) + const resolved = keys + .map((key) => Env.get(key) ?? options[key]) + .find((value) => value !== undefined && value !== null && value !== "") + if (resolved === undefined || resolved === null || resolved === "") return url + return url.replaceAll(match[0], String(resolved)) + }, template) + } + async function getSDK(model: Model) { try { using _ = log.time("getSDK", { @@ -970,7 +988,8 @@ export namespace Provider { options["includeUsage"] = true } - if (!options["baseURL"]) options["baseURL"] = model.api.url + const resolvedBaseURL = resolveModelBaseURL(model, options) + if (!options["baseURL"] && resolvedBaseURL) options["baseURL"] = resolvedBaseURL if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key if (model.headers) options["headers"] = { @@ -1093,9 +1112,8 @@ export namespace Provider { const sdk = await getSDK(model) try { - const language = s.modelLoaders[model.providerID] - ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options) - : sdk.languageModel(model.api.id) + const loader = s.modelLoaders[model.providerID] + const language = loader ? await loader(sdk, model, provider.options) : sdk.languageModel(model.api.id) s.models.set(key, language) return language } catch (e) {