diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts index 217d8fce280e..6270a25d4912 100644 --- a/packages/core/src/aisdk-native.ts +++ b/packages/core/src/aisdk-native.ts @@ -3,18 +3,87 @@ export * as AISDKNative from "./aisdk-native.js" import { Effect, Option, Schema, Struct } from "effect" import { Provider } from "./provider.js" -export interface Mapping { - readonly package: string - readonly settings: Provider.Settings +/** A provider, model, or variant overlay written in legacy AI SDK vocabulary. */ +export interface Overlay { + readonly settings?: Provider.Settings readonly headers?: Readonly> readonly body?: Readonly> } -export interface MapInput { - readonly packageName: string | undefined - readonly settings: Provider.Settings - readonly modelID: string - readonly providerID: string +/** Fields the rewrite touches; catalog records and migrated config entries both fit. */ +export interface Target { + package?: string + settings?: Provider.Settings + headers?: Readonly> + body?: Readonly> +} + +export interface ModelTarget extends Target { + readonly id: string + readonly modelID?: string + variants?: ReadonlyArray<{ readonly id: string } & Overlay> +} + +/** Rewrites a provider and its models in place so they name native packages and native settings wherever one exists. */ +export function rewrite(provider: Target, models: Iterable) { + const inherited = Provider.isAISDK(provider.package) + ? nativePackage(Provider.packageName(provider.package), undefined, provider.settings) + : undefined + // Converse request settings depend on the model, so provider-level ones move onto each model first. + const converse = inherited === BEDROCK ? Struct.pick(provider.settings ?? {}, CONVERSE_KEYS) : {} + if (inherited === BEDROCK && provider.settings) provider.settings = Struct.omit(provider.settings, CONVERSE_KEYS) + for (const model of models) { + const legacy = model.package ?? provider.package + if (!Provider.isAISDK(legacy)) continue + if (model.package === undefined) model.settings = Provider.mergeOverlay(converse, model.settings) + const modelID = model.modelID ?? model.id + const native = nativePackage( + Provider.packageName(legacy), + modelID, + Provider.mergeOverlay(provider.settings, model.settings), + ) + if (!native) continue + assign(model, translate(native, model, modelID)) + model.variants = model.variants?.map((variant) => ({ id: variant.id, ...translate(native, variant, modelID) })) + model.package = model.package !== undefined || native !== inherited ? native : undefined + } + if (!inherited) return + assign(provider, translate(inherited, provider)) + provider.package = inherited +} + +function assign(target: Target, overlay: Overlay) { + target.settings = overlay.settings + target.headers = overlay.headers + target.body = overlay.body +} + +/** Native package replacing a legacy AI SDK package, or undefined when the AI SDK runtime must run it. */ +export function nativePackage(npm: string | undefined, modelID = "", settings?: Provider.Settings) { + switch (npm) { + case "@ai-sdk/anthropic": + case "@ai-sdk/cerebras": + case "@ai-sdk/deepinfra": + case "@ai-sdk/google": + case "@ai-sdk/google-vertex": + case "@ai-sdk/groq": + case "@ai-sdk/mistral": + case "@ai-sdk/openai": + case "@ai-sdk/togetherai": + case "@ai-sdk/xai": + case "@ai-sdk/amazon-bedrock": + return `@opencode/ai/providers/${npm.slice("@ai-sdk/".length)}` + case "@ai-sdk/amazon-bedrock/mantle": + return `@opencode/ai/providers/amazon-bedrock/mantle/${modelID.includes("gpt-oss") ? "chat" : "responses"}` + case "@ai-sdk/azure": + return `@opencode/ai/providers/azure/${settings?.useCompletionUrls === true ? "chat" : "responses"}` + case "@ai-sdk/google-vertex/anthropic": + return "@opencode/ai/providers/google-vertex/messages" + case "@ai-sdk/openai-compatible": + return "@opencode/ai/providers/openai-compatible" + case "@openrouter/ai-sdk-provider": + return "@opencode/ai/providers/openrouter" + } } // A wrongly typed legacy value is dropped rather than failing the whole decode. @@ -75,51 +144,36 @@ const Legacy = Schema.StructWithRest( type Legacy = typeof Legacy.Type const decode = Schema.decodeUnknownSync(Legacy) -/** Maps a legacy AI SDK package onto the native package that replaces it. */ -export function map(input: MapInput): Mapping | undefined { - const settings = decode(input.settings) - const native = mapPackage(input.packageName, input.modelID, settings) - if (!native) return - const converse = native === "@opencode/ai/providers/amazon-bedrock" - const mapped = { - ...Struct.omit(settings, ["headers", "extraBody", "useCompletionUrls", ...OPENROUTER_KEYS]), - ...(native === "@opencode/ai/providers/openai-compatible" ? { provider: input.providerID } : {}), - } - return { - package: native, - settings: native.startsWith("@opencode/ai/providers/amazon-bedrock") ? bedrockSettings(mapped, converse) : mapped, - ...(settings.headers === undefined ? {} : { headers: settings.headers }), - ...(settings.extraBody === undefined ? {} : { body: settings.extraBody }), - ...(converse ? bedrockRequest(input.modelID, settings) : {}), - ...(native === "@opencode/ai/providers/openrouter" ? openRouterRequest(settings) : {}), - } +const BEDROCK = "@opencode/ai/providers/amazon-bedrock" + +/** Translates one legacy overlay into the spelling `native` reads. Explicit headers and body win over translated ones. */ +export function translate(native: string, overlay: Overlay, modelID = ""): Overlay { + const settings = decode(overlay.settings ?? {}) + const converse = native === BEDROCK + // Wrongly typed legacy values decode to undefined; drop them rather than forward them. + const mapped = Object.fromEntries( + Object.entries(Struct.omit(settings, ["headers", "extraBody", "useCompletionUrls", ...OPENROUTER_KEYS])).filter( + ([, value]) => value !== undefined, + ), + ) + return defined({ + settings: native.startsWith(BEDROCK) ? bedrockSettings(mapped, converse) : mapped, + headers: Provider.mergeHeaders( + native === "@opencode/ai/providers/openrouter" ? openRouterHeaders(settings) : settings.headers, + overlay.headers, + ), + body: Provider.mergeOverlay( + Provider.mergeOverlay(settings.extraBody, converse ? bedrockBody(modelID, settings) : undefined), + overlay.body, + ), + }) } -function mapPackage(packageName: string | undefined, modelID: string, settings: Legacy) { - switch (packageName) { - case "@ai-sdk/anthropic": - case "@ai-sdk/cerebras": - case "@ai-sdk/deepinfra": - case "@ai-sdk/google": - case "@ai-sdk/google-vertex": - case "@ai-sdk/groq": - case "@ai-sdk/mistral": - case "@ai-sdk/openai": - case "@ai-sdk/togetherai": - case "@ai-sdk/xai": - case "@ai-sdk/amazon-bedrock": - return `@opencode/ai/providers/${packageName.slice("@ai-sdk/".length)}` - case "@ai-sdk/amazon-bedrock/mantle": - return `@opencode/ai/providers/amazon-bedrock/mantle/${modelID.includes("gpt-oss") ? "chat" : "responses"}` - case "@ai-sdk/azure": - return `@opencode/ai/providers/azure/${settings.useCompletionUrls === true ? "chat" : "responses"}` - case "@ai-sdk/google-vertex/anthropic": - return "@opencode/ai/providers/google-vertex/messages" - case "@ai-sdk/openai-compatible": - return settings.baseURL === undefined ? undefined : "@opencode/ai/providers/openai-compatible" - case "@openrouter/ai-sdk-provider": - return "@opencode/ai/providers/openrouter" - } +// Keeps catalog records free of empty overlays. +function defined(overlay: Overlay): Overlay { + return Object.fromEntries( + Object.entries(overlay).filter(([, value]) => value !== undefined && Object.keys(value).length > 0), + ) } // AI SDK spellings the native Bedrock packages do not read. @@ -132,7 +186,7 @@ const BEDROCK_KEYS = [ "secretAccessKey", "sessionToken", ] -// Request settings Converse takes in the body; translated by `bedrockRequest`. +// Request settings Converse takes in the body; translated by `bedrockBody`. const CONVERSE_KEYS = ["additionalModelRequestFields", "reasoningConfig", "anthropicBeta", "serviceTier"] function bedrockSettings(settings: Legacy, converse: boolean) { @@ -158,7 +212,7 @@ function bedrockSettings(settings: Legacy, converse: boolean) { } } -function bedrockRequest(modelID: string, settings: Legacy): Pick { +function bedrockBody(modelID: string, settings: Legacy) { const additional = settings.additionalModelRequestFields ?? {} const reasoning = settings.reasoningConfig const anthropic = modelID.includes("anthropic") @@ -197,24 +251,22 @@ function bedrockRequest(modelID: string, settings: Legacy): Pick 0 ? { additionalModelRequestFields: fields } : {}), ...(settings.serviceTier === undefined ? {} : { serviceTier: { type: settings.serviceTier } }), } - return Object.keys(body).length === 0 ? {} : { body } + return Object.keys(body).length === 0 ? undefined : body } // Constructor options the native OpenRouter package takes as headers, plus `compatibility`, which the // native package would otherwise forward to the request body. const OPENROUTER_KEYS = ["appName", "appUrl", "api_keys", "compatibility"] as const -function openRouterRequest(settings: Legacy): Pick { - const headers = - Provider.mergeHeaders( - { - ...(settings.appName === undefined ? {} : { "X-OpenRouter-Title": settings.appName }), - ...(settings.appUrl === undefined ? {} : { "HTTP-Referer": settings.appUrl }), - ...(settings.api_keys === undefined || Object.keys(settings.api_keys).length === 0 - ? {} - : { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }), - }, - settings.headers, - ) ?? {} - return Object.keys(headers).length === 0 ? {} : { headers } +function openRouterHeaders(settings: Legacy) { + return Provider.mergeHeaders( + { + ...(settings.appName === undefined ? {} : { "X-OpenRouter-Title": settings.appName }), + ...(settings.appUrl === undefined ? {} : { "HTTP-Referer": settings.appUrl }), + ...(settings.api_keys === undefined || Object.keys(settings.api_keys).length === 0 + ? {} + : { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }), + }, + settings.headers, + ) } diff --git a/packages/core/src/github-copilot/models.ts b/packages/core/src/github-copilot/models.ts index ef2a74484d69..9562560320bf 100644 --- a/packages/core/src/github-copilot/models.ts +++ b/packages/core/src/github-copilot/models.ts @@ -149,7 +149,7 @@ function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Mo providerID: Provider.ID.githubCopilot, family: previous?.family ?? Model.Family.make(remote.capabilities.family), name: previous?.name ?? remote.name, - package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"), + package: messages ? "@opencode/ai/providers/anthropic" : Provider.aisdk("@ai-sdk/github-copilot"), settings: Provider.mergeOverlay(previous?.settings, { baseURL: messages ? `${baseURL}/v1` : baseURL, ...(endpoint ? { endpoint } : {}), diff --git a/packages/core/src/modal/models.ts b/packages/core/src/modal/models.ts index 4ee089d71ad0..74d4d4867ab6 100644 --- a/packages/core/src/modal/models.ts +++ b/packages/core/src/modal/models.ts @@ -97,7 +97,7 @@ function build(id: Model.ID, remote: RemoteModel, baseURL: string, previous?: Mo remote.interleaved === undefined ? previous?.compatibility : (Model.compatibility(remote.interleaved) ?? previous?.compatibility), - package: Provider.aisdk("@ai-sdk/openai-compatible"), + package: "@opencode/ai/providers/openai-compatible", settings: Provider.mergeOverlay(previous?.settings, { baseURL }), headers: previous?.headers, body: previous?.body, diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index f9de64f3e2c8..c17954be17a0 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -5,7 +5,6 @@ import { LanguageModel, ProviderConfigurationError } from "@opencode/ai" import { Auth } from "@opencode/ai/route" import { Context, Effect, Layer, Schema, Struct } from "effect" import { AISDK } from "./aisdk.js" -import { AISDKNative } from "./aisdk-native.js" import { Catalog } from "./catalog.js" import { Credential } from "./credential.js" import { Integration } from "./integration.js" @@ -195,25 +194,15 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct dependencies?: Dependencies, ) { const resolved = prepareRuntimeModel(model, credential) - const packageName = Provider.packageName(resolved.package) const configuration = credential?.type === "key" ? credential.configuration : undefined const configured = { ...resolved.settings, ...credential?.metadata, ...configuration } - const mapping = Provider.isAISDK(resolved.package) - ? AISDKNative.map({ - packageName, - settings: configured, - modelID: resolved.modelID ?? resolved.id, - providerID: resolved.canonical ?? resolved.providerID, - }) - : undefined - const native = mapping?.package ?? packageName - if (Provider.isAISDK(resolved.package) && !mapping) { + if (Provider.isAISDK(resolved.package)) { const loadAISDK = dependencies?.loadAISDK if (!loadAISDK) return yield* unsupported(resolved) const settings = yield* prepareProviderSettings( resolved, Provider.mergeOverlay(resolved.settings, { - ...nativeCredentialSettings(resolved.package ?? "", credential), + ...nativeCredentialSettings(resolved.package, credential), ...credential?.metadata, ...configuration, }) ?? {}, @@ -222,19 +211,22 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct Effect.mapError((error) => initialization(resolved, "init", error.cause)), ) } - if (!native) return yield* unsupported(resolved) + const specifier = Provider.packageName(resolved.package) + if (!specifier) return yield* unsupported(resolved) - const specifier = native - const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(mapping?.settings ?? configured)) + const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(configured)) const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe( Effect.mapError((error) => initialization(resolved, "load", error.cause)), ) + // The generic OpenAI-compatible package takes the catalog provider as its identity; others carry their own. + const provider = + resolved.canonical ?? (specifier === "@opencode/ai/providers/openai-compatible" ? resolved.providerID : undefined) const settings = { ...(credential ? Struct.omit(mapped, ["accessToken", "apiKey", "authToken"]) : mapped), - ...(resolved.canonical === undefined ? {} : { provider: resolved.canonical }), + ...(provider === undefined ? {} : { provider }), ...nativeCredentialSettings(specifier, credential), - headers: Provider.mergeHeaders(mapping?.headers, resolved.headers), - body: Provider.mergeOverlay(mapping?.body, resolved.body), + headers: resolved.headers, + body: resolved.body, } return yield* Effect.try({ try: () => { @@ -424,18 +416,6 @@ function hasConfiguredAuth(model: Info) { function usesAPIKeyAuth(packageName: string | undefined) { const name = Provider.packageName(packageName) return ( - name === "@ai-sdk/openai" || - name === "@ai-sdk/anthropic" || - name === "@ai-sdk/cerebras" || - name === "@ai-sdk/deepinfra" || - name === "@ai-sdk/openai-compatible" || - name === "@ai-sdk/google" || - name === "@ai-sdk/groq" || - name === "@ai-sdk/mistral" || - name === "@ai-sdk/togetherai" || - name === "@ai-sdk/xai" || - name === "@openrouter/ai-sdk-provider" || - name === "@ai-sdk/azure" || name === "@opencode/ai/providers/openai" || name?.startsWith("@opencode/ai/providers/openai/") === true || name === "@opencode/ai/providers/anthropic" || diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index a4526d87c5f4..2f4bc118b732 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -2,6 +2,7 @@ import { Cause, Context, Duration, Effect, Layer, Option, Schedule, Schema, Sema import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { ModelsDev } from "@opencode/schema/models-dev" import { Money } from "@opencode/schema/money" +import { AISDKNative } from "./aisdk-native.js" import { App } from "./app.js" import { Hash } from "@opencode/util/hash" import { FSUtil } from "@opencode/util/fs-util" @@ -87,23 +88,30 @@ function normalize(input: Record): readonly Snapshot[] { id: providerID, name: item.name, activation: "auto", - package: Provider.aisdk(item.npm), - ...(item.api ? { settings: { baseURL: item.api } } : {}), + ...packageInfo(item.npm, undefined, item.api), } satisfies Provider.Info const models: Model.Info[] = [] for (const model of Object.values(item.models)) { const baseCost = cost(model.cost) - const variants = reasoningVariants(item, model) + const npm = model.provider?.npm ?? item.npm + const own = packageInfo(npm, model.id, model.provider?.api) + const request = { + ...own, + // Models inherit the provider package unless theirs differs, e.g. Mantle gpt-oss selecting chat. + package: own.package === info.package ? undefined : own.package, + variants: reasoningVariants(own.package, model), + } const id = Model.ID.make(model.id) - models.push(modelInfo(providerID, id, model, { cost: baseCost, variants })) + models.push(modelInfo(providerID, id, model, { ...request, cost: baseCost })) for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { const modeID = Model.ID.make(`${model.id}-${mode}`) models.push( modelInfo(providerID, modeID, model, { + ...request, name: modeName(model, mode), cost: mergeCost(baseCost, options.cost), - request: options.provider, - variants, + headers: Provider.mergeHeaders(own.headers, options.provider?.headers), + body: Provider.mergeOverlay(own.body, options.provider?.body), }), ) } @@ -113,6 +121,18 @@ function normalize(input: Record): readonly Snapshot[] { return providers } +/** The catalog package for a models.dev npm package: native where one exists, otherwise the AI SDK runtime. */ +function packageInfo( + npm: string, + modelID: string | undefined, + api: string | undefined, +): { readonly package: string } & AISDKNative.Overlay { + const native = AISDKNative.nativePackage(npm, modelID) + const overlay = api === undefined ? {} : { settings: { baseURL: api } } + if (!native) return { package: Provider.aisdk(npm), ...overlay } + return { package: native, ...AISDKNative.translate(native, overlay, modelID) } +} + function released(date: string) { const time = Date.parse(date) return Number.isFinite(time) ? time : 0 @@ -187,14 +207,45 @@ function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | und const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] const OUTPUT_TOKEN_MAX = 32_000 -function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable { - const npm = model.provider?.npm ?? provider.npm +type Variant = NonNullable[number] +type Overlay = Omit + +const NATIVE = "@opencode/ai/providers/" +const OPENAI_FAMILY = [ + `${NATIVE}openai`, + `${NATIVE}azure/responses`, + `${NATIVE}amazon-bedrock/mantle/chat`, + `${NATIVE}amazon-bedrock/mantle/responses`, +] +const CHAT_FAMILY = [ + `${NATIVE}openai-compatible`, + `${NATIVE}xai`, + `${NATIVE}mistral`, + `${NATIVE}groq`, + `${NATIVE}cerebras`, + `${NATIVE}deepinfra`, + `${NATIVE}togetherai`, + Provider.aisdk("venice-ai-sdk-provider"), + Provider.aisdk("ai-gateway-provider"), +] +// The Vercel gateway runs on the AI SDK and spells upstream options the AI SDK way, so its upstream keys stay legacy. +const ANTHROPIC_FAMILY = [`${NATIVE}anthropic`, `${NATIVE}google-vertex/messages`, Provider.aisdk("@ai-sdk/anthropic")] +const GEMINI_FAMILY = [`${NATIVE}google`, `${NATIVE}google-vertex`, Provider.aisdk("@ai-sdk/google")] +const BEDROCK = `${NATIVE}amazon-bedrock` +const LEGACY_BEDROCK = Provider.aisdk("@ai-sdk/amazon-bedrock") +const OPENROUTER = `${NATIVE}openrouter` +const GATEWAY = Provider.aisdk("@ai-sdk/gateway") +const SAP = Provider.aisdk("@jerome-benoit/sap-ai-provider-v2") +const ALIBABA = Provider.aisdk("@ai-sdk/alibaba") +const COHERE = Provider.aisdk("@ai-sdk/cohere") + +function reasoningVariants(pkg: string, model: SourceModel): NonNullable { const options = model.reasoning_options if (!options?.length) return [] const toggle = options.some((option) => option.type === "toggle") const effort = options.find((option) => option.type === "effort") if (effort?.type === "effort") { - const off = toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : [] + const off = toggle ? toggleVariants(pkg, model.id).filter((variant) => variant.id === "none") : [] const variants = [ ...off, ...effort.values.flatMap((value) => { @@ -202,8 +253,8 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul const id = typeof raw === "string" && raw !== "null" ? raw : undefined if (id === undefined) return [] if (id === "none" && off.length > 0) return [] - const settings = settingsForEffort(npm, model.id, id) - return settings ? [{ id: Model.VariantID.make(id), settings }] : [] + const overlay = effortVariant(pkg, model.id, id) + return overlay ? [{ id: Model.VariantID.make(id), ...overlay }] : [] }), ] return [...new Map(variants.map((variant) => [variant.id, variant])).values()] @@ -211,80 +262,78 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul const budget = options.find((option) => option.type === "budget_tokens") if (budget?.type === "budget_tokens") return [ - ...(toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []), - ...budgetVariants(npm, model, budget), + ...(toggle ? toggleVariants(pkg, model.id).filter((variant) => variant.id === "none") : []), + ...budgetVariants(pkg, model, budget), ] - if (toggle) return toggleVariants(npm, model.id) + if (toggle) return toggleVariants(pkg, model.id) return [] } -function settingsForEffort(npm: string, modelID: string, effort: string): Provider.Settings | undefined { - if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } } - if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") { - if (anthropicManualThinking(modelID)) return { effort } +function effortVariant(pkg: string, modelID: string, effort: string): Overlay | undefined { + if (pkg === OPENROUTER) return { settings: { reasoning: { effort } } } + if (ANTHROPIC_FAMILY.includes(pkg)) + return anthropicManualThinking(modelID) + ? { settings: { effort } } + : { settings: { thinking: { type: "adaptive", display: "summarized" }, effort } } + if (GEMINI_FAMILY.includes(pkg)) + return { settings: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } } + if (pkg === BEDROCK) return { body: { additionalModelRequestFields: bedrockEffort(modelID, effort) } } + if (pkg === LEGACY_BEDROCK) return { - thinking: { type: "adaptive", display: "summarized" }, - effort, + settings: { + reasoningConfig: modelID.includes("anthropic") + ? { + ...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }), + maxReasoningEffort: effort, + } + : { type: "enabled", maxReasoningEffort: effort }, + }, } - } - if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") - return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } - if (npm === "@ai-sdk/amazon-bedrock") { - if (modelID.includes("anthropic")) - return { - reasoningConfig: { - ...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }), - maxReasoningEffort: effort, - }, - } - return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } } - } - if (npm === "@ai-sdk/gateway") { + if (pkg === GATEWAY) { const upstream = gatewayPackage(modelID) - if (upstream) return settingsForEffort(upstream, modelID, effort) - return { reasoningEffort: effort } + if (upstream) return effortVariant(upstream, modelID, effort) + return { settings: { reasoningEffort: effort } } } - if (npm === "@ai-sdk/github-copilot") { - if (modelID.includes("gemini")) return - if (modelID.includes("claude")) return { reasoningEffort: effort } - return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING } - } - if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/amazon-bedrock/mantle" || npm === "@ai-sdk/azure") - return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING } - if (npm === "@jerome-benoit/sap-ai-provider-v2") { + if (OPENAI_FAMILY.includes(pkg)) + return { + settings: { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }, + } + if (pkg === SAP) { if (modelID.includes("anthropic")) return { - modelParams: { - additionalModelRequestFields: { - ...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }), - output_config: { effort }, + settings: { + modelParams: { + additionalModelRequestFields: { + ...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }), + output_config: { effort }, + }, }, }, } if (modelID.includes("gemini")) - return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } } + return { settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } } } if (modelID.includes("amazon--nova")) - return { modelParams: { additionalModelRequestFields: { output_config: { effort } } } } - return { modelParams: { reasoning_effort: effort } } + return { settings: { modelParams: { additionalModelRequestFields: { output_config: { effort } } } } } + return { settings: { modelParams: { reasoning_effort: effort } } } } - if ( - [ - "@ai-sdk/openai-compatible", - "@ai-sdk/xai", - "@ai-sdk/mistral", - "@ai-sdk/groq", - "@ai-sdk/cerebras", - "@ai-sdk/deepinfra", - "@ai-sdk/togetherai", - "venice-ai-sdk-provider", - "ai-gateway-provider", - ].includes(npm) - ) - return { reasoningEffort: effort } + if (CHAT_FAMILY.includes(pkg)) return { settings: { reasoningEffort: effort } } +} + +// Converse spells reasoning per model family inside `additionalModelRequestFields`. +function bedrockEffort(modelID: string, effort: string) { + if (modelID.includes("anthropic")) + return { + ...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }), + output_config: { effort }, + } + // gpt-oss (Harmony) takes the flat chat-completions `reasoning_effort`; GPT-5.6+ take Responses-style `reasoning.effort`. + if (modelID.includes("openai.gpt-oss")) return { reasoning_effort: effort } + if (modelID.includes("openai.")) return { reasoning: { effort } } + return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } } } function budgetVariants( - npm: string, + pkg: string, model: SourceModel, option: Extract[number], { type: "budget_tokens" }>, ): NonNullable { @@ -295,133 +344,89 @@ function budgetVariants( { id: "high", budget: high }, { id: "max", budget: maximum }, ].flatMap((item) => { - const settings = settingsForBudget(npm, model.id, item.budget) - return settings ? [{ id: Model.VariantID.make(item.id), settings }] : [] + const overlay = budgetVariant(pkg, model.id, item.budget) + return overlay ? [{ id: Model.VariantID.make(item.id), ...overlay }] : [] }) } -function toggleVariants(npm: string, modelID: string): NonNullable { - if (npm === "@ai-sdk/gateway") { +function toggleVariants(pkg: string, modelID: string): NonNullable { + const none = Model.VariantID.make("none") + const thinking = Model.VariantID.make("thinking") + if (pkg === GATEWAY) { const upstream = gatewayPackage(modelID) if (upstream) return toggleVariants(upstream, modelID) return [ - { - id: Model.VariantID.make("none"), - settings: { reasoning: { enabled: false } }, - }, - { - id: Model.VariantID.make("thinking"), - settings: { reasoning: { enabled: true } }, - }, + { id: none, settings: { reasoning: { enabled: false } } }, + { id: thinking, settings: { reasoning: { enabled: true } } }, ] } - if (npm === "@openrouter/ai-sdk-provider") + if (pkg === OPENROUTER) return [ - { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, - { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, + { id: none, settings: { reasoning: { enabled: false } } }, + { id: thinking, settings: { reasoning: { enabled: true } } }, ] - if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") + if (ANTHROPIC_FAMILY.includes(pkg)) return [ - { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, - { - id: Model.VariantID.make("thinking"), - settings: { - thinking: { type: "adaptive", display: "summarized" }, - }, - }, + { id: none, settings: { thinking: { type: "disabled" } } }, + { id: thinking, settings: { thinking: { type: "adaptive", display: "summarized" } } }, ] - if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") + if (GEMINI_FAMILY.includes(pkg)) return [ - { - id: Model.VariantID.make("none"), - settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }, - }, - { - id: Model.VariantID.make("thinking"), - settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } }, - }, + { id: none, settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } }, + { id: thinking, settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } }, ] - if (npm === "@ai-sdk/amazon-bedrock") { + if (pkg === BEDROCK || pkg === LEGACY_BEDROCK) { const anthropic = modelID.includes("anthropic") + const off = anthropic ? { thinking: { type: "disabled" } } : { reasoningConfig: { type: "disabled" } } + const on = anthropic + ? { thinking: { type: "adaptive", display: "summarized" } } + : { reasoningConfig: { type: "enabled" } } + if (pkg === LEGACY_BEDROCK) + return [ + { id: none, settings: { additionalModelRequestFields: off } }, + { id: thinking, settings: { additionalModelRequestFields: on } }, + ] return [ - { - id: Model.VariantID.make("none"), - settings: { - additionalModelRequestFields: anthropic - ? { thinking: { type: "disabled" } } - : { reasoningConfig: { type: "disabled" } }, - }, - }, - { - id: Model.VariantID.make("thinking"), - settings: { - additionalModelRequestFields: anthropic - ? { thinking: { type: "adaptive", display: "summarized" } } - : { reasoningConfig: { type: "enabled" } }, - }, - }, + { id: none, body: { additionalModelRequestFields: off } }, + { id: thinking, body: { additionalModelRequestFields: on } }, ] } - if (npm === "@ai-sdk/alibaba") + if (pkg === ALIBABA) return [ - { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, - { id: Model.VariantID.make("thinking"), settings: { enableThinking: true } }, + { id: none, settings: { enableThinking: false } }, + { id: thinking, settings: { enableThinking: true } }, ] - if (npm === "@ai-sdk/cohere") + if (pkg === COHERE) return [ - { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, - { id: Model.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } }, + { id: none, settings: { thinking: { type: "disabled" } } }, + { id: thinking, settings: { thinking: { type: "enabled" } } }, ] - if (npm === "@jerome-benoit/sap-ai-provider-v2") { + if (pkg === SAP) { if (modelID.includes("gemini")) return [ - { - id: Model.VariantID.make("none"), - settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } }, - }, - { - id: Model.VariantID.make("thinking"), - settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } }, - }, + { id: none, settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } } }, + { id: thinking, settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } } }, ] if (modelID.includes("cohere")) return [ - { - id: Model.VariantID.make("none"), - settings: { modelParams: { thinking: { type: "disabled" } } }, - }, - { - id: Model.VariantID.make("thinking"), - settings: { modelParams: { thinking: { type: "enabled" } } }, - }, + { id: none, settings: { modelParams: { thinking: { type: "disabled" } } } }, + { id: thinking, settings: { modelParams: { thinking: { type: "enabled" } } } }, ] if (modelID.includes("amazon--nova")) return [ + { id: none, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } } }, { - id: Model.VariantID.make("none"), - settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } }, - }, - { - id: Model.VariantID.make("thinking"), + id: thinking, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } }, }, ] if (modelID.includes("anthropic")) return [ + { id: none, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } } }, { - id: Model.VariantID.make("none"), - settings: { - modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } }, - }, - }, - { - id: Model.VariantID.make("thinking"), + id: thinking, settings: { - modelParams: { - additionalModelRequestFields: { - thinking: { type: "adaptive", display: "summarized" }, - }, - }, + modelParams: { additionalModelRequestFields: { thinking: { type: "adaptive", display: "summarized" } } }, }, }, ] @@ -429,40 +434,46 @@ function toggleVariants(npm: string, modelID: string): NonNullable["modes"]>[string]["provider"] readonly variants?: NonNullable - } = {}, + }, ): Model.Info { return { id, @@ -497,8 +508,8 @@ function modelInfo( name: input.name ?? model.name, compatibility: Model.compatibility(model.interleaved), family: model.family ? Model.Family.make(model.family) : undefined, - package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined, - settings: model.provider?.api ? { baseURL: model.provider.api } : undefined, + package: input.package, + settings: input.settings, capabilities: { tools: model.tool_call, input: [...(model.modalities?.input ?? [])], @@ -514,8 +525,8 @@ function modelInfo( status: model.status ?? "active", enabled: true, limit: { context: model.limit.context, input: model.limit.input, output: model.limit.output }, - headers: input.request?.headers ? { ...input.request.headers } : undefined, - body: input.request?.body ? { ...input.request.body } : undefined, + headers: input.headers, + body: input.body, } } diff --git a/packages/core/src/plugin/aisdk-native.ts b/packages/core/src/plugin/aisdk-native.ts new file mode 100644 index 000000000000..1d5d83e5b7b4 --- /dev/null +++ b/packages/core/src/plugin/aisdk-native.ts @@ -0,0 +1,15 @@ +export * as AISDKNativePlugin from "./aisdk-native.js" + +import { Effect } from "effect" +import { define } from "@opencode/plugin/effect/plugin" +import { AISDKNative } from "../aisdk-native.js" + +/** Legacy `aisdk:` packages written by config or plugins become their native replacements before anything reads them. */ +export const Plugin = define({ + id: "opencode.aisdk-native", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform((catalog) => { + for (const record of catalog.provider.list()) AISDKNative.rewrite(record.provider, record.models.values()) + }) + }), +}) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 1691a3c7bbe8..27817f414715 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -82,6 +82,7 @@ import { AgentPlugin } from "./agent.js" import BrowserPlugin from "@opencode/plugin-browser" import { CommandPlugin } from "./command.js" import { PlanPlugin } from "./plan.js" +import { AISDKNativePlugin } from "./aisdk-native.js" import { ModelsDevPlugin } from "./models-dev.js" import { McpCodeModeExclusionPlugin } from "./mcp-codemode-exclusion.js" import { ProviderPlugins } from "./provider.js" @@ -240,6 +241,7 @@ const post = [ ConfigProviderPlugin.Plugin, ConfigWebSearchPlugin.Plugin, ConfigWorktreePlugin.Plugin, + AISDKNativePlugin.Plugin, VariantPlugin.Plugin, ConfigPolicyPlugin.Plugin, ] as const satisfies readonly InternalPlugin[] diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index d15ffd4b4997..bdf98cc5fca0 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -13,10 +13,8 @@ const CHAIN_ENV = [ "AWS_CONTAINER_CREDENTIALS_FULL_URI", ] -const isBedrock = (item: { readonly package: string }) => { - const name = Provider.packageName(item.package) - return name.startsWith("@ai-sdk/amazon-bedrock") || name.startsWith("@opencode/ai/providers/amazon-bedrock") -} +const isBedrock = (item: { readonly package: string }) => + Provider.packageName(item.package).startsWith("@opencode/ai/providers/amazon-bedrock") export const AmazonBedrockPlugin = define({ id: "opencode.provider.amazon.bedrock", diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index 5064130ef661..4cdf794bcd47 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -134,7 +134,10 @@ export const AzurePlugin = define({ yield* load() yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (item.provider.id !== Provider.ID.azure && Provider.packageName(item.provider.package) !== "@ai-sdk/azure") + if ( + item.provider.id !== Provider.ID.azure && + !Provider.packageName(item.provider.package)?.startsWith("@opencode/ai/providers/azure") + ) continue const resourceName = resolveResourceName(item.provider.settings, loaded.resource) if (resourceName) @@ -209,9 +212,9 @@ function expandResourceName(baseURL: string, resourceName: string) { } function responsesWebSocketCapable(provider: Provider.Info, model: Model.Info) { - if (Provider.packageName(model.package ?? provider.package) !== "@ai-sdk/azure") return false + if (Provider.packageName(model.package ?? provider.package) !== "@opencode/ai/providers/azure/responses") return false const settings = Provider.mergeOverlay(provider.settings, model.settings) - if (settings?.useCompletionUrls === true || settings?.useDeploymentBasedUrls === true) return false + if (settings?.useDeploymentBasedUrls === true) return false if (settings?.apiVersion !== undefined && settings.apiVersion !== "v1") return false if (typeof settings?.baseURL !== "string") return true return /^https:\/\/[^/]+\.openai\.azure\.com(?:\/|$)/i.test(settings.baseURL) diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index 3baf20f1977b..1237fdcf3108 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -7,8 +7,7 @@ export const CerebrasPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - const name = Provider.packageName(item.provider.package) - if (name !== "@ai-sdk/cerebras" && name !== "@opencode/ai/providers/cerebras") continue + if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/cerebras") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" } }) diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index 84c783d6f7f9..f11ad5c635d2 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -59,13 +59,10 @@ export const GoogleVertexPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!Provider.isAISDK(item.provider.package)) continue + const packageName = Provider.packageName(item.provider.package) if ( - Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex" && - !( - item.provider.id === Provider.ID.googleVertex && - Provider.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible") - ) + packageName !== "@opencode/ai/providers/google-vertex" && + !(item.provider.id === Provider.ID.googleVertex && packageName === "@opencode/ai/providers/openai-compatible") ) continue const project = resolveProject(item.provider.settings ?? {}) diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index 17e46d168e37..2561f3665d26 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -7,8 +7,7 @@ export const KiloPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!Provider.isAISDK(item.provider.package)) continue - if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" } diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index c720455cc654..78edeb4aea13 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -11,8 +11,7 @@ export const LLMGatewayPlugin = define({ yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { if (item.provider.activation === "disabled") continue - if (!Provider.isAISDK(item.provider.package)) continue - if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue if (!configured.has(Integration.ID.make(item.provider.id))) continue evt.provider.update(item.provider.id, (provider) => { diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index e8266ffdfd01..2194d4fd538d 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -7,8 +7,7 @@ export const NvidiaPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!Provider.isAISDK(item.provider.package)) continue - if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index eb392171d3e8..7e3fc1b900ad 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -8,8 +8,7 @@ export const OpenRouterPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!Provider.isAISDK(item.provider.package)) continue - if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue + if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openrouter") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" } }) diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index 8b90bde2b377..2159fbd66b6f 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -7,8 +7,7 @@ export const ZenmuxPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!Provider.isAISDK(item.provider.package)) continue - if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts index 0931e68c07c5..1a79ebe05229 100644 --- a/packages/core/src/plugin/variant.ts +++ b/packages/core/src/plugin/variant.ts @@ -33,8 +33,7 @@ export function generate( model: { readonly id: string; readonly modelID?: string; readonly package?: string }, provider?: { readonly package: string }, ): NonNullable { - const packageName = model.package ?? provider?.package - if (!Provider.isAISDK(packageName) || Provider.packageName(packageName) !== "@ai-sdk/openai-compatible") return [] + if (Provider.packageName(model.package ?? provider?.package) !== "@opencode/ai/providers/openai-compatible") return [] const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase() if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] return ["high", "max"].map((id) => ({ diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index ccc70d878207..6bc90cfb96a4 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -9,6 +9,7 @@ import { ConfigCommandV1 } from "./command.js" import { ConfigMCPV1 } from "./mcp.js" import { ConfigPermissionV1 } from "./permission.js" import { ConfigProviderV1 } from "./provider.js" +import { AISDKNative } from "../../aisdk-native.js" import { ConfigProviderOptionsV1 } from "./provider-options.js" import { Provider } from "../../provider.js" import { Model } from "../../model.js" @@ -239,9 +240,19 @@ function providers(info?: Readonly>) { } export function migrateProvider(sourceID: string, info: ConfigProviderV1.Info) { - if (sourceID === "azure-cognitive-services") return migrateAzureCognitiveServicesProvider(info) - if (sourceID === "google-vertex-anthropic") return migrateGoogleVertexAnthropicProvider(info) - return migrateStandardProvider(info) + const migrated = + sourceID === "azure-cognitive-services" + ? migrateAzureCognitiveServicesProvider(info) + : sourceID === "google-vertex-anthropic" + ? migrateGoogleVertexAnthropicProvider(info) + : migrateStandardProvider(info) + // V1 only knew AI SDK packages; the migrated document names the native replacements directly. + const models = Object.entries(migrated.models ?? {}).map(([id, model]) => ({ id, ...model })) + AISDKNative.rewrite(migrated, models) + return { + ...migrated, + models: migrated.models && Object.fromEntries(models.map(({ id, ...model }) => [id, model])), + } } function migrateStandardProvider(info: ConfigProviderV1.Info) { diff --git a/packages/core/test/aisdk-native.test.ts b/packages/core/test/aisdk-native.test.ts index 11c657a3396d..f4464250ee6b 100644 --- a/packages/core/test/aisdk-native.test.ts +++ b/packages/core/test/aisdk-native.test.ts @@ -1,10 +1,76 @@ import { describe, expect, test } from "bun:test" import { AISDKNative } from "@opencode/core/aisdk-native" +import { Provider } from "@opencode/core/provider" -const map = (packageName: string, settings: Readonly>, modelID = "test-model") => - AISDKNative.map({ packageName, settings, modelID, providerID: "test-provider" }) +// A model that names a legacy package, rewritten the way the catalog does it. +const map = (packageName: string, settings: Readonly>, modelID = "test-model") => { + const model: AISDKNative.ModelTarget = { id: modelID, package: Provider.aisdk(packageName), settings } + AISDKNative.rewrite({}, [model]) + const { id: _, ...rest } = model + return rest as Partial +} describe("AISDKNative", () => { + test("rewrites a provider and its models in place", () => { + const provider: AISDKNative.Target = { + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), + settings: { region: "us-east-1", bearerToken: "token" }, + } + const chat: AISDKNative.ModelTarget = { + id: "openai.gpt-oss-120b", + variants: [{ id: "high", settings: { reasoningEffort: "high" } }], + } + const responses: AISDKNative.ModelTarget = { id: "openai.gpt-5.5" } + const explicit: AISDKNative.ModelTarget = { + id: "claude", + package: Provider.aisdk("@ai-sdk/anthropic"), + settings: { effort: "high" }, + } + const opaque: AISDKNative.ModelTarget = { + id: "sonar", + package: Provider.aisdk("@ai-sdk/perplexity"), + settings: { extraBody: {} }, + } + AISDKNative.rewrite(provider, [chat, responses, explicit, opaque]) + expect(provider).toEqual({ + package: "@opencode/ai/providers/amazon-bedrock/mantle/responses", + settings: { region: "us-east-1", apiKey: "token" }, + }) + // Only models whose native package differs from the provider's name one. + expect(chat).toEqual({ + id: "openai.gpt-oss-120b", + package: "@opencode/ai/providers/amazon-bedrock/mantle/chat", + variants: [{ id: "high", settings: { reasoningEffort: "high" } }], + }) + expect(responses).toEqual({ id: "openai.gpt-5.5" }) + expect(explicit).toEqual({ + id: "claude", + package: "@opencode/ai/providers/anthropic", + settings: { effort: "high" }, + }) + expect(opaque).toEqual({ id: "sonar", package: Provider.aisdk("@ai-sdk/perplexity"), settings: { extraBody: {} } }) + }) + + test("moves provider-level Converse request settings onto each model", () => { + const provider: AISDKNative.Target = { + package: Provider.aisdk("@ai-sdk/amazon-bedrock"), + settings: { region: "us-east-1", reasoningConfig: { maxReasoningEffort: "high" } }, + } + const claude: AISDKNative.ModelTarget = { id: "anthropic.claude-sonnet-4-6" } + const nova: AISDKNative.ModelTarget = { + id: "amazon.nova-2-lite-v1:0", + variants: [{ id: "none", settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } } }], + } + AISDKNative.rewrite(provider, [claude, nova]) + expect(provider).toEqual({ package: "@opencode/ai/providers/amazon-bedrock", settings: { region: "us-east-1" } }) + expect(claude.body).toEqual({ additionalModelRequestFields: { output_config: { effort: "high" } } }) + expect(nova).toEqual({ + id: "amazon.nova-2-lite-v1:0", + body: { additionalModelRequestFields: { reasoningConfig: { maxReasoningEffort: "high" } } }, + variants: [{ id: "none", body: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } } }], + }) + }) + test("maps OpenAI-family packages and request options to native providers", () => { expect( map("@ai-sdk/openai", { @@ -30,11 +96,7 @@ describe("AISDKNative", () => { }) expect(map("@ai-sdk/openai-compatible", { baseURL: "https://example.com/v1", reasoningEffort: "high" })).toEqual({ package: "@opencode/ai/providers/openai-compatible", - settings: { - baseURL: "https://example.com/v1", - provider: "test-provider", - reasoningEffort: "high", - }, + settings: { baseURL: "https://example.com/v1", reasoningEffort: "high" }, }) }) @@ -77,10 +139,7 @@ describe("AISDKNative", () => { }, headers: { "x-provider": name }, }) - expect(map(`@ai-sdk/${name}`, {})).toEqual({ - package: `@opencode/ai/providers/${name}`, - settings: {}, - }) + expect(map(`@ai-sdk/${name}`, {})).toEqual({ package: `@opencode/ai/providers/${name}` }) } }) @@ -323,7 +382,6 @@ describe("AISDKNative", () => { }) expect(map("@ai-sdk/amazon-bedrock", { auth: "bogus" }, "anthropic.claude")).toEqual({ package: "@opencode/ai/providers/amazon-bedrock", - settings: {}, }) }) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index b593c19dd6c7..680894f1f23a 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -712,8 +712,8 @@ describe("Config", () => { }) expect(migrated.providers?.bedrock).toMatchObject({ - package: Provider.aisdk("@ai-sdk/amazon-bedrock"), - models: { claude: { package: Provider.aisdk("@ai-sdk/anthropic") } }, + package: "@opencode/ai/providers/amazon-bedrock", + models: { claude: { package: "@opencode/ai/providers/anthropic" } }, settings: { region: "us-east-1", profile: "dev" }, headers: { "x-test": "1" }, body: { trace: true }, @@ -755,14 +755,14 @@ describe("Config", () => { ]) expect(migrated.providers?.azure).toMatchObject({ env: ["AZURE_COGNITIVE_SERVICES_API_KEY"], - package: Provider.aisdk("@ai-sdk/azure"), + package: "@opencode/ai/providers/azure/responses", models: { deployment: {} }, }) expect(migrated.providers?.["azure-cognitive-services"]).toBeUndefined() expect(migrated.providers?.["google-vertex"]).toMatchObject({ settings: { project: "test-project", location: "us-central1" }, models: { - "claude-sonnet": { package: Provider.aisdk("@ai-sdk/google-vertex/anthropic") }, + "claude-sonnet": { package: "@opencode/ai/providers/google-vertex/messages" }, }, }) expect(migrated.providers?.["google-vertex"]).not.toHaveProperty("package") @@ -781,7 +781,7 @@ describe("Config", () => { expect(migrated.providers?.azure).toMatchObject({ env: ["AZURE_COGNITIVE_SERVICES_API_KEY"], - package: Provider.aisdk("@ai-sdk/openai-compatible"), + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.cognitiveservices.azure.com/openai", }, @@ -813,7 +813,7 @@ describe("Config", () => { expect(migrated.providers?.["google-vertex"]?.package).toBeUndefined() expect(migrated.providers?.["google-vertex"]?.models?.claude?.package).toBe( - Provider.aisdk("@ai-sdk/google-vertex/anthropic"), + "@opencode/ai/providers/google-vertex/messages", ) }) @@ -1445,7 +1445,7 @@ describe("Config", () => { }, }) expect(documents[0]?.info.providers?.openai).toMatchObject({ - package: Provider.aisdk("@ai-sdk/openai"), + package: "@opencode/ai/providers/openai", settings: { apiKey: "secret", organization: "org" }, models: { model: { @@ -1455,7 +1455,7 @@ describe("Config", () => { }, }) expect(documents[0]?.info.providers?.anthropic).toMatchObject({ - package: Provider.aisdk("@ai-sdk/anthropic"), + package: "@opencode/ai/providers/anthropic", models: { model: { settings: { diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 08bfd9da7388..0847fa2372ad 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -116,7 +116,7 @@ describe("ConfigProviderPlugin.Plugin", () => { info: decode({ providers: { litellm: { - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", models: { chat: {} }, }, }, @@ -145,7 +145,7 @@ describe("ConfigProviderPlugin.Plugin", () => { info: decode({ providers: { custom: { - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", models: { chat: {} }, }, }, @@ -181,7 +181,7 @@ describe("ConfigProviderPlugin.Plugin", () => { info: decode({ providers: { custom: { - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", models: { inherited: { name: "Inherited" }, overridden: { @@ -273,7 +273,7 @@ describe("ConfigProviderPlugin.Plugin", () => { models: { chat: {} }, }, native: { - package: "aisdk:@ai-sdk/openai", + package: "@opencode/ai/providers/openai", settings: { baseURL: "https://proxy.example/v1" }, headers: { "x-provider": "default" }, body: { store: false }, @@ -298,13 +298,13 @@ describe("ConfigProviderPlugin.Plugin", () => { }, }, native: { - package: "aisdk:@ai-sdk/openai", + package: "@opencode/ai/providers/openai", settings: { baseURL: "https://proxy.example/v1" }, models: { chat: { modelID: "vendor/chat", name: "Custom chat", - package: "aisdk:@ai-sdk/anthropic", + package: "@opencode/ai/providers/anthropic", settings: { baseURL: "https://model.example/v1", temperature: 0.2 }, limit: { context: 100000, input: 80000, output: 16000 }, cost: { input: 1, output: 2, cache: { read: 0.1, write: 0.2 } }, @@ -350,7 +350,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const modelID = Model.ID.make("chat") yield* catalog.transform((editor) => { editor.model.update(providerID, modelID, (model) => { - model.package = "aisdk:@ai-sdk/anthropic" + model.package = "@opencode/ai/providers/anthropic" model.settings = { baseURL: "https://catalog.example/v1" } model.capabilities = { tools: false, input: ["audio"], output: ["audio"] } model.limit = { context: 100000, input: 80000, output: 16000 } @@ -384,7 +384,7 @@ describe("ConfigProviderPlugin.Plugin", () => { info: decode({ providers: { opencode: { - package: "aisdk:@ai-sdk/openai", + package: "@opencode/ai/providers/openai", settings: { baseURL: "https://opencode.test/v1" }, models: { "alpha-gpt-next": { @@ -433,7 +433,7 @@ describe("ConfigProviderPlugin.Plugin", () => { info: decode({ providers: { opencode: { - package: "aisdk:@ai-sdk/openai", + package: "@opencode/ai/providers/openai", settings: { baseURL: "https://opencode.test/v1" }, }, }, @@ -557,7 +557,7 @@ describe("ConfigProviderPlugin.Plugin", () => { yield* catalog.transform((editor) => { editor.provider.update(Provider.ID.anthropic, (provider) => { - provider.package = "aisdk:@ai-sdk/anthropic" + provider.package = "@opencode/ai/providers/anthropic" }) editor.model.update(Provider.ID.anthropic, modelID, (model) => { model.variants = [{ id: Model.VariantID.make("fast"), settings: { effort: "high" } }] diff --git a/packages/core/test/github-copilot/models.test.ts b/packages/core/test/github-copilot/models.test.ts index b08a4f6e194c..d07254d01b3d 100644 --- a/packages/core/test/github-copilot/models.test.ts +++ b/packages/core/test/github-copilot/models.test.ts @@ -105,7 +105,7 @@ test("defensively syncs advertised Copilot models", async () => { Model.VariantID.make("high"), ]) expect(model?.capabilities.input).toEqual(["text", "image", "pdf"]) - expect(models.get(Model.ID.make("claude-sonnet"))?.package).toBe(Provider.aisdk("@ai-sdk/anthropic")) + expect(models.get(Model.ID.make("claude-sonnet"))?.package).toBe("@opencode/ai/providers/anthropic") expect(models.get(Model.ID.make("claude-sonnet"))?.settings).toMatchObject({ baseURL: `${server.url.origin}/v1`, endpoint: "messages", @@ -148,8 +148,16 @@ test("prices cache reads from either token price spelling", async () => { try { const models = await CopilotModels.get(server.url.origin, {}, []) - expect(models.get(Model.ID.make("renamed"))?.cost[0]).toMatchObject({ input: 2.5, output: 15, cache: { read: 0.25 } }) - expect(models.get(Model.ID.make("legacy"))?.cost[0]).toMatchObject({ input: 2.5, output: 15, cache: { read: 0.25 } }) + expect(models.get(Model.ID.make("renamed"))?.cost[0]).toMatchObject({ + input: 2.5, + output: 15, + cache: { read: 0.25 }, + }) + expect(models.get(Model.ID.make("legacy"))?.cost[0]).toMatchObject({ + input: 2.5, + output: 15, + cache: { read: 0.25 }, + }) expect(models.get(Model.ID.make("unpriced"))?.cost[0]).toMatchObject({ cache: { read: 0 } }) } finally { await server.stop(true) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 8c9f28cf984f..6591a21f9f27 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -657,7 +657,7 @@ describe("LocationServiceMap", () => { const catalog = yield* Catalog.Service yield* catalog.transform((editor) => { editor.provider.update(Provider.ID.make("aliased"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai") + provider.package = "@opencode/ai/providers/openai" }) editor.model.update(Provider.ID.make("aliased"), Model.ID.make("fast"), (model) => { // Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5. diff --git a/packages/core/test/modal/models.test.ts b/packages/core/test/modal/models.test.ts index fb12f0446cbe..07518f370c96 100644 --- a/packages/core/test/modal/models.test.ts +++ b/packages/core/test/modal/models.test.ts @@ -65,7 +65,7 @@ test("maps live Modal models onto catalog templates", async () => { expect(model?.family).toBe(Model.Family.make("catalog-family")) expect(model?.providerID).toBe(providerID) expect(model?.modelID).toBe(Model.ID.make("live-model")) - expect(model?.package).toBe(Provider.aisdk("@ai-sdk/openai-compatible")) + expect(model?.package).toBe("@opencode/ai/providers/openai-compatible") expect(model?.settings).toMatchObject({ baseURL: `${server.url.origin}/v1` }) expect(model?.compatibility).toMatchObject({ reasoningField: "reasoning_content" }) expect(model?.capabilities).toMatchObject({ tools: true, input: ["text", "image"], output: ["text"] }) diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index c8e2ccadbee3..832c0098916a 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -11,6 +11,7 @@ import { Provider } from "@opencode/core/provider" import { ModelResolver } from "@opencode/core/model-resolver" import { Catalog } from "@opencode/core/catalog" import { AISDK } from "@opencode/core/aisdk" +import { AISDKNative } from "@opencode/core/aisdk-native" import { Npm } from "@opencode/util/npm" import { it } from "./lib/effect" @@ -26,8 +27,9 @@ interface ModelOptions { readonly limit?: Info["limit"] } -const model = (packageName: string | undefined, options: ModelOptions = {}) => - Info.make({ +// A catalog model as the resolver receives it: legacy packages are already rewritten to native ones. +const model = (packageName: string | undefined, options: ModelOptions = {}) => { + const info = Info.make({ id: ID.make("test-model"), modelID: ID.make(options.modelID ?? "api-test-model"), providerID: options.providerID ?? Provider.ID.make("test-provider"), @@ -46,6 +48,10 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => enabled: true, limit: options.limit ?? { context: 100, output: 20 }, }) + const draft = structuredClone(info) + AISDKNative.rewrite({}, [draft]) + return draft +} function withEnv(variables: Record, effect: () => Effect.Effect) { return Effect.acquireUseRelease( @@ -215,7 +221,7 @@ describe("ModelResolver", () => { id: "bedrock-mantle-responses", endpoint: { baseURL: "https://bedrock-mantle.us-west-2.api.aws/openai/v1" }, }) - expect(catalog.settings?.baseURL).toBe("https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1") + expect(catalog.settings?.baseURL).toBe("https://bedrock-mantle.us-west-2.api.aws/openai/v1") }), ), ) @@ -478,7 +484,7 @@ describe("ModelResolver", () => { expect(prepared.body).toMatchObject({ max_completion_tokens: 10 }) expect(prepared.body).not.toHaveProperty("max_tokens") expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1") - expect(resolved.route.defaults.http?.body).toEqual({}) + expect(resolved.route.defaults.http?.body ?? {}).toEqual({}) }), ) @@ -711,7 +717,7 @@ describe("ModelResolver", () => { }), ) - expect(resolved.route.defaults.http?.body).toEqual({}) + expect(resolved.route.defaults.http?.body ?? {}).toEqual({}) }), ) @@ -1409,7 +1415,7 @@ describe("ModelResolver", () => { _tag: "SessionRunnerModel.ModelConfigurationError", providerID: "azure", modelID: "test-model", - package: "aisdk:@ai-sdk/azure", + package: "@opencode/ai/providers/azure/responses", detail: "Azure requires resourceName or baseURL", }) expect(failure.message).toBe("Cannot initialize azure/test-model: Azure requires resourceName or baseURL") diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 1672464ce92c..445c86a6f2a0 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -49,7 +49,7 @@ const fixtureSnapshot = [ id: Provider.ID.make("acme"), name: "Acme", activation: "auto", - package: Provider.aisdk("@ai-sdk/openai-compatible"), + package: "@opencode/ai/providers/openai-compatible", }, models: [ { @@ -112,7 +112,7 @@ const fixture2Snapshot = [ id: Provider.ID.make("beta"), name: "Beta", activation: "auto", - package: Provider.aisdk("@ai-sdk/openai-compatible"), + package: "@opencode/ai/providers/openai-compatible", }, models: [ { @@ -254,8 +254,8 @@ describe("ModelsDev Service", () => { cache, ModelsDev.Service.use((service) => service.get()), ) - expect(result[0]?.info.package).toBe(Provider.aisdk("@ai-sdk/openai-compatible")) - expect(result[0]?.models[0]?.package).toBe(Provider.aisdk("@ai-sdk/openai")) + expect(result[0]?.info.package).toBe("@opencode/ai/providers/openai-compatible") + expect(result[0]?.models[0]?.package).toBe("@opencode/ai/providers/openai") }), ) diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 37796b2045bf..0ce45795341d 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -1081,18 +1081,19 @@ describe("ModelsDevPlugin", () => { Provider.ID.make("amazon-bedrock"), Model.ID.make("us.amazon.nova-2-lite-v1:0"), ) + // Native Bedrock takes Converse request fields in the body. expect(bedrock?.variants).toEqual([ { id: Model.VariantID.make("none"), - settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } }, + body: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } }, }, { id: Model.VariantID.make("low"), - settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, + body: { additionalModelRequestFields: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } } }, }, { id: Model.VariantID.make("high"), - settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, + body: { additionalModelRequestFields: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } } }, }, ]) diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index 44e6a57c4bb0..7c67d94a9701 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -57,7 +57,7 @@ const seedBedrock = Effect.fn(function* (settings?: Record) { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.amazonBedrock, (item) => { - item.package = Provider.aisdk("@ai-sdk/amazon-bedrock") + item.package = "@opencode/ai/providers/amazon-bedrock" if (settings) item.settings = settings }) }) @@ -71,7 +71,7 @@ describe("AmazonBedrockPlugin", () => { const catalog = yield* seedBedrock({ endpoint: "https://bedrock.example" }) yield* addPlugin() const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock)) - expect(result.package).toBe(Provider.aisdk("@ai-sdk/amazon-bedrock")) + expect(result.package).toBe("@opencode/ai/providers/amazon-bedrock") expect(result.settings).toEqual({ baseURL: "https://bedrock.example", region: "us-east-1" }) }), ), @@ -152,7 +152,7 @@ describe("AmazonBedrockPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.amazonBedrock, (item) => { - item.package = Provider.aisdk("@ai-sdk/amazon-bedrock") + item.package = "@opencode/ai/providers/amazon-bedrock" item.activation = "disabled" }) }) @@ -206,13 +206,13 @@ describe("AmazonBedrockPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("mantle"), (item) => { - item.package = Provider.aisdk("@ai-sdk/amazon-bedrock/mantle") + item.package = "@opencode/ai/providers/amazon-bedrock/mantle/responses" }) catalog.provider.update(Provider.ID.make("native"), (item) => { item.package = "@opencode/ai/providers/amazon-bedrock" }) catalog.provider.update(Provider.ID.make("other"), (item) => { - item.package = Provider.aisdk("@ai-sdk/anthropic") + item.package = "@opencode/ai/providers/anthropic" }) }) yield* addPlugin() diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index a42861fd4e54..bbc73586a8f9 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -244,7 +244,7 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((editor) => { editor.provider.update(Provider.ID.azure, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/azure") + provider.package = "@opencode/ai/providers/azure/responses" }) editor.model.update(Provider.ID.azure, Model.ID.make("gpt-5-mini"), () => {}) editor.model.update(Provider.ID.azure, Model.ID.make("gpt-5-nano"), () => {}) @@ -311,7 +311,7 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.azure, (item) => { - item.package = Provider.aisdk("@ai-sdk/azure") + item.package = "@opencode/ai/providers/azure/responses" }) }) yield* addPlugin() @@ -326,7 +326,7 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.azure, (item) => { - item.package = Provider.aisdk("@ai-sdk/azure") + item.package = "@opencode/ai/providers/azure/responses" }) }) yield* addPlugin() @@ -341,13 +341,13 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.azure, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.cognitiveservices.azure.com/openai", } }) catalog.model.update(Provider.ID.azure, Model.ID.make("anthropic"), (model) => { - model.package = Provider.aisdk("@ai-sdk/anthropic") + model.package = "@opencode/ai/providers/anthropic" model.settings = { resourceName: "model-resource", baseURL: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", @@ -376,7 +376,7 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.azure, (item) => { - item.package = Provider.aisdk("@ai-sdk/azure") + item.package = "@opencode/ai/providers/azure/responses" item.settings = { resourceName: "from-config" } }) catalog.provider.update(Provider.ID.openai, () => {}) @@ -394,7 +394,7 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.azure, (item) => { - item.package = Provider.aisdk("@ai-sdk/azure") + item.package = "@opencode/ai/providers/azure/responses" item.settings = { resourceName: "" } }) }) @@ -410,7 +410,7 @@ describe("AzurePlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.azure, (item) => { - item.package = Provider.aisdk("@ai-sdk/azure") + item.package = "@opencode/ai/providers/azure/responses" item.settings = { resourceName: " " } }) }) @@ -434,11 +434,11 @@ describe("AzurePlugin", () => { } yield* catalog.transform((editor) => { editor.provider.update(Provider.ID.azure, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/azure") + provider.package = "@opencode/ai/providers/azure/responses" }) editor.model.update(Provider.ID.azure, models.responses, () => {}) editor.model.update(Provider.ID.azure, models.chat, (model) => { - model.settings = { useCompletionUrls: true } + model.package = "@opencode/ai/providers/azure/chat" }) editor.model.update(Provider.ID.azure, models.preview, (model) => { model.settings = { apiVersion: "2025-04-01-preview" } @@ -450,7 +450,7 @@ describe("AzurePlugin", () => { model.settings = { baseURL: "https://gateway.example/azure" } }) editor.model.update(Provider.ID.azure, models.nonAzure, (model) => { - model.package = Provider.aisdk("@ai-sdk/anthropic") + model.package = "@opencode/ai/providers/anthropic" }) }) @@ -467,5 +467,4 @@ describe("AzurePlugin", () => { }), ), ) - }) diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 9940c5b5bd59..d8cfb337ea9d 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -22,7 +22,7 @@ describe("CerebrasPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("cerebras"), (item) => { - item.package = Provider.aisdk("@ai-sdk/cerebras") + item.package = "@opencode/ai/providers/cerebras" item.headers = { ...item.headers, Existing: "1" } }) }) diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index 6e4c53aac724..9b5f3810cd90 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -302,7 +302,7 @@ describe("GithubCopilotPlugin", () => { yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.githubCopilot, () => {}) catalog.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"), (model) => { - model.package = Provider.aisdk("@ai-sdk/openai-compatible") + model.package = "@opencode/ai/providers/openai-compatible" }) }) yield* addPlugin() diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 306907def6b4..a68b7ad22543 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -69,7 +69,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.opencode, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" } }), ) @@ -95,7 +95,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: @@ -108,7 +108,7 @@ describe("GoogleVertexPlugin", () => { expect(provider.settings?.project).toBe("google-cloud-project") expect(provider.settings?.location).toBe("google-vertex-location") expect(provider).toMatchObject({ - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location", @@ -131,7 +131,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/google-vertex") + provider.package = "@opencode/ai/providers/google-vertex" }), ) yield* addPlugin() @@ -154,7 +154,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/google-vertex") + provider.package = "@opencode/ai/providers/google-vertex" }), ) yield* addPlugin() @@ -180,7 +180,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: @@ -193,7 +193,7 @@ describe("GoogleVertexPlugin", () => { expect(provider.settings?.project).toBe("vertex-project") expect(provider).toMatchObject({ - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4", @@ -218,7 +218,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: @@ -234,7 +234,7 @@ describe("GoogleVertexPlugin", () => { expect(provider.settings?.project).toBe("config-project") expect(provider.settings?.location).toBe("global") expect(provider).toMatchObject({ - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" }, }) expect(model.settings).toEqual(provider.settings) @@ -247,7 +247,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: @@ -259,7 +259,7 @@ describe("GoogleVertexPlugin", () => { yield* addPlugin() const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider).toMatchObject({ - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" }, }) }), @@ -280,7 +280,7 @@ describe("GoogleVertexPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/google-vertex") + provider.package = "@opencode/ai/providers/google-vertex" provider.settings = { ...provider.settings, project: "config-project" } }), ) @@ -313,7 +313,7 @@ describe("GoogleVertexPlugin", () => { model: Model.Info.make({ ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), modelID: Model.ID.make("gemini"), - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", }), package: "@ai-sdk/openai-compatible", options: { diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index ccfd90241c96..97fd3e6d0714 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -27,7 +27,7 @@ describe("KiloPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("kilo"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } provider.headers = { Existing: "value" } }) @@ -48,7 +48,7 @@ describe("KiloPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("kilo"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } }) }) @@ -72,7 +72,7 @@ describe("KiloPlugin", () => { provider.package = Provider.aisdk("kilo") }) catalog.provider.update(Provider.ID.make("custom-kilo"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } }) }) diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 5f8e381983f4..b7256602891b 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -33,7 +33,7 @@ describe("LLMGatewayPlugin", () => { }) yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://api.llmgateway.io/v1" } provider.headers = { Existing: "value" } }) @@ -60,7 +60,7 @@ describe("LLMGatewayPlugin", () => { yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => { provider.activation = "disabled" - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://api.llmgateway.io/v1" } }) }) diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index 667fc6c44aea..a247a6f227ba 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -27,7 +27,7 @@ describe("NvidiaPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } provider.headers = { Existing: "value" } }) @@ -49,7 +49,7 @@ describe("NvidiaPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } }) }) @@ -68,7 +68,7 @@ describe("NvidiaPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" } }) diff --git a/packages/core/test/plugin/provider-ollama.test.ts b/packages/core/test/plugin/provider-ollama.test.ts index 1bf3fb7d7896..97abb31cdc6a 100644 --- a/packages/core/test/plugin/provider-ollama.test.ts +++ b/packages/core/test/plugin/provider-ollama.test.ts @@ -71,7 +71,7 @@ describe("OllamaPlugin", () => { } : body.model === "unknown-context" ? show({ family: "unknown", capabilities: ["completion"], context: 0 }) - : show({ family: "nomic-bert", capabilities: ["embedding"], context: 8192 }), + : show({ family: "nomic-bert", capabilities: ["embedding"], context: 8192 }), ) }, }), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 1bcb8f8990be..aac0bb7ff3dc 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -370,7 +370,7 @@ describe("OpencodePlugin", () => { remote: { canonical: "openai", name: "Remote", - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", env: ["REMOTE_API_KEY"], settings: { baseURL: `${origin}/v1`, @@ -409,7 +409,7 @@ describe("OpencodePlugin", () => { }, override: { name: "Override", - package: "aisdk:@ai-sdk/anthropic", + package: "@opencode/ai/providers/anthropic", settings: { baseURL: `${origin}/anthropic` }, }, disabled: { name: "Disabled", disabled: true }, @@ -428,11 +428,11 @@ describe("OpencodePlugin", () => { const integrations = yield* Integration.Service yield* catalog.transform((editor) => { editor.provider.update(Provider.ID.openai, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai") + provider.package = "@opencode/ai/providers/openai" provider.integrationID = Integration.ID.make("openai") }) editor.model.update(Provider.ID.openai, Model.ID.make("api-model"), (model) => { - model.package = Provider.aisdk("@ai-sdk/openai") + model.package = "@opencode/ai/providers/openai" model.settings = { baseURL: "https://upstream.example/v1" } model.variants = [ { @@ -464,7 +464,7 @@ describe("OpencodePlugin", () => { canonical: "openai", name: "Remote", integrationID: "opencode", - package: Provider.aisdk("@ai-sdk/openai-compatible"), + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: `${server.url.origin}/v1`, custom: "value" }, headers: { "x-org-id": "org" }, }) @@ -481,13 +481,13 @@ describe("OpencodePlugin", () => { capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }], limit: { context: 1000, output: 100 }, - package: Provider.aisdk("@ai-sdk/openai-compatible"), + package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 }, headers: { "x-org-id": "org" }, }) expect(model.settings).toEqual({ baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 }) const override = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("override"))) - expect(override.package).toBe(Provider.aisdk("@ai-sdk/anthropic")) + expect(override.package).toBe("@opencode/ai/providers/anthropic") expect(override.settings?.baseURL).toBe(`${server.url.origin}/anthropic`) expect(model.variants).toEqual([ { @@ -990,16 +990,16 @@ describe("OpencodePlugin", () => { providers: { remote: { canonical: "openai", - package: "aisdk:@ai-sdk/openai", + package: "@opencode/ai/providers/openai", settings: { baseURL: new URL(request.url).origin }, models: { ...models, - override: { ...models.astra, package: "aisdk:@ai-sdk/openai-compatible" }, + override: { ...models.astra, package: "@opencode/ai/providers/openai-compatible" }, }, }, compatible: { canonical: "openai", - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode/ai/providers/openai-compatible", models, }, }, diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 3a4061852bd2..69f56e578159 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -28,7 +28,7 @@ describe("OpenRouterPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.openrouter, (provider) => { - provider.package = Provider.aisdk("@openrouter/ai-sdk-provider") + provider.package = "@opencode/ai/providers/openrouter" provider.headers = { Existing: "value" } }) catalog.provider.update(Provider.ID.make("nvidia"), () => {}) @@ -49,7 +49,7 @@ describe("OpenRouterPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.openrouter, (provider) => { - provider.package = Provider.aisdk("@openrouter/ai-sdk-provider") + provider.package = "@opencode/ai/providers/openrouter" }) catalog.provider.update(Provider.ID.openai, () => {}) catalog.model.update(Provider.ID.openrouter, Model.ID.make("openai/gpt-5-chat"), () => {}) diff --git a/packages/core/test/plugin/provider-poe.test.ts b/packages/core/test/plugin/provider-poe.test.ts index c2288cc3cf9d..923ff7f27f0b 100644 --- a/packages/core/test/plugin/provider-poe.test.ts +++ b/packages/core/test/plugin/provider-poe.test.ts @@ -43,7 +43,7 @@ const fixture = Effect.gen(function* () { }) yield* catalog.transform((editor) => { editor.provider.update(providerID, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://api.poe.com/v1" } }) editor.model.update(providerID, modelID, () => {}) diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 063d38884c99..3c0595c55e2b 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -56,7 +56,7 @@ const fixture = Effect.fn(function* () { const host = yield* PluginHost.make(plugin) yield* catalog.transform((editor) => { editor.provider.update(providerID, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { baseURL: "https://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/api/v2/cortex/v1" } }) editor.model.update(providerID, modelID, () => {}) @@ -225,9 +225,7 @@ it.live("uses environment tokens rather than the account identifier and bypasses type: "env", name: "SNOWFLAKE_CORTEX_TOKEN", }) - expect((yield* test.catalog.provider.get(providerID))?.package).toBe( - Provider.aisdk("@ai-sdk/openai-compatible"), - ) + expect((yield* test.catalog.provider.get(providerID))?.package).toBe("@opencode/ai/providers/openai-compatible") expect(yield* test.hooks.has("aisdk", "sdk", providerID)).toBe(false) yield* test.stop diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index 9ada80a90d23..a9a8f4553bbd 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -32,7 +32,7 @@ describe("ZenmuxPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } }) }) @@ -48,7 +48,7 @@ describe("ZenmuxPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } provider.headers = { ...provider.headers, Existing: "value" } }) @@ -68,7 +68,7 @@ describe("ZenmuxPlugin", () => { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" } }) diff --git a/packages/core/test/plugin/skill.test.ts b/packages/core/test/plugin/skill.test.ts index 66fdbed89318..7e379b1e8419 100644 --- a/packages/core/test/plugin/skill.test.ts +++ b/packages/core/test/plugin/skill.test.ts @@ -71,12 +71,7 @@ describe("SkillPlugin.Plugin", () => { expect(report?.content).toContain("- Active plugins: -disabled, local.ts, package-plugin, package-plugin") }).pipe( Effect.provide( - config([ - "package-plugin", - "-disabled", - "local.ts", - { package: "package-plugin", options: { enabled: true } }, - ]), + config(["package-plugin", "-disabled", "local.ts", { package: "package-plugin", options: { enabled: true } }]), ), ), ) diff --git a/packages/core/test/plugin/variant.test.ts b/packages/core/test/plugin/variant.test.ts index 249364592a64..b688c3ee5a44 100644 --- a/packages/core/test/plugin/variant.test.ts +++ b/packages/core/test/plugin/variant.test.ts @@ -23,11 +23,11 @@ describe("VariantPlugin", () => { const service = yield* Catalog.Service yield* service.transform((catalog) => { catalog.provider.update(Provider.ID.opencode, (provider) => { - provider.package = Provider.aisdk("@ai-sdk/openai-compatible") + provider.package = "@opencode/ai/providers/openai-compatible" }) catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => { model.modelID = Model.ID.make("glm-5.2") - model.package = Provider.aisdk("@ai-sdk/openai-compatible") + model.package = "@opencode/ai/providers/openai-compatible" }) }) yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) @@ -45,7 +45,7 @@ describe("VariantPlugin", () => { yield* service.transform((catalog) => { catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => { model.modelID = Model.ID.make("glm-5.2") - model.package = Provider.aisdk("@ai-sdk/openai-compatible") + model.package = "@opencode/ai/providers/openai-compatible" model.variants = [{ id: Model.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }] }) }) diff --git a/services/www/src/docs/content/migrate-v1.mdx b/services/www/src/docs/content/migrate-v1.mdx index da5c8c432ae5..250145357e98 100644 --- a/services/www/src/docs/content/migrate-v1.mdx +++ b/services/www/src/docs/content/migrate-v1.mdx @@ -336,7 +336,7 @@ Rename the singular `provider` map to `providers`. V2 separates the runtime pack { "providers": { "acme": { - "package": "aisdk:@ai-sdk/openai-compatible", + "package": "@opencode/ai/providers/openai-compatible", "settings": { "baseURL": "https://llm.example.com/v1", "apiKey": "{env:ACME_API_KEY}" @@ -346,8 +346,10 @@ Rename the singular `provider` map to `providers`. V2 separates the runtime pack } ``` -V1 `npm` becomes `package`, and AI SDK packages receive the `aisdk:` prefix. `api` becomes `settings.baseURL`. Provider -`options` are separated into `settings`, `headers`, and `body` according to their request role. See [Providers](/providers). +V1 `npm` becomes `package`. AI SDK packages with a native OpenCode implementation become that native package, and their +options are rewritten to the native spelling; other AI SDK packages keep running on the AI SDK with an `aisdk:` prefix. +`api` becomes `settings.baseURL`. Provider `options` are separated into `settings`, `headers`, and `body` according to +their request role. See [Providers](/providers). V2 consolidated two legacy provider namespaces: