diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 1f355b5036..2c691cedb5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -474,15 +474,7 @@ export namespace Config { .extend({ whitelist: z.array(z.string()).optional(), blacklist: z.array(z.string()).optional(), - models: z - .record( - z.string(), - ModelsDev.Model.partial().refine( - (input) => input.id === undefined, - "The model.id field can no longer be specified. Use model.target to specify an alternate model id to use when calling the provider.", - ), - ) - .optional(), + models: z.record(z.string(), ModelsDev.Model.partial()).optional(), options: z .object({ apiKey: z.string().optional(), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 4dff85616c..871fc94d2a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -481,8 +481,14 @@ export namespace Provider { function mergeProvider(providerID: string, provider: Partial) { const match = database[providerID] if (!match) return - // @ts-expect-error - providers[providerID] = mergeDeep(match, provider) + const existing = providers[providerID] + if (existing) { + // @ts-expect-error + providers[providerID] = mergeDeep(existing, provider) + } else { + // @ts-expect-error + providers[providerID] = mergeDeep(match, provider) + } } // extend database from config @@ -494,7 +500,7 @@ export namespace Provider { env: provider.env ?? existing?.env ?? [], options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), source: "config", - models: {}, + models: existing?.models ?? {}, } for (const [modelID, model] of Object.entries(provider.models ?? {})) { @@ -520,18 +526,18 @@ export namespace Provider { attachment: model.attachment ?? existing?.capabilities.attachment ?? false, toolcall: model.tool_call ?? existing?.capabilities.toolcall ?? true, 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, + text: model.modalities?.input?.includes("text") ?? existing?.capabilities.input.text ?? true, + audio: model.modalities?.input?.includes("audio") ?? existing?.capabilities.input.audio ?? false, + image: model.modalities?.input?.includes("image") ?? existing?.capabilities.input.image ?? false, + video: model.modalities?.input?.includes("video") ?? existing?.capabilities.input.video ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? existing?.capabilities.input.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, + text: model.modalities?.output?.includes("text") ?? existing?.capabilities.output.text ?? true, + audio: model.modalities?.output?.includes("audio") ?? existing?.capabilities.output.audio ?? false, + image: model.modalities?.output?.includes("image") ?? existing?.capabilities.output.image ?? false, + video: model.modalities?.output?.includes("video") ?? existing?.capabilities.output.video ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? existing?.capabilities.output.pdf ?? false, }, }, cost: { @@ -638,12 +644,11 @@ export namespace Provider { // load config for (const [providerID, provider] of configProviders) { - mergeProvider(providerID, { - source: "config", - env: provider.env, - name: provider.name, - options: provider.options, - }) + const partial: Partial = { 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) } for (const [providerID, provider] of Object.entries(providers)) { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 103ad7f257..698fdddfb4 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -634,7 +634,7 @@ test("getModel uses realIdByKey for aliased models", async () => { }) }) -test("provider api field sets default baseURL", async () => { +test("provider api field sets model api.url", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( @@ -667,7 +667,8 @@ test("provider api field sets default baseURL", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["custom-api"].options.baseURL).toBe("https://api.example.com/v1") + // api field is stored on model.api.url, used by getSDK to set baseURL + expect(providers["custom-api"].models["model-1"].api.url).toBe("https://api.example.com/v1") }, }) }) @@ -1122,8 +1123,8 @@ test("provider with multiple env var options only includes apiKey when single en fn: async () => { const providers = await Provider.list() expect(providers["multi-env"]).toBeDefined() - // When multiple env options exist, apiKey should NOT be auto-set - expect(providers["multi-env"].options.apiKey).toBeUndefined() + // When multiple env options exist, key should NOT be auto-set + expect(providers["multi-env"].key).toBeUndefined() }, }) }) @@ -1164,8 +1165,8 @@ test("provider with single env var includes apiKey automatically", async () => { fn: async () => { const providers = await Provider.list() expect(providers["single-env"]).toBeDefined() - // Single env option should auto-set apiKey - expect(providers["single-env"].options.apiKey).toBe("my-api-key") + // Single env option should auto-set key + expect(providers["single-env"].key).toBe("my-api-key") }, }) })