diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 6f6e0528da..8ea69ccc01 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -53,6 +53,9 @@ export const Plugin = define({ catalog.provider.update(providerID, (provider) => { if (item.name !== undefined) provider.name = item.name if (item.api !== undefined) provider.api = { ...item.api } + if (item.package !== undefined || item.settings !== undefined) { + provider.api = configuredApi(provider.api, item.package, item.settings) + } if (item.request !== undefined) { Object.assign(provider.request.headers, item.request.headers) Object.assign(provider.request.body, item.request.body) @@ -63,6 +66,16 @@ export const Plugin = define({ if (config.family !== undefined) model.family = config.family if (config.name !== undefined) model.name = config.name if (config.api !== undefined) model.api = { ...model.api, ...config.api } + if (config.package !== undefined || config.settings !== undefined) { + model.api = { + ...configuredApi( + config.api === undefined ? catalog.provider.get(providerID)!.provider.api : model.api, + config.package, + config.settings, + ), + id: model.api.id, + } + } if (config.capabilities !== undefined) { model.capabilities = { tools: config.capabilities.tools, @@ -111,3 +124,25 @@ export const Plugin = define({ ) }), }) + +function configuredApi(api: ProviderV2.MutableApi, packageName?: string, settings?: Record) { + const merged = { ...api.settings, ...settings } + const baseURL = typeof merged.baseURL === "string" ? merged.baseURL : api.url + if (packageName?.startsWith("aisdk:")) { + return { + type: "aisdk" as const, + package: packageName.slice("aisdk:".length), + ...(baseURL === undefined ? {} : { url: baseURL }), + settings: merged, + } + } + if (packageName !== undefined) { + return { + type: "native" as const, + package: packageName, + ...(baseURL === undefined ? {} : { url: baseURL }), + settings: merged, + } + } + return { ...api, ...(baseURL === undefined ? {} : { url: baseURL }), settings: merged } +} diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index 1b54757078..130bbfaba7 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -47,6 +47,8 @@ const ModelApi = Schema.Union([ class Model extends Schema.Class("ConfigV2.Model")({ family: ModelV2.Family.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional), + package: Schema.String.pipe(Schema.optional), + settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), api: ModelApi.pipe(Schema.optional), capabilities: ModelV2.Capabilities.pipe(Schema.optional), request: Schema.Struct({ @@ -65,6 +67,8 @@ class Model extends Schema.Class("ConfigV2.Model")({ export class Info extends Schema.Class("ConfigV2.Provider")({ name: Schema.String.pipe(Schema.optional), env: Schema.String.pipe(Schema.Array, Schema.optional), + package: Schema.String.pipe(Schema.optional), + settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), api: ProviderV2.Api.pipe(Schema.optional), request: Request.pipe(Schema.optional), models: Schema.Record(Schema.String, Model).pipe(Schema.optional), diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 605bcd63cf..ebba0296a6 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -55,6 +55,76 @@ function request(headers: Record, variant?: string) { const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigProviderPlugin.Plugin", () => { + it.effect("loads documented native provider packages", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("custom") + const modelID = ModelV2.ID.make("chat") + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "@opencode-ai/llm/providers/openai-compatible", + settings: { baseURL: "https://example.test/v1" }, + models: { chat: { name: "Chat" } }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({ + id: modelID, + type: "native", + package: "@opencode-ai/llm/providers/openai-compatible", + url: "https://example.test/v1", + settings: { baseURL: "https://example.test/v1" }, + }) + }), + ) + + it.effect("loads documented aisdk provider packages", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("custom") + const modelID = ModelV2.ID.make("chat") + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + settings: { baseURL: "https://example.test/v1" }, + models: { chat: { name: "Chat" } }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({ + id: modelID, + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://example.test/v1", + settings: { baseURL: "https://example.test/v1" }, + }) + }), + ) + it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service