From 1520b0de204d9443e032feb76905ad0d4e1383d4 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 3 Jun 2026 13:32:58 -0400 Subject: [PATCH] refactor(core): remove ai sdk option fields (#30581) --- packages/core/src/agent.ts | 8 +- packages/core/src/aisdk.ts | 20 +- packages/core/src/catalog.ts | 49 ++-- packages/core/src/config/agent.ts | 2 +- packages/core/src/config/plugin/agent.ts | 8 +- packages/core/src/config/plugin/provider.ts | 28 +-- packages/core/src/config/provider.ts | 18 +- packages/core/src/model.ts | 23 +- packages/core/src/plugin/account.ts | 6 +- packages/core/src/plugin/models-dev.ts | 16 +- .../src/plugin/provider/amazon-bedrock.ts | 12 +- .../core/src/plugin/provider/anthropic.ts | 6 +- packages/core/src/plugin/provider/azure.ts | 16 +- packages/core/src/plugin/provider/cerebras.ts | 6 +- .../plugin/provider/cloudflare-workers-ai.ts | 14 +- packages/core/src/plugin/provider/gitlab.ts | 8 +- .../core/src/plugin/provider/google-vertex.ts | 34 +-- packages/core/src/plugin/provider/kilo.ts | 10 +- .../core/src/plugin/provider/llmgateway.ts | 12 +- packages/core/src/plugin/provider/nvidia.ts | 12 +- packages/core/src/plugin/provider/openai.ts | 4 +- packages/core/src/plugin/provider/opencode.ts | 4 +- .../core/src/plugin/provider/openrouter.ts | 8 +- packages/core/src/plugin/provider/vercel.ts | 8 +- packages/core/src/plugin/provider/zenmux.ts | 10 +- packages/core/src/provider.ts | 69 ++---- packages/core/src/v1/config/migrate.ts | 51 +++-- .../core/src/v1/config/provider-options.ts | 210 ++++++++++++++++++ packages/core/test/account.test.ts | 5 +- packages/core/test/catalog.test.ts | 59 +++-- packages/core/test/config/agent.test.ts | 15 +- packages/core/test/config/config.test.ts | 119 +++++++--- packages/core/test/config/provider.test.ts | 22 +- .../plugin/provider-amazon-bedrock.test.ts | 17 +- .../test/plugin/provider-anthropic.test.ts | 14 +- .../provider-azure-cognitive-services.test.ts | 22 +- .../core/test/plugin/provider-azure.test.ts | 42 ++-- .../test/plugin/provider-cerebras.test.ts | 8 +- .../provider-cloudflare-workers-ai.test.ts | 28 +-- .../test/plugin/provider-deepinfra.test.ts | 2 +- .../core/test/plugin/provider-dynamic.test.ts | 8 +- .../core/test/plugin/provider-gitlab.test.ts | 16 +- .../provider-google-vertex-anthropic.test.ts | 16 +- .../plugin/provider-google-vertex.test.ts | 48 ++-- .../core/test/plugin/provider-google.test.ts | 10 +- .../core/test/plugin/provider-groq.test.ts | 10 +- packages/core/test/plugin/provider-helper.ts | 34 +-- .../core/test/plugin/provider-kilo.test.ts | 36 +-- .../test/plugin/provider-llmgateway.test.ts | 18 +- .../core/test/plugin/provider-nvidia.test.ts | 35 ++- .../core/test/plugin/provider-openai.test.ts | 4 +- .../test/plugin/provider-opencode.test.ts | 26 +-- .../test/plugin/provider-openrouter.test.ts | 16 +- .../core/test/plugin/provider-vercel.test.ts | 20 +- .../core/test/plugin/provider-xai.test.ts | 2 +- .../core/test/plugin/provider-zenmux.test.ts | 36 ++- packages/sdk/js/src/v2/gen/types.gen.ts | 151 +++---------- 57 files changed, 825 insertions(+), 686 deletions(-) create mode 100644 packages/core/src/v1/config/provider-options.ts diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index d7c249031e..cbe561e616 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -19,7 +19,7 @@ export const Color = Schema.Union([ export class Info extends Schema.Class("AgentV2.Info")({ id: ID, model: ModelV2.Ref.pipe(Schema.optional), - options: ProviderV2.Options, + request: ProviderV2.Request, system: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), mode: Schema.Literals(["subagent", "primary", "all"]), @@ -31,13 +31,9 @@ export class Info extends Schema.Class("AgentV2.Info")({ static empty(id: ID) { return new Info({ id, - options: { + request: { headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, }, mode: "all", hidden: false, diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index f3d39f3ac0..8640e3cf12 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -58,8 +58,12 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { } function prepareOptions(model: ModelV2.Info, pkg: string) { - const options: Record = { name: model.providerID, ...model.options.aisdk.provider } - if (model.endpoint.type === "aisdk" && model.endpoint.url) options.baseURL = model.endpoint.url + const options: Record = { + name: model.providerID, + ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}), + ...model.request.body, + } + if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url const customFetch = options.fetch const chunkTimeout = options.chunkTimeout @@ -123,25 +127,25 @@ export const layer = Layer.effect( return Service.of({ language: Effect.fn("AISDK.language")(function* (model) { - const key = `${model.providerID}/${model.id}/${model.options.variant ?? "default"}` + const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` const existing = languages.get(key) if (existing) return existing - if (model.endpoint.type !== "aisdk") + if (model.api.type !== "aisdk") return yield* new InitError({ providerID: model.providerID, - cause: new Error(`Unsupported endpoint ${model.endpoint.type}`), + cause: new Error(`Unsupported api ${model.api.type}`), }) - const options = prepareOptions(model, model.endpoint.package) + const options = prepareOptions(model, model.api.package) const sdkKey = JSON.stringify({ providerID: model.providerID, - endpoint: model.endpoint, + api: model.api, options, }) const sdk = sdks.get(sdkKey) ?? (yield* plugin - .trigger("aisdk.sdk", { model, package: model.endpoint.package, options }, {}) + .trigger("aisdk.sdk", { model, package: model.api.package, options }, {}) .pipe(initError(model.providerID))).sdk if (!sdk) return yield* new InitError({ diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 5b53b92bd8..d3c131e65e 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -97,34 +97,29 @@ export const layer = Layer.effect( const resolve = (model: ModelV2.Info) => { const provider = state.get().providers.get(model.providerID)!.provider - const endpoint = - model.endpoint.type === "unknown" - ? provider.endpoint - : model.endpoint.type === "aisdk" && provider.endpoint.type === "aisdk" && !model.endpoint.url - ? { ...model.endpoint, url: provider.endpoint.url } - : model.endpoint - const options = { + const api = + model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0 + ? provider.api + : model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url + ? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } } + : model.api.type === "aisdk" && provider.api.type === "aisdk" + ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } } + : model.api + const request = { headers: { - ...provider.options.headers, - ...model.options.headers, + ...provider.request.headers, + ...model.request.headers, }, body: { - ...provider.options.body, - ...model.options.body, + ...provider.request.body, + ...model.request.body, }, - aisdk: { - provider: { - ...provider.options.aisdk.provider, - ...model.options.aisdk.provider, - }, - request: model.options.aisdk.request, - }, - variant: model.options.variant, + variant: model.request.variant, } return new ModelV2.Info({ ...model, - endpoint, - options, + api, + request, }) } @@ -134,10 +129,10 @@ export const layer = Layer.effect( return match } - const normalizeEndpoint = (item: Draft | Draft) => { - if (item.endpoint.type !== "aisdk" || typeof item.options.aisdk.provider.baseURL !== "string") return - item.endpoint.url = item.options.aisdk.provider.baseURL - delete item.options.aisdk.provider.baseURL + const normalizeApi = (item: Draft | Draft) => { + if (typeof item.request.body.baseURL !== "string") return + item.api.url = item.request.body.baseURL + delete item.request.body.baseURL } const state = State.create({ @@ -157,7 +152,7 @@ export const layer = Layer.effect( draft.providers.set(providerID, current) } fn(current.provider) - normalizeEndpoint(current.provider) + normalizeApi(current.provider) }, remove: (providerID) => { draft.providers.delete(providerID) @@ -179,7 +174,7 @@ export const layer = Layer.effect( fn(model) model.id = modelID model.providerID = providerID - normalizeEndpoint(model) + normalizeApi(model) }, remove: (providerID, modelID) => { draft.providers.get(providerID)?.models.delete(modelID) diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 40d2bc94b5..80d4b08bfd 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -13,7 +13,7 @@ export const Color = Schema.Union([ export class Info extends Schema.Class("ConfigV2.Agent")({ model: Schema.String.pipe(Schema.optional), variant: Schema.String.pipe(Schema.optional), - options: ConfigProvider.Options.pipe(Schema.optional), + request: ConfigProvider.Request.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional), diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index c05b0a578f..ffe4209f38 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -34,11 +34,9 @@ export const Plugin = PluginV2.define({ if (item.variant !== undefined && agent.model !== undefined) { agent.model.variant = ModelV2.VariantID.make(item.variant) } - if (item.options !== undefined) { - Object.assign(agent.options.headers, item.options.headers ?? {}) - Object.assign(agent.options.body, item.options.body ?? {}) - Object.assign(agent.options.aisdk.provider, item.options.aisdk?.provider ?? {}) - Object.assign(agent.options.aisdk.request, item.options.aisdk?.request ?? {}) + if (item.request !== undefined) { + Object.assign(agent.request.headers, item.request.headers ?? {}) + Object.assign(agent.request.body, item.request.body ?? {}) } if (item.system !== undefined) agent.system = item.system if (item.description !== undefined) agent.description = item.description diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index fca2e53302..42eb176860 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -23,12 +23,10 @@ export const Plugin = PluginV2.define({ if (item.name !== undefined) provider.name = item.name if (item.env !== undefined) provider.env = [...item.env] provider.enabled = { via: "custom", data: {} } - if (item.endpoint !== undefined) provider.endpoint = { ...item.endpoint } - if (item.options !== undefined) { - Object.assign(provider.options.headers, item.options.headers ?? {}) - Object.assign(provider.options.body, item.options.body ?? {}) - Object.assign(provider.options.aisdk.provider, item.options.aisdk?.provider ?? {}) - Object.assign(provider.options.aisdk.request, item.options.aisdk?.request ?? {}) + if (item.api !== undefined) provider.api = { ...item.api } + if (item.request !== undefined) { + Object.assign(provider.request.headers, item.request.headers ?? {}) + Object.assign(provider.request.body, item.request.body ?? {}) } }) @@ -37,7 +35,7 @@ export const Plugin = PluginV2.define({ if (config.api_id !== undefined) model.apiID = config.api_id if (config.family !== undefined) model.family = config.family if (config.name !== undefined) model.name = config.name - if (config.endpoint !== undefined) model.endpoint = { ...config.endpoint } + if (config.api !== undefined) model.api = { ...config.api } if (config.capabilities !== undefined) { model.capabilities = { tools: config.capabilities.tools, @@ -45,12 +43,10 @@ export const Plugin = PluginV2.define({ output: [...config.capabilities.output], } } - if (config.options !== undefined) { - Object.assign(model.options.headers, config.options.headers ?? {}) - Object.assign(model.options.body, config.options.body ?? {}) - Object.assign(model.options.aisdk.provider, config.options.aisdk?.provider ?? {}) - Object.assign(model.options.aisdk.request, config.options.aisdk?.request ?? {}) - if (config.options.variant !== undefined) model.options.variant = config.options.variant + if (config.request !== undefined) { + Object.assign(model.request.headers, config.request.headers ?? {}) + Object.assign(model.request.body, config.request.body ?? {}) + if (config.request.variant !== undefined) model.request.variant = config.request.variant } if (config.variants !== undefined) { for (const variant of config.variants) { @@ -60,17 +56,11 @@ export const Plugin = PluginV2.define({ id: variant.id, headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, } model.variants.push(existing) } Object.assign(existing.headers, variant.headers ?? {}) Object.assign(existing.body, variant.body ?? {}) - Object.assign(existing.aisdk.provider, variant.aisdk?.provider ?? {}) - Object.assign(existing.aisdk.request, variant.aisdk?.request ?? {}) } } if (config.cost !== undefined) { diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index fbb0e1c3ef..94e7546ddb 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -4,13 +4,9 @@ import { Schema } from "effect" import { ProviderV2 } from "../provider" import { ModelV2 } from "../model" -export class Options extends Schema.Class("ConfigV2.Provider.Options")({ +export class Request extends Schema.Class("ConfigV2.Provider.Request")({ headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - aisdk: Schema.Struct({ - provider: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - request: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }).pipe(Schema.optional), }) {} class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ @@ -38,15 +34,15 @@ class Model extends Schema.Class("ConfigV2.Model")({ api_id: ModelV2.ID.pipe(Schema.optional), family: ModelV2.Family.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional), - endpoint: ProviderV2.Endpoint.pipe(Schema.optional), + api: ProviderV2.Api.pipe(Schema.optional), capabilities: ModelV2.Capabilities.pipe(Schema.optional), - options: Schema.Struct({ - ...Options.fields, + request: Schema.Struct({ + ...Request.fields, variant: Schema.String.pipe(Schema.optional), }).pipe(Schema.optional), variants: Schema.Struct({ id: ModelV2.VariantID, - ...Options.fields, + ...Request.fields, }).pipe(Schema.Array, Schema.optional), cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), @@ -56,7 +52,7 @@ 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), - endpoint: ProviderV2.Endpoint.pipe(Schema.optional), - options: Options.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/src/model.ts b/packages/core/src/model.ts index b0de0802a3..ed1741341f 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -46,15 +46,15 @@ export class Info extends Schema.Class("ModelV2.Info")({ providerID: ProviderV2.ID, family: Family.pipe(Schema.optional), name: Schema.String, - endpoint: ProviderV2.Endpoint, + api: ProviderV2.Api, capabilities: Capabilities, - options: Schema.Struct({ - ...ProviderV2.Options.fields, + request: Schema.Struct({ + ...ProviderV2.Request.fields, variant: Schema.String.pipe(Schema.optional), }), variants: Schema.Struct({ id: VariantID, - ...ProviderV2.Options.fields, + ...ProviderV2.Request.fields, }).pipe(Schema.Array), time: Schema.Struct({ released: DateTimeUtcFromMillis, @@ -69,26 +69,23 @@ export class Info extends Schema.Class("ModelV2.Info")({ }), }) { static empty(providerID: ProviderV2.ID, modelID: ID): Info { - return { + return new Info({ id: modelID, apiID: modelID, providerID, name: modelID, - endpoint: { - type: "unknown", + api: { + type: "native", + settings: {}, }, capabilities: { tools: false, input: [], output: [], }, - options: { + request: { headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, }, variants: [], time: { @@ -101,7 +98,7 @@ export class Info extends Schema.Class("ModelV2.Info")({ context: 0, output: 0, }, - } + }) } } diff --git a/packages/core/src/plugin/account.ts b/packages/core/src/plugin/account.ts index 68bb43674d..98c9ef7a3f 100644 --- a/packages/core/src/plugin/account.ts +++ b/packages/core/src/plugin/account.ts @@ -32,10 +32,10 @@ export const AccountPlugin = PluginV2.define({ service: account.serviceID, } if (account.credential.type === "api") { - provider.options.aisdk.provider.apiKey = account.credential.key - Object.assign(provider.options.aisdk.provider, account.credential.metadata ?? {}) + provider.request.body.apiKey = account.credential.key + Object.assign(provider.request.body, account.credential.metadata ?? {}) } - if (account.credential.type === "oauth") provider.options.aisdk.provider.apiKey = account.credential.access + if (account.credential.type === "oauth") provider.request.body.apiKey = account.credential.access }) } }), diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 7ee38ac6de..9523a2edea 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -43,10 +43,6 @@ function variants(model: ModelsDev.Model) { id: ModelV2.VariantID.make(id), headers: { ...(item.provider?.headers ?? {}) }, body: { ...(item.provider?.body ?? {}) }, - aisdk: { - provider: {}, - request: {}, - }, })) } @@ -66,14 +62,16 @@ export const ModelsDevPlugin = PluginV2.define({ catalog.provider.update(providerID, (provider) => { provider.name = item.name provider.env = [...item.env] - provider.endpoint = item.npm + provider.api = item.npm ? { type: "aisdk", package: item.npm, url: item.api, } : { - type: "unknown", + type: "native", + url: item.api, + settings: {}, } }) @@ -82,14 +80,16 @@ export const ModelsDevPlugin = PluginV2.define({ catalog.model.update(providerID, modelID, (draft) => { draft.name = model.name draft.family = model.family ? ModelV2.Family.make(model.family) : undefined - draft.endpoint = model.provider?.npm + draft.api = model.provider?.npm ? { type: "aisdk", package: model.provider?.npm, url: model.provider.api, } : { - type: "unknown", + type: "native", + url: model.provider?.api, + settings: {}, } draft.capabilities = { tools: model.tool_call, diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index e7452ac2e9..31df9ca120 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -52,15 +52,15 @@ export const AmazonBedrockPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/amazon-bedrock") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue evt.provider.update(item.provider.id, (provider) => { - if (provider.endpoint.type !== "aisdk") return - if (typeof provider.options.aisdk.provider.endpoint !== "string") return + if (provider.api.type !== "aisdk") return + if (typeof provider.request.body.endpoint !== "string") return // The AI SDK expects a base URL, but users configure Bedrock private/VPC // endpoints as `endpoint`; move it into the catalog endpoint URL once. - provider.endpoint.url = provider.options.aisdk.provider.endpoint - delete provider.options.aisdk.provider.endpoint + provider.api.url = provider.request.body.endpoint + delete provider.request.body.endpoint }) } }), diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts index 026da36349..9bd69fe036 100644 --- a/packages/core/src/plugin/provider/anthropic.ts +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -7,10 +7,10 @@ export const AnthropicPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/anthropic") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/anthropic") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["anthropic-beta"] = + provider.request.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" }) } diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index bea98cf211..cc8cf3c65b 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -16,14 +16,14 @@ export const AzurePlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/azure") continue - const configured = item.provider.options.aisdk.provider.resourceName + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/azure") continue + const configured = item.provider.request.body.resourceName const resourceName = typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME if (!resourceName) continue evt.provider.update(item.provider.id, (provider) => { - provider.options.aisdk.provider.resourceName = resourceName + provider.request.body.resourceName = resourceName }) } }), @@ -33,7 +33,7 @@ export const AzurePlugin = PluginV2.define({ if ( !evt.options.resourceName && !evt.options.baseURL && - (evt.model.endpoint.type !== "aisdk" || !evt.model.endpoint.url) + (evt.model.api.type !== "aisdk" || !evt.model.api.url) ) { throw new Error( "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", @@ -59,11 +59,11 @@ export const AzureCognitiveServicesPlugin = PluginV2.define({ const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME if (!resourceName) return for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (!item.provider.id.includes("azure-cognitive-services")) continue evt.provider.update(item.provider.id, (provider) => { - provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` + provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` }) } }), diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index b18884cb6e..f871943687 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -7,10 +7,10 @@ export const CerebrasPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (ctx) { for (const item of ctx.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/cerebras") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/cerebras") continue ctx.provider.update(item.provider.id, (provider) => { - provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" + provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" }) } }), diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 32cfb059f4..1ad3834c97 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -14,17 +14,17 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({ const item = evt.provider.get(providerID) if (!item) return evt.provider.update(item.provider.id, (provider) => { - if (provider.endpoint.type !== "aisdk") return - if (provider.endpoint.url) return - const accountId = resolveAccountId(provider.options.aisdk.provider) - if (accountId) provider.endpoint.url = workersEndpoint(accountId) + if (provider.api.type !== "aisdk") return + if (provider.api.url) return + const accountId = resolveAccountId(provider.request.body) + if (accountId) provider.api.url = workersEndpoint(accountId) }) }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.model.providerID !== providerID) return if (evt.package !== "@ai-sdk/openai-compatible") return - if (!hasWorkersEndpoint(evt.model.endpoint)) return + if (!hasWorkersEndpoint(evt.model.api)) return const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) evt.sdk = mod.createOpenAICompatible(sdkOptions(evt.options) as any) }), @@ -44,8 +44,8 @@ function workersEndpoint(accountId: string) { return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1` } -function hasWorkersEndpoint(endpoint: ProviderV2.Endpoint) { - return endpoint.type === "aisdk" && Boolean(endpoint.url) +function hasWorkersEndpoint(api: ProviderV2.Api) { + return api.type === "aisdk" && Boolean(api.url) } function sdkOptions(options: Record) { diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 226f5a45eb..0d7c987cbc 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -37,12 +37,12 @@ export const GitLabPlugin = PluginV2.define({ if (evt.model.apiID.startsWith("duo-workflow-")) { const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie) const workflowRef = - typeof evt.model.options.aisdk.request.workflowRef === "string" - ? evt.model.options.aisdk.request.workflowRef + typeof evt.model.request.body.workflowRef === "string" + ? evt.model.request.body.workflowRef : undefined const workflowDefinition = - typeof evt.model.options.aisdk.request.workflowDefinition === "string" - ? evt.model.options.aisdk.request.workflowDefinition + typeof evt.model.request.body.workflowDefinition === "string" + ? evt.model.request.body.workflowDefinition : undefined const language = evt.sdk.workflowChat( gitlab.isWorkflowModel(evt.model.apiID) ? evt.model.apiID : "duo-workflow", diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index ae1692b933..abf6c0e0c6 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -60,22 +60,22 @@ export const GoogleVertexPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.api.type !== "aisdk") continue if ( - item.provider.endpoint.package !== "@ai-sdk/google-vertex" && - !item.provider.endpoint.package.includes("@ai-sdk/openai-compatible") + item.provider.api.package !== "@ai-sdk/google-vertex" && + !item.provider.api.package.includes("@ai-sdk/openai-compatible") ) continue - const project = resolveProject(item.provider.options.aisdk.provider) - const location = String(resolveLocation(item.provider.options.aisdk.provider)) + const project = resolveProject(item.provider.request.body) + const location = String(resolveLocation(item.provider.request.body)) evt.provider.update(item.provider.id, (provider) => { - if (project) provider.options.aisdk.provider.project = project - provider.options.aisdk.provider.location = location - if (provider.endpoint.type === "aisdk" && provider.endpoint.url) { - provider.endpoint.url = replaceVertexVars(provider.endpoint.url, project, location) + if (project) provider.request.body.project = project + provider.request.body.location = location + if (provider.api.type === "aisdk" && provider.api.url) { + provider.api.url = replaceVertexVars(provider.api.url, project, location) } - if (provider.endpoint.type === "aisdk" && provider.endpoint.package.includes("@ai-sdk/openai-compatible")) { - provider.options.aisdk.provider.fetch = authFetch(provider.options.aisdk.provider.fetch) + if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) { + provider.request.body.fetch = authFetch(provider.request.body.fetch) } }) } @@ -111,21 +111,21 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/google-vertex/anthropic") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue const project = - item.provider.options.aisdk.provider.project ?? + item.provider.request.body.project ?? process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT const location = - item.provider.options.aisdk.provider.location ?? + item.provider.request.body.location ?? process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global" evt.provider.update(item.provider.id, (provider) => { - if (project) provider.options.aisdk.provider.project = project - provider.options.aisdk.provider.location = location + if (project) provider.request.body.project = project + provider.request.body.location = location }) } }), diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index 098e5576c4..e293a66dad 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -7,12 +7,12 @@ export const KiloPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://api.kilo.ai/api/gateway") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://opencode.ai/" - provider.options.headers["X-Title"] = "opencode" + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" }) } }), diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index 8a971f0a0d..8261830f7e 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -8,13 +8,13 @@ export const LLMGatewayPlugin = PluginV2.define({ "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { if (item.provider.enabled === false) continue - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://api.llmgateway.io/v1") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://opencode.ai/" - provider.options.headers["X-Title"] = "opencode" - provider.options.headers["X-Source"] = "opencode" + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + provider.request.headers["X-Source"] = "opencode" }) } }), diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index f9c2a0420b..837fce2c09 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -7,13 +7,13 @@ export const NvidiaPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://integrate.api.nvidia.com/v1") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://opencode.ai/" - provider.options.headers["X-Title"] = "opencode" - provider.options.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode" + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode" }) } }), diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 2d33fbcbbe..61f409ee60 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -18,8 +18,8 @@ export const OpenAIPlugin = PluginV2.define({ }), "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai") continue if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { // OpenAIPlugin sends OpenAI models through Responses; this alias is a diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 64d20f8bd4..29b48e825b 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -13,11 +13,11 @@ export const OpencodePlugin = PluginV2.define({ hasKey = Boolean( process.env.OPENCODE_API_KEY || item.provider.env.some((env) => process.env[env]) || - item.provider.options.aisdk.provider.apiKey || + item.provider.request.body.apiKey || (item.provider.enabled && item.provider.enabled.via === "account"), ) evt.provider.update(item.provider.id, (provider) => { - if (!hasKey) provider.options.aisdk.provider.apiKey = "public" + if (!hasKey) provider.request.body.apiKey = "public" }) if (hasKey) return for (const model of item.models.values()) { diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index dd3e16070b..bc56a11b54 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -8,11 +8,11 @@ export const OpenRouterPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@openrouter/ai-sdk-provider") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://opencode.ai/" - provider.options.headers["X-Title"] = "opencode" + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" }) for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { if (!item.models.has(modelID)) continue diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index 1da00989b3..a7e0bdf5a8 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -7,11 +7,11 @@ export const VercelPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/vercel") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/vercel") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["http-referer"] = "https://opencode.ai/" - provider.options.headers["x-title"] = "opencode" + provider.request.headers["http-referer"] = "https://opencode.ai/" + provider.request.headers["x-title"] = "opencode" }) } }), diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index 01c3bd8adb..a4f6a0ea01 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -7,12 +7,12 @@ export const ZenmuxPlugin = PluginV2.define({ return { "catalog.transform": Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://zenmux.ai/api/v1") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] ??= "https://opencode.ai/" - provider.options.headers["X-Title"] ??= "opencode" + provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/" + provider.request.headers["X-Title"] ??= "opencode" }) } }), diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 31127f2f3c..eae4ab9909 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -25,59 +25,27 @@ export type ID = typeof ID.Type export const ModelID = Schema.String.pipe(Schema.brand("ModelID")) export type ModelID = typeof ModelID.Type -const OpenAIResponses = Schema.Struct({ - type: Schema.Literal("openai/responses"), - url: Schema.String, - websocket: Schema.optional(Schema.Boolean), -}) - -const OpenAICompletions = Schema.Struct({ - type: Schema.Literal("openai/completions"), - url: Schema.String, - reasoning: Schema.Union([ - Schema.Struct({ - type: Schema.Literal("reasoning_content"), - }), - Schema.Struct({ - type: Schema.Literal("reasoning_details"), - }), - ]).pipe(Schema.optional), -}) -export type OpenAICompletions = typeof OpenAICompletions.Type - const AISDK = Schema.Struct({ type: Schema.Literal("aisdk"), package: Schema.String, url: Schema.String.pipe(Schema.optional), + settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) -const AnthropicMessages = Schema.Struct({ - type: Schema.Literal("anthropic/messages"), - url: Schema.String, +const Native = Schema.Struct({ + type: Schema.Literal("native"), + url: Schema.String.pipe(Schema.optional), + settings: Schema.Record(Schema.String, Schema.Unknown), }) -const UnknownEndpoint = Schema.Struct({ - type: Schema.Literal("unknown"), -}) +export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type")) +export type Api = typeof Api.Type -export const Endpoint = Schema.Union([ - UnknownEndpoint, - OpenAIResponses, - OpenAICompletions, - AnthropicMessages, - AISDK, -]).pipe(Schema.toTaggedUnion("type")) -export type Endpoint = typeof Endpoint.Type - -export const Options = Schema.Struct({ +export const Request = Schema.Struct({ headers: Schema.Record(Schema.String, Schema.String), body: Schema.Record(Schema.String, Schema.Any), - aisdk: Schema.Struct({ - provider: Schema.Record(Schema.String, Schema.Any), - request: Schema.Record(Schema.String, Schema.Any), - }), }) -export type Options = typeof Options.Type +export type Request = typeof Request.Type export class Info extends Schema.Class("ProviderV2.Info")({ id: ID, @@ -98,26 +66,23 @@ export class Info extends Schema.Class("ProviderV2.Info")({ }), ]), env: Schema.String.pipe(Schema.Array), - endpoint: Endpoint, - options: Options, + api: Api, + request: Request, }) { static empty(providerID: ID): Info { - return { + return new Info({ id: providerID, name: providerID, enabled: false, env: [], - endpoint: { - type: "unknown", + api: { + type: "native", + settings: {}, }, - options: { + request: { headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, }, - } + }) } } diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 4db2c2d887..19c7ce56b9 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -5,6 +5,7 @@ import { ConfigAgentV1 } from "./agent" import { ConfigMCPV1 } from "./mcp" import { ConfigPermissionV1 } from "./permission" import { ConfigProviderV1 } from "./provider" +import { ConfigProviderOptionsV1 } from "./provider-options" const keys = new Set([ "logLevel", @@ -99,7 +100,7 @@ function agents(info: typeof ConfigV1.Info.Type) { ...Object.entries(info.mode ?? {}).map(([name, agent]) => [name, { ...agent, mode: "primary" as const }] as const), ] if (!entries.length) return undefined - return Object.fromEntries(entries.map(([name, agent]) => [name, migrateAgent(agent)])) + return Object.fromEntries(entries.flatMap(([name, agent]) => (agent ? [[name, migrateAgent(agent)]] : []))) } function migrateAgent(info: ConfigAgentV1.Info) { @@ -111,7 +112,7 @@ function migrateAgent(info: ConfigAgentV1.Info) { return { model: info.model, variant: info.variant, - options: Object.keys(body).length ? { body } : undefined, + request: Object.keys(body).length ? { body } : undefined, system: info.prompt, description: info.description, mode: info.mode, @@ -160,22 +161,27 @@ function providers(info?: Readonly>) { } function migrateProvider(info: ConfigProviderV1.Info) { + const lowerer = ConfigProviderOptionsV1.get(info.npm) + const options = lowerer.provider(info.options ?? {}) return { name: info.name, env: info.env, - endpoint: info.npm && { - type: "aisdk" as const, - package: info.npm, - url: info.api ?? (typeof info.options?.baseURL === "string" ? info.options.baseURL : undefined), - }, - options: info.options && { body: info.options }, + api: info.npm + ? { + type: "aisdk" as const, + package: info.npm, + url: info.api ?? options.url, + settings: options.settings ?? {}, + } + : undefined, + request: info.options && { headers: options.headers, body: options.body }, models: info.models && - Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])), + Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])), } } -function migrateModel(info: typeof ConfigProviderV1.Model.Type) { +function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) { const costs = info.cost && [ { input: info.cost.input, @@ -197,16 +203,33 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) { info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined ? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] } : undefined + const lowerer = ConfigProviderOptionsV1.get(info.provider?.npm ?? packageName) return { api_id: info.id, family: info.family, name: info.name, - endpoint: info.provider?.npm && { type: "aisdk" as const, package: info.provider.npm, url: info.provider.api }, + api: info.provider?.npm + ? { type: "aisdk" as const, package: info.provider.npm, url: info.provider.api, settings: {} } + : undefined, capabilities, - options: (info.headers || info.options) && { headers: info.headers, body: info.options }, - variants: info.variants && Object.entries(info.variants).map(([id, options]) => ({ id, body: options })), + request: (info.headers || info.options) && { + headers: info.headers, + body: info.options && lowerer.request(info.options), + }, + variants: + info.variants && + Object.entries(info.variants).map(([id, options]) => ({ id, body: lowerer.request(options) })), cost: costs, disabled: info.status === "deprecated" ? true : undefined, - limit: info.limit, + limit: + info.limit && { + context: int(info.limit.context), + input: info.limit.input === undefined ? undefined : int(info.limit.input), + output: int(info.limit.output), + }, } } + +function int(value: number) { + return Math.max(Number.MIN_SAFE_INTEGER, Math.min(Number.MAX_SAFE_INTEGER, Math.trunc(value))) +} diff --git a/packages/core/src/v1/config/provider-options.ts b/packages/core/src/v1/config/provider-options.ts new file mode 100644 index 0000000000..e04542dd70 --- /dev/null +++ b/packages/core/src/v1/config/provider-options.ts @@ -0,0 +1,210 @@ +export * as ConfigProviderOptionsV1 from "./provider-options" + +type Options = Readonly> + +export interface ProviderResult { + readonly headers?: Record + readonly body?: Record + readonly url?: string + readonly settings?: Record +} + +export interface Lowerer { + readonly provider: (options: Options) => ProviderResult + readonly request: (options: Options) => Record +} + +export function get(packageName?: string): Lowerer { + const key = packageName ?? "" + return Object.hasOwn(lowerers, key) ? lowerers[key]! : raw +} + +const raw: Lowerer = { + provider(options) { + return { body: clone(options) } + }, + request: clone, +} + +const openai: Lowerer = { + provider(options) { + return { + url: string(options.baseURL), + headers: compact({ + Authorization: bearer(options.apiKey), + "OpenAI-Organization": string(options.organization), + "OpenAI-Project": string(options.project), + ...headers(options.headers), + }), + body: body(options.body), + settings: omit(options, ["apiKey", "baseURL", "organization", "project", "headers", "body"]), + } + }, + request: snake, +} + +const anthropic: Lowerer = { + provider(options) { + return { + url: string(options.baseURL), + headers: compact({ + "x-api-key": string(options.apiKey), + Authorization: options.authToken ? bearer(options.authToken) : undefined, + ...headers(options.headers), + }), + body: body(options.body), + settings: omit(options, ["apiKey", "authToken", "baseURL", "headers", "body"]), + } + }, + request(options) { + const result = snake(options) + if (options.effort !== undefined || options.taskBudget !== undefined) { + result.output_config = compactUnknown({ effort: options.effort, task_budget: options.taskBudget }) + delete result.effort + delete result.task_budget + } + if (isRecord(options.metadata) && options.metadata.userId !== undefined) { + result.metadata = { ...options.metadata, user_id: options.metadata.userId } + delete (result.metadata as Record).userId + } + return result + }, +} + +const google: Lowerer = { + provider(options) { + return { + url: string(options.baseURL), + headers: compact({ "x-goog-api-key": string(options.apiKey), ...headers(options.headers) }), + body: body(options.body), + settings: omit(options, ["apiKey", "baseURL", "headers", "body"]), + } + }, + request(options) { + const generationConfig = pick(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"]) + return { + ...omit(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"]), + ...(Object.keys(generationConfig).length ? { generationConfig } : {}), + } + }, +} + +const azure: Lowerer = { + provider(options) { + return { + url: string(options.baseURL), + headers: compact({ "api-key": string(options.apiKey), ...headers(options.headers) }), + body: body(options.body), + settings: omit(options, ["apiKey", "baseURL", "headers", "body"]), + } + }, + request: openai.request, +} + +const bedrock: Lowerer = { + provider(options) { + return direct(options) + }, + request(options) { + return { additionalModelRequestFields: clone(options) } + }, +} + +const openaiCompatible: Lowerer = { + provider(options) { + return { ...direct(options, ["baseURL"]), url: string(options.baseURL) } + }, + request(options) { + const result = clone(options) + if (options.reasoningEffort !== undefined) { + result.reasoning_effort = options.reasoningEffort + delete result.reasoningEffort + } + return result + }, +} + +const lowerers: Readonly> = { + "@ai-sdk/openai": openai, + "@ai-sdk/anthropic": anthropic, + "@ai-sdk/google-vertex/anthropic": anthropic, + "@ai-sdk/google": google, + "@ai-sdk/google-vertex": google, + "@ai-sdk/azure": azure, + "@ai-sdk/amazon-bedrock": bedrock, + "@ai-sdk/openai-compatible": openaiCompatible, + "@ai-sdk/cerebras": openaiCompatible, + "@ai-sdk/deepinfra": openaiCompatible, + "@ai-sdk/groq": openaiCompatible, + "@ai-sdk/mistral": openaiCompatible, + "@ai-sdk/togetherai": openaiCompatible, + "@ai-sdk/xai": openaiCompatible, + "@openrouter/ai-sdk-provider": openaiCompatible, + "ai-gateway-provider": openaiCompatible, + "venice-ai-sdk-provider": openaiCompatible, +} + +function direct(options: Options, extraKeys: ReadonlyArray = []): ProviderResult { + return { + headers: headers(options.headers), + body: body(options.body), + settings: omit(options, ["headers", "body", ...extraKeys]), + } +} + +function body(input: unknown) { + if (!isRecord(input)) return undefined + return { ...input } +} + +function snake(options: Options) { + return Object.fromEntries(Object.entries(options).map(([key, value]) => [snakeKey(key), snakeValue(value)])) +} + +function snakeValue(value: unknown): unknown { + if (Array.isArray(value)) return value.map(snakeValue) + if (!isRecord(value)) return value + return Object.fromEntries(Object.entries(value).map(([key, value]) => [snakeKey(key), snakeValue(value)])) +} + +function snakeKey(key: string) { + return key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase()) +} + +function clone(options: Options) { + return { ...options } +} + +function omit(options: Options, keys: ReadonlyArray) { + return Object.fromEntries(Object.entries(options).filter(([key]) => !keys.includes(key))) +} + +function pick(options: Options, keys: ReadonlyArray) { + return Object.fromEntries(Object.entries(options).filter(([key]) => keys.includes(key))) +} + +function headers(input: unknown) { + if (!isRecord(input)) return undefined + return Object.fromEntries(Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === "string")) +} + +function compact(input: Record) { + const entries = Object.entries(input).filter((entry): entry is [string, string] => entry[1] !== undefined) + return entries.length ? Object.fromEntries(entries) : undefined +} + +function compactUnknown(input: Record) { + return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined)) +} + +function string(input: unknown) { + return typeof input === "string" && input ? input : undefined +} + +function bearer(input: unknown) { + return typeof input === "string" && input ? `Bearer ${input}` : undefined +} + +function isRecord(input: unknown): input is Record { + return typeof input === "object" && input !== null && !Array.isArray(input) +} diff --git a/packages/core/test/account.test.ts b/packages/core/test/account.test.ts index c0d1479ef5..51cf2e4253 100644 --- a/packages/core/test/account.test.ts +++ b/packages/core/test/account.test.ts @@ -32,10 +32,7 @@ function context( updates.push({ id: providerID, enabled: provider.enabled, - apiKey: - typeof provider.options.aisdk.provider.apiKey === "string" - ? provider.options.aisdk.provider.apiKey - : undefined, + apiKey: typeof provider.request.body.apiKey === "string" ? provider.request.body.apiKey : undefined, }) }, remove: (providerID) => { diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 121b1fe012..08cbb4dbcd 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -20,7 +20,7 @@ const it = testEffect( ) describe("CatalogV2", () => { - it.effect("normalizes provider baseURL into endpoint url", () => + it.effect("normalizes provider baseURL into api url", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") @@ -28,16 +28,16 @@ describe("CatalogV2", () => { yield* transform((catalog) => catalog.provider.update(providerID, (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://default.example.com", } - provider.options.aisdk.provider.baseURL = "https://override.example.com" + provider.request.body.baseURL = "https://override.example.com" }), ) - expect((yield* catalog.provider.get(providerID)).endpoint).toEqual({ + expect((yield* catalog.provider.get(providerID)).api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://override.example.com", @@ -45,7 +45,7 @@ describe("CatalogV2", () => { }), ) - it.effect("normalizes model baseURL into endpoint url", () => + it.effect("normalizes model baseURL into api url", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") @@ -54,27 +54,28 @@ describe("CatalogV2", () => { yield* transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", } }) catalog.model.update(providerID, modelID, (model) => { - model.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://model.example.com" } - model.options.aisdk.provider.baseURL = "https://override.example.com" + model.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://model.example.com" } + model.request.body.baseURL = "https://override.example.com" }) }) - expect((yield* catalog.model.get(providerID, modelID)).endpoint).toEqual({ + expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://override.example.com", + settings: {}, }) }), ) - it.effect("resolves unknown model endpoint from provider endpoint", () => + it.effect("resolves default model api from provider api", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") @@ -83,7 +84,7 @@ describe("CatalogV2", () => { yield* transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", @@ -92,7 +93,7 @@ describe("CatalogV2", () => { catalog.model.update(providerID, modelID, () => {}) }) - expect((yield* catalog.model.get(providerID, modelID)).endpoint).toEqual({ + expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", @@ -115,16 +116,16 @@ describe("CatalogV2", () => { Effect.sync(() => { const item = evt.provider.get(providerID) if (!item) return - seen.push(item.provider.endpoint.type) - if (item?.provider.endpoint.type === "aisdk") seen.push(item.provider.endpoint.url) - seen.push(item?.provider.options.aisdk.provider.baseURL) + seen.push(item.provider.api.type) + if (item?.provider.api.type === "aisdk") seen.push(item.provider.api.url) + seen.push(item?.provider.request.body.baseURL) }), }), }) yield* transform((catalog) => catalog.provider.update(providerID, (provider) => { - provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" } - provider.options.aisdk.provider.baseURL = "https://provider.example.com" + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } + provider.request.body.baseURL = "https://provider.example.com" }), ) @@ -187,7 +188,7 @@ describe("CatalogV2", () => { }), ) - it.effect("resolves provider and model option merges", () => + it.effect("resolves provider and model request merges", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") @@ -196,25 +197,21 @@ describe("CatalogV2", () => { yield* transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.options.headers.provider = "provider" - provider.options.headers.shared = "provider" - provider.options.body.provider = true - provider.options.aisdk.provider.provider = true + provider.request.headers.provider = "provider" + provider.request.headers.shared = "provider" + provider.request.body.provider = true }) catalog.model.update(providerID, modelID, (model) => { - model.options.headers.model = "model" - model.options.headers.shared = "model" - model.options.body.model = true - model.options.aisdk.provider.model = true - model.options.aisdk.request.request = true + model.request.headers.model = "model" + model.request.headers.shared = "model" + model.request.body.model = true + model.request.body.request = true }) }) const model = yield* catalog.model.get(providerID, modelID) - expect(model.options.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) - expect(model.options.body).toEqual({ provider: true, model: true }) - expect(model.options.aisdk.provider).toEqual({ provider: true, model: true }) - expect(model.options.aisdk.request).toEqual({ request: true }) + expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) + expect(model.request.body).toEqual({ provider: true, model: true, request: true }) }), ) diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index dac2933cc1..f88d607906 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -107,10 +107,9 @@ describe("ConfigAgentPlugin.Plugin", () => { hidden: true, color: "warning", steps: 12, - options: { + request: { headers: { first: "one", shared: "first" }, - body: { enabled: true }, - aisdk: { provider: { profile: "review" }, request: { effort: "medium" } }, + body: { enabled: true, profile: "review", effort: "medium" }, }, }, }, @@ -121,10 +120,9 @@ describe("ConfigAgentPlugin.Plugin", () => { info: decode({ agents: { reviewer: { - options: { + request: { headers: { shared: "last", second: "two" }, - body: { retries: 2 }, - aisdk: { request: { effort: "high" } }, + body: { retries: 2, effort: "high" }, }, }, }, @@ -149,10 +147,9 @@ describe("ConfigAgentPlugin.Plugin", () => { steps: 12, model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined }, }) - expect(reviewer.options).toEqual({ + expect(reviewer.request).toEqual({ headers: { first: "one", shared: "last", second: "two" }, - body: { enabled: true, retries: 2 }, - aisdk: { provider: { profile: "review" }, request: { effort: "high" } }, + body: { enabled: true, profile: "review", retries: 2, effort: "high" }, }) }), ) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 070183c6d2..63aa757906 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -1,10 +1,12 @@ import path from "path" import fs from "fs/promises" import { describe, expect } from "bun:test" -import { Effect, Layer } from "effect" +import { Effect, Layer, Schema } from "effect" +import { FastCheck } from "effect/testing" import { Config } from "@opencode-ai/core/config" import { ConfigProvider } from "@opencode-ai/core/config/provider" import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate" +import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { FSUtil } from "@opencode-ai/core/fs-util" import { Global } from "@opencode-ai/core/global" import { Location } from "@opencode-ai/core/location" @@ -41,14 +43,10 @@ function testLayer( } const provider = { - endpoint: { type: "unknown" }, - options: { + api: { type: "native", settings: {} }, + request: { headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, }, models: {}, } @@ -62,6 +60,46 @@ describe("Config", () => { }), ) + it.effect("migrates arbitrary v1 configuration into valid v2 configuration", () => + Effect.sync(() => { + FastCheck.assert( + FastCheck.property(Schema.toArbitrary(ConfigV1.Info), (info) => { + Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(info), { errors: "all" }) + }), + { numRuns: 100 }, + ) + }), + ) + + it.effect("migrates v1 provider setup options into AISDK settings", () => + Effect.sync(() => { + const migrated = ConfigMigrateV1.migrate({ + provider: { + bedrock: { + npm: "@ai-sdk/amazon-bedrock", + options: { + headers: { "x-test": "1" }, + body: { trace: true }, + region: "us-east-1", + profile: "dev", + }, + }, + }, + }) + + expect(migrated.providers?.bedrock?.api).toEqual({ + type: "aisdk", + package: "@ai-sdk/amazon-bedrock", + url: undefined, + settings: { region: "us-east-1", profile: "dev" }, + }) + expect(migrated.providers?.bedrock?.request).toEqual({ + headers: { "x-test": "1" }, + body: { trace: true }, + }) + }), + ) + it.live("returns an empty configuration when directory files do not exist", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), @@ -186,9 +224,9 @@ describe("Config", () => { reviewer: { model: "openrouter/openai/gpt-5", variant: "high", - options: { + request: { headers: { "x-agent": "reviewer" }, - aisdk: { request: { reasoningEffort: "high" } }, + body: { reasoningEffort: "high" }, }, description: "Review changes for correctness", system: "Find regressions.", @@ -266,22 +304,21 @@ describe("Config", () => { { action: "bash", resource: "*", effect: "ask" }, { action: "bash", resource: "git status", effect: "allow" }, ]) - expect(documents[0]?.info.agents?.reviewer).toEqual({ - model: "openrouter/openai/gpt-5", - variant: "high", - options: { - headers: { "x-agent": "reviewer" }, - aisdk: { request: { reasoningEffort: "high" } }, - }, - description: "Review changes for correctness", - system: "Find regressions.", - mode: "subagent", - hidden: false, - color: "warning", - steps: 12, - disabled: false, - permissions: [{ action: "edit", resource: "*", effect: "deny" }], + const reviewer = documents[0]?.info.agents?.reviewer + expect(reviewer?.model).toBe("openrouter/openai/gpt-5") + expect(reviewer?.variant).toBe("high") + expect(reviewer?.request).toEqual({ + headers: { "x-agent": "reviewer" }, + body: { reasoningEffort: "high" }, }) + expect(reviewer?.description).toBe("Review changes for correctness") + expect(reviewer?.system).toBe("Find regressions.") + expect(reviewer?.mode).toBe("subagent") + expect(reviewer?.hidden).toBe(false) + expect(reviewer?.color).toBe("warning") + expect(reviewer?.steps).toBe(12) + expect(reviewer?.disabled).toBe(false) + expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }]) expect(documents[0]?.info.snapshots).toBe(false) expect(documents[0]?.info.watcher).toEqual({ ignore: ["node_modules/**", "dist/**", ".git"] }) expect(documents[0]?.info.formatter).toEqual({ @@ -379,6 +416,24 @@ describe("Config", () => { skills: { paths: ["./skills"], urls: ["https://example.com/.well-known/skills/"] }, reference: { docs: { path: "../docs" } }, attachment: { image: { auto_resize: false, max_width: 1200 } }, + provider: { + custom: { + options: { apiKey: "secret" }, + models: { + model: { + options: { reasoningEffort: "high" }, + variants: { fast: { temperature: 0.2 } }, + }, + }, + }, + openai: { + npm: "@ai-sdk/openai", + options: { apiKey: "secret", organization: "org" }, + models: { + model: { options: { reasoningEffort: "high", serviceTier: "priority" } }, + }, + }, + }, compaction: { auto: true, tail_turns: 3, preserve_recent_tokens: 2000, reserved: 10000 }, experimental: { mcp_timeout: 5000 }, mcp: { @@ -410,7 +465,7 @@ describe("Config", () => { expect(documents[0]?.info.agents?.reviewer).toMatchObject({ system: "Review changes.", disabled: true, - options: { body: { temperature: 0.2 } }, + request: { body: { temperature: 0.2 } }, permissions: [{ action: "read", resource: "*", effect: "allow" }], }) expect(documents[0]?.info.plugins).toEqual([ @@ -420,6 +475,20 @@ describe("Config", () => { expect(documents[0]?.info.skills).toEqual(["./skills", "https://example.com/.well-known/skills/"]) expect(documents[0]?.info.references).toEqual({ docs: { path: "../docs" } }) expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } }) + expect(documents[0]?.info.providers?.custom).toMatchObject({ + request: { body: { apiKey: "secret" } }, + models: { + model: { + request: { body: { reasoningEffort: "high" } }, + variants: [{ id: "fast", body: { temperature: 0.2 } }], + }, + }, + }) + expect(documents[0]?.info.providers?.openai).toMatchObject({ + api: { settings: {} }, + request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } }, + models: { model: { request: { body: { reasoning_effort: "high", service_tier: "priority" } } } }, + }) expect(documents[0]?.info.compaction).toEqual({ auto: true, prune: undefined, diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 01b8c9c9aa..2737396625 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -8,7 +8,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderV2 } from "@opencode-ai/core/provider" import { it } from "../plugin/provider-helper" -function options(headers: Record, variant?: string) { +function request(headers: Record, variant?: string) { return { headers, variant, @@ -35,8 +35,8 @@ describe("ConfigProviderPlugin.Plugin", () => { custom: { name: "Configured", env: ["CUSTOM_API_KEY"], - endpoint: { type: "unknown" }, - options: options({ first: "first", shared: "first" }), + api: { type: "native", settings: {} }, + request: request({ first: "first", shared: "first" }), models: { chat: { name: "First", @@ -44,7 +44,7 @@ describe("ConfigProviderPlugin.Plugin", () => { disabled: true, limit: { context: 100, output: 50 }, cost: { input: 1, output: 2 }, - options: options({ first: "first", shared: "first" }, "retained"), + request: request({ first: "first", shared: "first" }, "retained"), variants: [ { id: "fast", @@ -62,14 +62,14 @@ describe("ConfigProviderPlugin.Plugin", () => { info: decode({ providers: { custom: { - endpoint: { type: "aisdk", package: "custom-sdk", url: "https://example.test" }, - options: options({ last: "last", shared: "last" }), + api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" }, + request: request({ last: "last", shared: "last" }), models: { chat: { api_id: "api-chat", name: "Last", limit: { output: 75 }, - options: options({ last: "last", shared: "last" }), + request: request({ last: "last", shared: "last" }), variants: [ { id: "fast", @@ -110,16 +110,16 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(provider.name).toBe("Renamed") expect(provider.env).toEqual(["CUSTOM_API_KEY"]) expect(provider.enabled).toEqual({ via: "custom", data: {} }) - expect(provider.endpoint).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" }) - expect(provider.options.headers).toEqual({ first: "first", shared: "last", last: "last" }) + expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" }) + expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.apiID).toBe(ModelV2.ID.make("api-chat")) expect(model.name).toBe("Last") expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(model.enabled).toBe(false) expect(model.limit).toEqual({ context: 100, output: 75 }) expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }]) - expect(model.options.headers).toEqual({ first: "first", shared: "last", last: "last" }) - expect(model.options.variant).toBe("retained") + expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" }) + expect(model.request.variant).toBe("retained") expect(model.variants.map((variant) => variant.id)).toEqual([ ModelV2.VariantID.make("fast"), ModelV2.VariantID.make("slow"), diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index 602e85624b..5d7919936e 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -19,7 +19,7 @@ function bedrockFetch(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") { } describe("AmazonBedrockPlugin", () => { - it.effect("moves endpoint option to endpoint URL", () => + it.effect("moves endpoint option to api URL", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service const catalog = yield* Catalog.Service @@ -27,25 +27,24 @@ describe("AmazonBedrockPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const bedrock = provider("amazon-bedrock", { - endpoint: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" }, - options: { + api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" }, + request: { headers: {}, - body: {}, - aisdk: { provider: { endpoint: "https://bedrock.example" }, request: {} }, + body: { endpoint: "https://bedrock.example" }, }, }) catalog.provider.update(bedrock.id, (item) => { - item.endpoint = bedrock.endpoint - item.options = bedrock.options + item.api = bedrock.api + item.request = bedrock.request }) }) const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock) - expect(result.endpoint).toEqual({ + expect(result.api).toEqual({ type: "aisdk", package: "@ai-sdk/amazon-bedrock", url: "https://bedrock.example", }) - expect(result.options.aisdk.provider.endpoint).toBeUndefined() + expect(result.request.body.endpoint).toBeUndefined() }), ) diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index 6cae612fd2..85881c3e84 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -15,18 +15,18 @@ describe("AnthropicPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("anthropic", { - endpoint: { type: "aisdk", package: "@ai-sdk/anthropic" }, - options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/anthropic" }, + request: { headers: { Existing: "1" }, body: {} }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint - draft.options = item.options + draft.api = item.api + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).options.headers["anthropic-beta"]).toBe( + expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe( "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", ) - expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).options.headers.Existing).toBe("1") + expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1") }), ) @@ -37,7 +37,7 @@ describe("AnthropicPlugin", () => { yield* plugin.add(AnthropicPlugin) const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {})) - expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.headers["anthropic-beta"]).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined() }), ) diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index a3837a66ad..3101052cf9 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -16,17 +16,17 @@ describe("AzureCognitiveServicesPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => { - item.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" } + item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } }) }) const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")) - expect(result.endpoint).toEqual({ + expect(result.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://cognitive.cognitiveservices.azure.com/openai", }) - expect(result.options.aisdk.provider.baseURL).toBeUndefined() - expect(result.options.aisdk.provider.resourceName).toBeUndefined() + expect(result.request.body.baseURL).toBeUndefined() + expect(result.request.body.resourceName).toBeUndefined() }), ), ) @@ -40,22 +40,22 @@ describe("AzureCognitiveServicesPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const azure = provider("azure-cognitive-services", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, }) const openai = provider("openai") catalog.provider.update(azure.id, (item) => { - item.endpoint = azure.endpoint + item.api = azure.api }) catalog.provider.update(openai.id, (item) => { - item.endpoint = openai.endpoint + item.api = openai.api }) }) const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")) const openai = yield* catalog.provider.get(ProviderV2.ID.openai) - expect(azure.options.aisdk.provider.baseURL).toBeUndefined() - expect(azure.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" }) - expect(openai.options.aisdk.provider.baseURL).toBeUndefined() - expect(openai.endpoint).toEqual({ type: "aisdk", package: "test-provider" }) + expect(azure.request.body.baseURL).toBeUndefined() + expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" }) + expect(openai.request.body.baseURL).toBeUndefined() + expect(openai.api).toEqual({ type: "aisdk", package: "test-provider" }) }), ), ) diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 1b917c5af8..5011d4e626 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -34,10 +34,10 @@ describe("AzurePlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { catalog.provider.update(ProviderV2.ID.azure, (item) => { - item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" } + item.api = { type: "aisdk", package: "@ai-sdk/azure" } }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env") + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") }), ), ) @@ -51,19 +51,19 @@ describe("AzurePlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const azure = provider("azure", { - endpoint: { type: "aisdk", package: "@ai-sdk/azure" }, - options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/azure" }, + request: { headers: {}, body: { resourceName: "from-config" } }, }) catalog.provider.update(azure.id, (item) => { - item.endpoint = azure.endpoint - item.options = azure.options + item.api = azure.api + item.request = azure.request }) catalog.provider.update(ProviderV2.ID.openai, () => {}) }) - expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe( + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe( "from-config", ) - expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.aisdk.provider.resourceName).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined() }), ), ) @@ -100,10 +100,10 @@ describe("AzurePlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { catalog.provider.update(ProviderV2.ID.azure, (item) => { - item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" } + item.api = { type: "aisdk", package: "@ai-sdk/azure" } }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe( + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe( "from-account", ) }), @@ -119,15 +119,15 @@ describe("AzurePlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const azure = provider("azure", { - endpoint: { type: "aisdk", package: "@ai-sdk/azure" }, - options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/azure" }, + request: { headers: {}, body: { resourceName: "" } }, }) catalog.provider.update(azure.id, (item) => { - item.endpoint = azure.endpoint - item.options = azure.options + item.api = azure.api + item.request = azure.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env") + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") }), ), ) @@ -141,15 +141,15 @@ describe("AzurePlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const azure = provider("azure", { - endpoint: { type: "aisdk", package: "@ai-sdk/azure" }, - options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/azure" }, + request: { headers: {}, body: { resourceName: " " } }, }) catalog.provider.update(azure.id, (item) => { - item.endpoint = azure.endpoint - item.options = azure.options + item.api = azure.api + item.request = azure.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env") + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") }), ), ) @@ -227,7 +227,7 @@ describe("AzurePlugin", () => { "aisdk.language", { model: model("azure", "deployment", { - options: { headers: {}, body: {}, aisdk: { provider: {}, request: { useCompletionUrls: true } } }, + request: { headers: {}, body: { useCompletionUrls: true } }, }), sdk: fakeSelectorSdk(calls), options: {}, diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 982b587a71..aa192274d6 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -27,11 +27,11 @@ describe("CerebrasPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => { - item.endpoint = { type: "aisdk", package: "@ai-sdk/cerebras" } - item.options.headers.Existing = "1" + item.api = { type: "aisdk", package: "@ai-sdk/cerebras" } + item.request.headers.Existing = "1" }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode", }) @@ -45,7 +45,7 @@ describe("CerebrasPlugin", () => { yield* plugin.add(CerebrasPlugin) const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {})) - expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({}) }), ) diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 980269b37e..cd71cea54f 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -54,20 +54,20 @@ describe("CloudflareWorkersAIPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.endpoint = { type: "aisdk", package: "test-provider" } + provider.api = { type: "aisdk", package: "test-provider" } }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")) const sdk = yield* plugin.trigger( "aisdk.sdk", { - model: model("cloudflare-workers-ai", "@cf/model", { endpoint: provider.endpoint }), + model: model("cloudflare-workers-ai", "@cf/model", { api: provider.api }), package: "@ai-sdk/openai-compatible", options: { name: "cloudflare-workers-ai", headers: { custom: "header" } }, }, {}, ) - expect(provider.endpoint).toEqual({ + expect(provider.api).toEqual({ type: "aisdk", package: "test-provider", url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1", @@ -86,10 +86,10 @@ describe("CloudflareWorkersAIPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.endpoint = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" } + provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" } }), ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({ type: "aisdk", package: "test-provider", url: "https://proxy.example/v1", @@ -107,7 +107,7 @@ describe("CloudflareWorkersAIPlugin", () => { "aisdk.sdk", { model: model("cloudflare-workers-ai", "@cf/model", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" }, }), package: "@ai-sdk/openai-compatible", options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" }, @@ -152,10 +152,10 @@ describe("CloudflareWorkersAIPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.endpoint = { type: "aisdk", package: "test-provider" } + provider.api = { type: "aisdk", package: "test-provider" } }), ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({ type: "aisdk", package: "test-provider", url: "https://api.cloudflare.com/client/v4/accounts/account-acct/ai/v1", @@ -173,11 +173,11 @@ describe("CloudflareWorkersAIPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.endpoint = { type: "aisdk", package: "test-provider" } - provider.options.aisdk.provider.accountId = "configured-acct" + provider.api = { type: "aisdk", package: "test-provider" } + provider.request.body.accountId = "configured-acct" }), ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({ type: "aisdk", package: "test-provider", url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1", @@ -195,7 +195,7 @@ describe("CloudflareWorkersAIPlugin", () => { "aisdk.sdk", { model: model("cloudflare-workers-ai", "@cf/model", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" }, }), package: "@ai-sdk/openai-compatible", options: { @@ -224,7 +224,7 @@ describe("CloudflareWorkersAIPlugin", () => { "aisdk.sdk", { model: model("cloudflare-workers-ai", "@cf/model", { - endpoint: { + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", @@ -273,7 +273,7 @@ describe("CloudflareWorkersAIPlugin", () => { "aisdk.sdk", { model: model("cloudflare-workers-ai", "@cf/model", { - endpoint: { type: "aisdk", package: "@ai-sdk/anthropic", url: "https://proxy.example/v1" }, + api: { type: "aisdk", package: "@ai-sdk/anthropic", url: "https://proxy.example/v1" }, }), package: "@ai-sdk/anthropic", options: { name: "cloudflare-workers-ai" }, diff --git a/packages/core/test/plugin/provider-deepinfra.test.ts b/packages/core/test/plugin/provider-deepinfra.test.ts index 2341272365..43db117a90 100644 --- a/packages/core/test/plugin/provider-deepinfra.test.ts +++ b/packages/core/test/plugin/provider-deepinfra.test.ts @@ -122,7 +122,7 @@ describe("DeepInfraPlugin", () => { yield* plugin.add(DeepInfraPlugin) const language = yield* aisdk.language( model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", { - endpoint: { type: "aisdk", package: "@ai-sdk/deepinfra" }, + api: { type: "aisdk", package: "@ai-sdk/deepinfra" }, }), ) expect(language.provider).toBe("deepinfra.chat") diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index d0e7c80e7d..7ffa899866 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -122,7 +122,7 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.none()))) const exit = yield* aisdk - .language(model("missing-entrypoint", "alias", { endpoint: { type: "aisdk", package: "fixture-provider" } })) + .language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } })) .pipe(Effect.exit) expect(exit._tag).toBe("Failure") if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError") @@ -136,7 +136,7 @@ describe("DynamicProviderPlugin", () => { yield* plugin.add(dynamicPlugin()) const exit = yield* aisdk .language( - model("bad-import", "alias", { endpoint: { type: "aisdk", package: "file:///missing/provider-factory.js" } }), + model("bad-import", "alias", { api: { type: "aisdk", package: "file:///missing/provider-factory.js" } }), ) .pipe(Effect.exit) expect(exit._tag).toBe("Failure") @@ -151,7 +151,7 @@ describe("DynamicProviderPlugin", () => { const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n") yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(tmp.entrypoint)))) const exit = yield* aisdk - .language(model("missing-factory", "alias", { endpoint: { type: "aisdk", package: "fixture-provider" } })) + .language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } })) .pipe(Effect.exit) expect(exit._tag).toBe("Failure") if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError") @@ -166,7 +166,7 @@ describe("DynamicProviderPlugin", () => { const language = yield* aisdk.language( model("custom", "alias", { apiID: ModelV2.ID.make("test-model-api"), - endpoint: { type: "aisdk", package: fixtureProvider }, + api: { type: "aisdk", package: fixtureProvider }, }), ) expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } }) diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index b2cba8e465..c2cbfc8c4f 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -190,7 +190,7 @@ describe("GitLabPlugin", () => { { model: model("gitlab", "claude"), package: "gitlab-ai-provider", - options: provider.options.aisdk.provider, + options: provider.request.body, }, {}, ) @@ -238,7 +238,7 @@ describe("GitLabPlugin", () => { { model: model("gitlab", "claude"), package: "gitlab-ai-provider", - options: provider.options.aisdk.provider, + options: provider.request.body, }, {}, ) @@ -256,10 +256,9 @@ describe("GitLabPlugin", () => { "aisdk.language", { model: model("gitlab", "duo-workflow-custom", { - options: { + request: { headers: {}, - body: {}, - aisdk: { provider: {}, request: { workflowRef: "ref", workflowDefinition: "definition" } }, + body: { workflowRef: "ref", workflowDefinition: "definition" }, }, }), sdk: { @@ -320,10 +319,9 @@ describe("GitLabPlugin", () => { "aisdk.language", { model: model("gitlab", "duo-workflow-custom", { - options: { + request: { headers: {}, - body: {}, - aisdk: { provider: {}, request: { featureFlags: { request_flag: true } } }, + body: { featureFlags: { request_flag: true } }, }, }), sdk: { @@ -350,7 +348,7 @@ describe("GitLabPlugin", () => { "aisdk.language", { model: model("gitlab", "claude", { - options: { headers: { h: "v" }, body: {}, aisdk: { provider: {}, request: {} } }, + request: { headers: { h: "v" }, body: {} }, }), sdk: { workflowChat: () => undefined, diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 9d23dfefb3..bdb6029487 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -25,12 +25,12 @@ describe("GoogleVertexAnthropicPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { - provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } + provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")) - expect(provider.options.aisdk.provider.project).toBe("cloud-project") - expect(provider.options.aisdk.provider.location).toBe("cloud-location") + expect(provider.request.body.project).toBe("cloud-project") + expect(provider.request.body.location).toBe("cloud-location") }), ), ) @@ -44,14 +44,14 @@ describe("GoogleVertexAnthropicPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { - provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } - provider.options.aisdk.provider.project = "configured-project" - provider.options.aisdk.provider.location = "configured-location" + provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } + provider.request.body.project = "configured-project" + provider.request.body.location = "configured-location" }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")) - expect(provider.options.aisdk.provider.project).toBe("configured-project") - expect(provider.options.aisdk.provider.location).toBe("configured-location") + expect(provider.request.body.project).toBe("configured-project") + expect(provider.request.body.location).toBe("configured-location") }), ), ) diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 2abb342cc3..f0e8cd7420 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -53,7 +53,7 @@ describe("GoogleVertexPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", @@ -61,9 +61,9 @@ describe("GoogleVertexPlugin", () => { }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) - expect(provider.options.aisdk.provider.project).toBe("google-cloud-project") - expect(provider.options.aisdk.provider.location).toBe("google-vertex-location") - expect(provider.endpoint).toEqual({ + expect(provider.request.body.project).toBe("google-cloud-project") + expect(provider.request.body.location).toBe("google-vertex-location") + expect(provider.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location", @@ -92,7 +92,7 @@ describe("GoogleVertexPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", @@ -104,7 +104,7 @@ describe("GoogleVertexPlugin", () => { "aisdk.sdk", { model: model("google-vertex", "gemini", { - endpoint: { type: "aisdk", package: "@ai-sdk/google-vertex" }, + api: { type: "aisdk", package: "@ai-sdk/google-vertex" }, }), package: "@ai-sdk/google-vertex", options: { name: "google-vertex" }, @@ -112,8 +112,8 @@ describe("GoogleVertexPlugin", () => { {}, ) - expect(provider.options.aisdk.provider.project).toBe("vertex-project") - expect(provider.endpoint).toEqual({ + expect(provider.request.body.project).toBe("vertex-project") + expect(provider.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4", @@ -142,19 +142,19 @@ describe("GoogleVertexPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", } - provider.options.aisdk.provider.project = "config-project" - provider.options.aisdk.provider.location = "global" + provider.request.body.project = "config-project" + provider.request.body.location = "global" }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) - expect(provider.options.aisdk.provider.project).toBe("config-project") - expect(provider.options.aisdk.provider.location).toBe("global") - expect(provider.endpoint).toEqual({ + expect(provider.request.body.project).toBe("config-project") + expect(provider.request.body.location).toBe("global") + expect(provider.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global", @@ -171,17 +171,17 @@ describe("GoogleVertexPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.endpoint = { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", } - provider.options.aisdk.provider.project = "config-project" - provider.options.aisdk.provider.location = "eu" + provider.request.body.project = "config-project" + provider.request.body.location = "eu" }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) - expect(provider.endpoint).toEqual({ + expect(provider.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu", @@ -207,13 +207,13 @@ describe("GoogleVertexPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex" } - provider.options.aisdk.provider.project = "config-project" + provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" } + provider.request.body.project = "config-project" }), ) const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) - expect(provider.options.aisdk.provider.project).toBe("config-project") - expect(provider.options.aisdk.provider.location).toBe("us-central1") + expect(provider.request.body.project).toBe("config-project") + expect(provider.request.body.location).toBe("us-central1") }), ), ) @@ -233,7 +233,7 @@ describe("GoogleVertexPlugin", () => { "aisdk.sdk", { model: model("google-vertex", "gemini", { - endpoint: { type: "aisdk", package: "@ai-sdk/google-vertex" }, + api: { type: "aisdk", package: "@ai-sdk/google-vertex" }, }), package: "@ai-sdk/google-vertex", options: { name: "google-vertex" }, @@ -283,7 +283,7 @@ describe("GoogleVertexPlugin", () => { "aisdk.sdk", { model: model("google-vertex", "gemini", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, }), package: "@ai-sdk/openai-compatible", options: { name: "google-vertex" }, diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index 8844208e4b..0ff78bc183 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -52,17 +52,13 @@ describe("GooglePlugin", () => { const language = yield* aisdk.language( model("custom-google", "alias", { apiID: ModelV2.ID.make("gemini-api"), - endpoint: { + api: { type: "aisdk", package: "@ai-sdk/google", }, - options: { + request: { headers: {}, - body: {}, - aisdk: { - provider: { apiKey: "test" }, - request: {}, - }, + body: { apiKey: "test" }, }, }), ) diff --git a/packages/core/test/plugin/provider-groq.test.ts b/packages/core/test/plugin/provider-groq.test.ts index 0eb3f538b8..099e4eed72 100644 --- a/packages/core/test/plugin/provider-groq.test.ts +++ b/packages/core/test/plugin/provider-groq.test.ts @@ -83,17 +83,13 @@ describe("GroqPlugin", () => { const result = yield* aisdk.language( model("groq", "alias", { apiID: ModelV2.ID.make("llama-api"), - endpoint: { + api: { type: "aisdk", package: "@ai-sdk/groq", }, - options: { + request: { headers: {}, - body: {}, - aisdk: { - provider: { apiKey: "test" }, - request: {}, - }, + body: { apiKey: "test" }, }, }), ) diff --git a/packages/core/test/plugin/provider-helper.ts b/packages/core/test/plugin/provider-helper.ts index f99510ac70..212fbf5a59 100644 --- a/packages/core/test/plugin/provider-helper.ts +++ b/packages/core/test/plugin/provider-helper.ts @@ -54,43 +54,45 @@ export const it = testEffect( ), ) -export function provider(providerID: string, options?: Partial) { +type ProviderInput = Partial> & { + api?: ProviderV2.Api + request?: ProviderV2.Request +} + +type ModelInput = Partial> & { + api?: ProviderV2.Api + request?: ModelV2.Info["request"] +} + +export function provider(providerID: string, options?: ProviderInput) { return new ProviderV2.Info({ ...ProviderV2.Info.empty(ProviderV2.ID.make(providerID)), - endpoint: { + api: options?.api ?? { type: "aisdk", package: "test-provider", }, ...options, - options: { + request: { headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, - ...options?.options, + ...options?.request, }, }) } -export function model(providerID: string, modelID: string, options?: Partial) { +export function model(providerID: string, modelID: string, options?: ModelInput) { return new ModelV2.Info({ ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), apiID: ModelV2.ID.make(modelID), - endpoint: { + api: options?.api ?? { type: "aisdk", package: "test-provider", }, ...options, - options: { + request: { headers: {}, body: {}, - aisdk: { - provider: {}, - request: {}, - }, - ...options?.options, + ...options?.request, }, }) } diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index ac3cc172a2..33da03c032 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -25,21 +25,21 @@ describe("KiloPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const kilo = provider("kilo", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + request: { headers: { Existing: "value" }, body: {} }, }) catalog.provider.update(kilo.id, (draft) => { - draft.endpoint = kilo.endpoint - draft.options = kilo.options + draft.api = kilo.api + draft.request = kilo.request }) catalog.provider.update(provider("openrouter").id, () => {}) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({}) }), ) @@ -51,21 +51,21 @@ describe("KiloPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("kilo", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint + draft.api = item.api }) }) const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo")) - expect(result.options.headers).toEqual({ + expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect(result.options.headers).not.toHaveProperty("http-referer") - expect(result.options.headers).not.toHaveProperty("x-title") - expect(result.options.headers).not.toHaveProperty("X-Source") + expect(result.request.headers).not.toHaveProperty("http-referer") + expect(result.request.headers).not.toHaveProperty("x-title") + expect(result.request.headers).not.toHaveProperty("X-Source") }), ) @@ -77,24 +77,24 @@ describe("KiloPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const kilo = provider("kilo", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, }) catalog.provider.update(kilo.id, (draft) => { - draft.endpoint = kilo.endpoint + draft.api = kilo.api }) const custom = provider("custom-kilo", { - endpoint: { type: "aisdk", package: "kilo" }, + api: { type: "aisdk", package: "kilo" }, }) catalog.provider.update(custom.id, (draft) => { - draft.endpoint = custom.endpoint + draft.api = custom.api }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({}) }), ) }) diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 980abe2258..561f2701e1 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -26,13 +26,13 @@ describe("LLMGatewayPlugin", () => { yield* transform((catalog) => { const llmgateway = provider("llmgateway", { enabled: { via: "env", name: "LLMGATEWAY_API_KEY" }, - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, + request: { headers: { Existing: "value" }, body: {} }, }) catalog.provider.update(llmgateway.id, (draft) => { draft.enabled = llmgateway.enabled - draft.endpoint = llmgateway.endpoint - draft.options = llmgateway.options + draft.api = llmgateway.api + draft.request = llmgateway.request }) const openrouter = provider("openrouter", { enabled: { via: "env", name: "OPENROUTER_API_KEY" }, @@ -41,13 +41,13 @@ describe("LLMGatewayPlugin", () => { draft.enabled = openrouter.enabled }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-Source": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({}) }), ) @@ -59,15 +59,15 @@ describe("LLMGatewayPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("llmgateway", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint + draft.api = item.api }) }) expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).enabled).toBe(false) - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({}) }), ) }) diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index dea0000717..e4f781e54a 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -25,22 +25,22 @@ describe("NvidiaPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const nvidia = provider("nvidia", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, + request: { headers: { Existing: "value" }, body: {} }, }) catalog.provider.update(nvidia.id, (draft) => { - draft.endpoint = nvidia.endpoint - draft.options = nvidia.options + draft.api = nvidia.api + draft.request = nvidia.request }) catalog.provider.update(provider("openrouter").id, () => {}) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({}) }), ) @@ -52,16 +52,16 @@ describe("NvidiaPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("nvidia", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, - options: { headers: {}, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, + request: { headers: {}, body: {} }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint - draft.options = item.options + draft.api = item.api + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", @@ -77,20 +77,19 @@ describe("NvidiaPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("nvidia", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, - options: { + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, + request: { headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }, - body: {}, - aisdk: { provider: { baseURL: "https://integrate.api.nvidia.com/v1" }, request: {} }, + body: { baseURL: "https://integrate.api.nvidia.com/v1" }, }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint - draft.options = item.options + draft.api = item.api + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "CustomOrigin", diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index b65beb3c71..28a048e64e 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -79,9 +79,9 @@ describe("OpenAIPlugin", () => { yield* plugin.add(OpenAIPlugin) const transform = yield* catalog.transform() yield* transform((catalog) => { - const item = provider("openai", { endpoint: { type: "aisdk", package: "@ai-sdk/openai" } }) + const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint + draft.api = item.api }) catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 4054880711..b9b79a0c9c 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -33,7 +33,7 @@ describe("OpencodePlugin", () => { draft.cost = [...paid.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public") + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public") expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false) }), ), @@ -54,7 +54,7 @@ describe("OpencodePlugin", () => { draft.cost = [...free.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public") + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public") expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true) }), ), @@ -75,7 +75,7 @@ describe("OpencodePlugin", () => { draft.cost = [...outputOnly.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public") + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public") expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(true) }), ), @@ -96,7 +96,7 @@ describe("OpencodePlugin", () => { draft.cost = [...paid.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined() expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), @@ -119,7 +119,7 @@ describe("OpencodePlugin", () => { draft.cost = [...paid.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined() expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), @@ -134,24 +134,20 @@ describe("OpencodePlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("opencode", { - options: { + request: { headers: {}, - body: {}, - aisdk: { - provider: { apiKey: "configured" }, - request: {}, - }, + body: { apiKey: "configured" }, }, }) catalog.provider.update(item.id, (draft) => { - draft.options = item.options + draft.request = item.request }) const paid = model("opencode", "paid", { cost: cost(1) }) catalog.model.update(item.id, paid.id, (draft) => { draft.cost = [...paid.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("configured") + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured") expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), @@ -174,7 +170,7 @@ describe("OpencodePlugin", () => { draft.cost = [...paid.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined() expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), @@ -195,7 +191,7 @@ describe("OpencodePlugin", () => { draft.cost = [...paid.cost] }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined() expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index d19575a9bb..fe8ccb6233 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -26,22 +26,22 @@ describe("OpenRouterPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const openrouter = provider("openrouter", { - endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, + request: { headers: { Existing: "value" }, body: {} }, }) catalog.provider.update(openrouter.id, (item) => { - item.endpoint = openrouter.endpoint - item.options = openrouter.options + item.api = openrouter.api + item.request = openrouter.request }) catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {}) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).request.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({}) }), ) @@ -78,10 +78,10 @@ describe("OpenRouterPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const openrouter = provider("openrouter", { - endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, + api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, }) catalog.provider.update(openrouter.id, (item) => { - item.endpoint = openrouter.endpoint + item.api = openrouter.api }) catalog.provider.update(ProviderV2.ID.openai, () => {}) for (const item of [ diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index ba34281196..fe0e599ffb 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -15,15 +15,15 @@ describe("VercelPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("vercel", { - endpoint: { type: "aisdk", package: "@ai-sdk/vercel" }, - options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/vercel" }, + request: { headers: { Existing: "1" }, body: {} }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint - draft.options = item.options + draft.api = item.api + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).toEqual({ Existing: "1", "http-referer": "https://opencode.ai/", "x-title": "opencode", @@ -38,15 +38,15 @@ describe("VercelPlugin", () => { yield* plugin.add(VercelPlugin) const transform = yield* catalog.transform() yield* transform((catalog) => { - const item = provider("vercel", { endpoint: { type: "aisdk", package: "@ai-sdk/vercel" } }) + const item = provider("vercel", { api: { type: "aisdk", package: "@ai-sdk/vercel" } }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint + draft.api = item.api }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).not.toHaveProperty( + expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty( "HTTP-Referer", ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).not.toHaveProperty("X-Title") + expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty("X-Title") }), ) @@ -71,7 +71,7 @@ describe("VercelPlugin", () => { yield* plugin.add(VercelPlugin) const transform = yield* catalog.transform() yield* transform((catalog) => catalog.provider.update(provider("gateway").id, () => {})) - expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).request.headers).toEqual({}) }), ) }) diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index de4953b060..6181fcccc4 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -13,7 +13,7 @@ const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultL const model = new ModelV2.Info({ ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), apiID: ModelV2.ID.make("grok-4"), - endpoint: { + api: { type: "aisdk", package: "@ai-sdk/xai", }, diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index c5899627cf..101d652615 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -25,15 +25,15 @@ describe("ZenmuxPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("zenmux", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint + draft.api = item.api }) }) const result = yield* catalog.provider.get(ProviderV2.ID.make("zenmux")) - expect(result.options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }) - expect(Object.keys(result.options.headers).sort()).toEqual(["HTTP-Referer", "X-Title"]) + expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }) + expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"]) }), ) @@ -45,16 +45,16 @@ describe("ZenmuxPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("zenmux", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + request: { headers: { Existing: "value" }, body: {} }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint - draft.options = item.options + draft.api = item.api + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", @@ -70,20 +70,19 @@ describe("ZenmuxPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("zenmux", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, - options: { + api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + request: { headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }, body: {}, - aisdk: { provider: {}, request: {} }, }, }) catalog.provider.update(item.id, (draft) => { - draft.endpoint = item.endpoint - draft.options = item.options + draft.api = item.api + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) @@ -98,18 +97,17 @@ describe("ZenmuxPlugin", () => { const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("openrouter", { - options: { + request: { headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }, body: {}, - aisdk: { provider: {}, request: {} }, }, }) catalog.provider.update(item.id, (draft) => { - draft.options = item.options + draft.request = item.request }) }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2f279b663c..7d914f85ee 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2689,55 +2689,34 @@ export type ModelV2Info = { providerID: string family?: string name: string - endpoint: - | { - type: "unknown" - } - | { - type: "openai/responses" - url: string - websocket?: boolean - } - | { - type: "openai/completions" - url: string - reasoning?: - | { - type: "reasoning_content" - } - | { - type: "reasoning_details" - } - } - | { - type: "anthropic/messages" - url: string - } + api: | { type: "aisdk" package: string url?: string + settings?: { + [key: string]: unknown + } + } + | { + type: "native" + url?: string + settings: { + [key: string]: unknown + } } capabilities: { tools: boolean input: Array output: Array } - options: { + request: { headers: { [key: string]: string } body: { [key: string]: unknown } - aisdk: { - provider: { - [key: string]: unknown - } - request: { - [key: string]: unknown - } - } variant?: string } variants: Array<{ @@ -2748,14 +2727,6 @@ export type ModelV2Info = { body: { [key: string]: unknown } - aisdk: { - provider: { - [key: string]: unknown - } - request: { - [key: string]: unknown - } - } }> time: { released: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" @@ -3640,50 +3611,29 @@ export type ProviderV2Info = { } } env: Array - endpoint: - | { - type: "unknown" - } - | { - type: "openai/responses" - url: string - websocket?: boolean - } - | { - type: "openai/completions" - url: string - reasoning?: - | { - type: "reasoning_content" - } - | { - type: "reasoning_details" - } - } - | { - type: "anthropic/messages" - url: string - } + api: | { type: "aisdk" package: string url?: string + settings?: { + [key: string]: unknown + } } - options: { + | { + type: "native" + url?: string + settings: { + [key: string]: unknown + } + } + request: { headers: { [key: string]: string } body: { [key: string]: unknown } - aisdk: { - provider: { - [key: string]: unknown - } - request: { - [key: string]: unknown - } - } } } @@ -3748,55 +3698,34 @@ export type ModelV2Info1 = { providerID: string family?: string name: string - endpoint: - | { - type: "unknown" - } - | { - type: "openai/responses" - url: string - websocket?: boolean - } - | { - type: "openai/completions" - url: string - reasoning?: - | { - type: "reasoning_content" - } - | { - type: "reasoning_details" - } - } - | { - type: "anthropic/messages" - url: string - } + api: | { type: "aisdk" package: string url?: string + settings?: { + [key: string]: unknown + } + } + | { + type: "native" + url?: string + settings: { + [key: string]: unknown + } } capabilities: { tools: boolean input: Array output: Array } - options: { + request: { headers: { [key: string]: string } body: { [key: string]: unknown } - aisdk: { - provider: { - [key: string]: unknown - } - request: { - [key: string]: unknown - } - } variant?: string } variants: Array<{ @@ -3807,14 +3736,6 @@ export type ModelV2Info1 = { body: { [key: string]: unknown } - aisdk: { - provider: { - [key: string]: unknown - } - request: { - [key: string]: unknown - } - } }> time: { released: number | "NaN" | "Infinity" | "-Infinity"