From 026c6f8c9c554604251ff46bd7ebc7307ffabe13 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Thu, 5 Feb 2026 16:13:37 -0800 Subject: [PATCH 1/4] fix: Recursively compute reasoning variants for Vercel AI Gateway provider based on model id --- packages/opencode/src/provider/transform.ts | 66 +++++- .../opencode/test/provider/transform.test.ts | 188 +++++++++++++++++- 2 files changed, 242 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 876a26fce71..fab6ae5b8f1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -44,6 +44,42 @@ export namespace ProviderTransform { return undefined } + // For Vercel AI Gateway models, maps the model ID prefix to the native SDK + // npm package so we can reuse existing provider-specific logic. + function gatewayUnderlyingNpm(model: Provider.Model): string | undefined { + if (model.api.npm !== "@ai-sdk/gateway") return undefined + const prefix = model.api.id.split("/")[0] + switch (prefix) { + case "anthropic": + return "@ai-sdk/anthropic" + case "openai": + return "@ai-sdk/openai" + case "google": + return "@ai-sdk/google" + case "xai": + return "@ai-sdk/xai" + case "groq": + return "@ai-sdk/groq" + } + return undefined + } + + // Creates a virtual model that looks like it comes from the native SDK, + // stripping the gateway provider prefix from IDs so existing checks work. + function gatewayModel(model: Provider.Model, npm: string): Provider.Model { + const prefix = model.api.id.split("/")[0] + const id = model.api.id.slice(prefix.length + 1) + return { ...model, id, providerID: prefix, api: { ...model.api, id, npm } } + } + + // Wraps provider-specific options under a providerOptions key so that + // providerOptions() can extract and place them under the correct provider key. + function wrapGatewayOptions(model: Provider.Model, opts: Record): Record { + if (Object.keys(opts).length === 0) return opts + const prefix = model.api.id.split("/")[0] + return { providerOptions: { [prefix]: opts } } + } + function normalizeMessages( msgs: ModelMessage[], model: Provider.Model, @@ -363,9 +399,12 @@ export namespace ProviderTransform { if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) return {} return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }])) - // TODO: YOU CANNOT SET max_tokens if this is set!!! - case "@ai-sdk/gateway": - return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) + case "@ai-sdk/gateway": { + const npm = gatewayUnderlyingNpm(model) + if (!npm) return {} + const native = variants(gatewayModel(model, npm)) + return Object.fromEntries(Object.entries(native).map(([key, opts]) => [key, wrapGatewayOptions(model, opts)])) + } case "@ai-sdk/github-copilot": if (model.id.includes("gemini")) { @@ -589,6 +628,12 @@ export namespace ProviderTransform { sessionID: string providerOptions?: Record }): Record { + const npm = gatewayUnderlyingNpm(input.model) + if (npm) { + const native = options({ ...input, model: gatewayModel(input.model, npm) }) + return wrapGatewayOptions(input.model, native) + } + const result: Record = {} // openai and providers using openai package should set store to false by default. @@ -693,7 +738,13 @@ export namespace ProviderTransform { return result } - export function smallOptions(model: Provider.Model) { + export function smallOptions(model: Provider.Model): Record { + const npm = gatewayUnderlyingNpm(model) + if (npm) { + const native = smallOptions(gatewayModel(model, npm)) + return wrapGatewayOptions(model, native) + } + if ( model.providerID === "openai" || model.api.npm === "@ai-sdk/openai" || @@ -725,6 +776,13 @@ export namespace ProviderTransform { export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { const key = sdkKey(model.api.npm) ?? model.providerID + if (model.api.npm === "@ai-sdk/gateway") { + const { providerOptions: explicit = {}, ...rest } = options + return { + ...(Object.keys(rest).length > 0 ? { [key]: rest } : {}), + ...explicit, + } + } return { [key]: options } } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 02bb5278fc7..b0ffd82ce1b 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -175,6 +175,101 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { }) }) +describe("ProviderTransform.providerOptions", () => { + const createMockModel = (overrides: Partial = {}): any => ({ + id: "test/test-model", + providerID: "test", + api: { + id: "test-model", + url: "https://api.test.com", + npm: "@ai-sdk/openai", + }, + ...overrides, + }) + + test("wraps options under SDK key for normal providers", () => { + const model = createMockModel({ + api: { id: "claude-sonnet-4", url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" }, + }) + const result = ProviderTransform.providerOptions(model, { + thinking: { type: "enabled", budgetTokens: 16000 }, + }) + expect(result).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }) + }) + + test("does not extract providerOptions key for non-gateway providers", () => { + const model = createMockModel({ + api: { id: "claude-sonnet-4", url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" }, + }) + const result = ProviderTransform.providerOptions(model, { + thinking: { type: "enabled", budgetTokens: 16000 }, + providerOptions: { something: "should be kept as-is" }, + }) + expect(result).toEqual({ + anthropic: { + thinking: { type: "enabled", budgetTokens: 16000 }, + providerOptions: { something: "should be kept as-is" }, + }, + }) + }) + + test("extracts providerOptions key for gateway and spreads into result", () => { + const model = createMockModel({ + providerID: "vercel", + api: { id: "anthropic/claude-sonnet-4", url: "https://ai-gateway.vercel.sh", npm: "@ai-sdk/gateway" }, + }) + const result = ProviderTransform.providerOptions(model, { + providerOptions: { + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }, + }) + expect(result).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }) + }) + + test("combines gateway rest options with explicit providerOptions", () => { + const model = createMockModel({ + providerID: "vercel", + api: { id: "anthropic/claude-sonnet-4", url: "https://ai-gateway.vercel.sh", npm: "@ai-sdk/gateway" }, + }) + const result = ProviderTransform.providerOptions(model, { + order: ["vertex", "anthropic"], + providerOptions: { + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }, + }) + expect(result).toEqual({ + gateway: { order: ["vertex", "anthropic"] }, + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }) + }) + + test("gateway with only rest options and no providerOptions key", () => { + const model = createMockModel({ + providerID: "vercel", + api: { id: "anthropic/claude-sonnet-4", url: "https://ai-gateway.vercel.sh", npm: "@ai-sdk/gateway" }, + }) + const result = ProviderTransform.providerOptions(model, { + order: ["vertex"], + }) + expect(result).toEqual({ + gateway: { order: ["vertex"] }, + }) + }) + + test("gateway with empty options", () => { + const model = createMockModel({ + providerID: "vercel", + api: { id: "anthropic/claude-sonnet-4", url: "https://ai-gateway.vercel.sh", npm: "@ai-sdk/gateway" }, + }) + const result = ProviderTransform.providerOptions(model, {}) + expect(result).toEqual({}) + }) +}) + describe("ProviderTransform.schema - gemini array items", () => { test("adds missing items for array properties", () => { const geminiModel = { @@ -1408,20 +1503,97 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/gateway", () => { - test("returns OPENAI_EFFORTS with reasoningEffort", () => { + test("anthropic models return thinking variants with providerOptions wrapper", () => { const model = createMockModel({ - id: "gateway/gateway-model", - providerID: "gateway", + id: "anthropic/claude-sonnet-4.5", + providerID: "vercel", api: { - id: "gateway-model", - url: "https://gateway.ai", + id: "anthropic/claude-sonnet-4.5", + url: "https://ai-gateway.vercel.sh", npm: "@ai-sdk/gateway", }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) - expect(result.low).toEqual({ reasoningEffort: "low" }) - expect(result.high).toEqual({ reasoningEffort: "high" }) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ + providerOptions: { + anthropic: { + thinking: { + type: "enabled", + budgetTokens: expect.any(Number), + }, + }, + }, + }) + expect(result.max).toEqual({ + providerOptions: { + anthropic: { + thinking: { + type: "enabled", + budgetTokens: expect.any(Number), + }, + }, + }, + }) + }) + + test("openai models return reasoningEffort variants with providerOptions wrapper", () => { + const model = createMockModel({ + id: "openai/o3", + providerID: "vercel", + api: { + id: "openai/o3", + url: "https://ai-gateway.vercel.sh", + npm: "@ai-sdk/gateway", + }, + release_date: "2025-12-04", + }) + const result = ProviderTransform.variants(model) + expect(result.high).toEqual({ + providerOptions: { + openai: expect.objectContaining({ + reasoningEffort: "high", + }), + }, + }) + }) + + test("google gemini-2.5 models return thinkingConfig variants with providerOptions wrapper", () => { + const model = createMockModel({ + id: "google/gemini-2.5-flash", + providerID: "vercel", + api: { + id: "google/gemini-2.5-flash", + url: "https://ai-gateway.vercel.sh", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ + providerOptions: { + google: { + thinkingConfig: { + includeThoughts: true, + thinkingBudget: 16000, + }, + }, + }, + }) + }) + + test("unknown provider prefix returns empty variants", () => { + const model = createMockModel({ + id: "meta/llama-4-scout", + providerID: "vercel", + api: { + id: "meta/llama-4-scout", + url: "https://ai-gateway.vercel.sh", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(result).toEqual({}) }) }) From 2daa3b46af26d1f4ce4456ad3a34720c6b71844b Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Thu, 5 Feb 2026 16:28:31 -0800 Subject: [PATCH 2/4] move parseModel to transform.ts and use it for splitting, update other imports to point to provider.ts --- packages/opencode/src/acp/agent.ts | 5 ++-- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/cli/cmd/agent.ts | 3 ++- packages/opencode/src/cli/cmd/github.ts | 3 ++- packages/opencode/src/cli/cmd/run.ts | 3 ++- packages/opencode/src/cli/cmd/tui/app.tsx | 3 ++- .../src/cli/cmd/tui/context/local.tsx | 5 ++-- packages/opencode/src/provider/provider.ts | 12 ++-------- packages/opencode/src/provider/transform.ts | 24 +++++++++++++------ packages/opencode/src/session/prompt.ts | 6 ++--- .../opencode/test/provider/provider.test.ts | 5 ++-- 11 files changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index ae6f6fcc296..d4e1fcb5360 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -33,6 +33,7 @@ import { pathToFileURL } from "bun" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" +import { ProviderTransform } from "../provider/transform" import { Agent as AgentModule } from "../agent/agent" import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" @@ -1471,7 +1472,7 @@ export namespace ACP { .then((resp) => { const cfg = resp.data if (!cfg || !cfg.model) return undefined - const parsed = Provider.parseModel(cfg.model) + const parsed = ProviderTransform.parseModel(cfg.model) return { providerID: parsed.providerID, modelID: parsed.modelID, @@ -1646,7 +1647,7 @@ export namespace ACP { modelId: string, providers: Array<{ id: string; models: Record }> }>, ): { model: { providerID: string; modelID: string }; variant?: string } { - const parsed = Provider.parseModel(modelId) + const parsed = ProviderTransform.parseModel(modelId) const provider = providers.find((p) => p.id === parsed.providerID) if (!provider) { return { model: parsed, variant: undefined } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index e338559be7e..b448d0314f2 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -215,7 +215,7 @@ export namespace Agent { options: {}, native: false, } - if (value.model) item.model = Provider.parseModel(value.model) + if (value.model) item.model = ProviderTransform.parseModel(value.model) item.variant = value.variant ?? item.variant item.prompt = value.prompt ?? item.prompt item.description = value.description ?? item.description diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index e5da9fdb386..804141d3b9c 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -4,6 +4,7 @@ import { UI } from "../ui" import { Global } from "../../global" import { Agent } from "../../agent/agent" import { Provider } from "../../provider/provider" +import { ProviderTransform } from "../../provider/transform" import path from "path" import fs from "fs/promises" import matter from "gray-matter" @@ -120,7 +121,7 @@ const AgentCreateCommand = cmd({ // Generate agent const spinner = prompts.spinner() spinner.start("Generating agent configuration...") - const model = args.model ? Provider.parseModel(args.model) : undefined + const model = args.model ? ProviderTransform.parseModel(args.model) : undefined const generated = await Agent.generate({ description, model }).catch((error) => { spinner.stop(`LLM failed to generate agent: ${error.message}`, 1) if (isFullyNonInteractive) process.exit(1) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 7f9a03d948a..4cf7aacfe95 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -23,6 +23,7 @@ import { bootstrap } from "../bootstrap" import { Session } from "../../session" import { Identifier } from "../../id/id" import { Provider } from "../../provider/provider" +import { ProviderTransform } from "../../provider/transform" import { Bus } from "../../bus" import { MessageV2 } from "../../session/message-v2" import { SessionPrompt } from "@/session/prompt" @@ -644,7 +645,7 @@ export const GithubRunCommand = cmd({ const value = process.env["MODEL"] if (!value) throw new Error(`Environment variable "MODEL" is not set`) - const { providerID, modelID } = Provider.parseModel(value) + const { providerID, modelID } = ProviderTransform.parseModel(value) if (!providerID.length || !modelID.length) throw new Error(`Invalid model ${value}. Model must be in the format "provider/model".`) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 163a5820d99..99724ef15ee 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -9,6 +9,7 @@ import { EOL } from "os" import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider/provider" +import { ProviderTransform } from "../../provider/transform" import { Agent } from "../../agent/agent" import { PermissionNext } from "../../permission/next" import { Tool } from "../../tool/tool" @@ -570,7 +571,7 @@ export const RunCommand = cmd({ variant: args.variant, }) } else { - const model = args.model ? Provider.parseModel(args.model) : undefined + const model = args.model ? ProviderTransform.parseModel(args.model) : undefined await sdk.session.prompt({ sessionID, agent, diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ab3d0968925..a90682438c4 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -34,6 +34,7 @@ import { Session as SessionApi } from "@/session" import { TuiEvent } from "./event" import { KVProvider, useKV } from "./context/kv" import { Provider } from "@/provider/provider" +import { ProviderTransform } from "@/provider/transform" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { writeHeapSnapshot } from "v8" @@ -283,7 +284,7 @@ function App() { batch(() => { if (args.agent) local.agent.set(args.agent) if (args.model) { - const { providerID, modelID } = Provider.parseModel(args.model) + const { providerID, modelID } = ProviderTransform.parseModel(args.model) if (!providerID || !modelID) return toast.show({ variant: "warning", diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 72c72dc5bb3..b4cdd9508f9 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -9,6 +9,7 @@ import { iife } from "@/util/iife" import { createSimpleContext } from "./helper" import { useToast } from "../ui/toast" import { Provider } from "@/provider/provider" +import { ProviderTransform } from "@/provider/transform" import { useArgs } from "./args" import { useSDK } from "./sdk" import { RGBA } from "@opentui/core" @@ -156,7 +157,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const args = useArgs() const fallbackModel = createMemo(() => { if (args.model) { - const { providerID, modelID } = Provider.parseModel(args.model) + const { providerID, modelID } = ProviderTransform.parseModel(args.model) if (isModelValid({ providerID, modelID })) { return { providerID, @@ -166,7 +167,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } if (sync.data.config.model) { - const { providerID, modelID } = Provider.parseModel(sync.data.config.model) + const { providerID, modelID } = ProviderTransform.parseModel(sync.data.config.model) if (isModelValid({ providerID, modelID })) { return { providerID, diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 44bcf8adb3d..418ee5439c7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1155,7 +1155,7 @@ export namespace Provider { const cfg = await Config.get() if (cfg.small_model) { - const parsed = parseModel(cfg.small_model) + const parsed = ProviderTransform.parseModel(cfg.small_model) return getModel(parsed.providerID, parsed.modelID) } @@ -1229,7 +1229,7 @@ export namespace Provider { export async function defaultModel() { const cfg = await Config.get() - if (cfg.model) return parseModel(cfg.model) + if (cfg.model) return ProviderTransform.parseModel(cfg.model) const providers = await list() const recent = (await Bun.file(path.join(Global.Path.state, "model.json")) @@ -1253,14 +1253,6 @@ export namespace Provider { } } - export function parseModel(model: string) { - const [providerID, ...rest] = model.split("/") - return { - providerID: providerID, - modelID: rest.join("/"), - } - } - export const ModelNotFoundError = NamedError.create( "ProviderModelNotFoundError", z.object({ diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index fab6ae5b8f1..ba8b39418b8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -44,12 +44,19 @@ export namespace ProviderTransform { return undefined } + export function parseModel(model: string) { + const [providerID, ...rest] = model.split("/") + return { + providerID: providerID, + modelID: rest.join("/"), + } + } + // For Vercel AI Gateway models, maps the model ID prefix to the native SDK // npm package so we can reuse existing provider-specific logic. function gatewayUnderlyingNpm(model: Provider.Model): string | undefined { if (model.api.npm !== "@ai-sdk/gateway") return undefined - const prefix = model.api.id.split("/")[0] - switch (prefix) { + switch (parseModel(model.api.id).providerID) { case "anthropic": return "@ai-sdk/anthropic" case "openai": @@ -67,17 +74,20 @@ export namespace ProviderTransform { // Creates a virtual model that looks like it comes from the native SDK, // stripping the gateway provider prefix from IDs so existing checks work. function gatewayModel(model: Provider.Model, npm: string): Provider.Model { - const prefix = model.api.id.split("/")[0] - const id = model.api.id.slice(prefix.length + 1) - return { ...model, id, providerID: prefix, api: { ...model.api, id, npm } } + const parsed = parseModel(model.api.id) + return { + ...model, + id: parsed.modelID, + providerID: parsed.providerID, + api: { ...model.api, id: parsed.modelID, npm }, + } } // Wraps provider-specific options under a providerOptions key so that // providerOptions() can extract and place them under the correct provider key. function wrapGatewayOptions(model: Provider.Model, opts: Record): Record { if (Object.keys(opts).length === 0) return opts - const prefix = model.api.id.split("/")[0] - return { providerOptions: { [prefix]: opts } } + return { providerOptions: { [parseModel(model.api.id).providerID]: opts } } } function normalizeMessages( diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index be813e823fb..b57d7b2b224 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1795,7 +1795,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const taskModel = await (async () => { if (command.model) { - return Provider.parseModel(command.model) + return ProviderTransform.parseModel(command.model) } if (command.agent) { const cmdAgent = await Agent.get(command.agent) @@ -1803,7 +1803,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the return cmdAgent.model } } - if (input.model) return Provider.parseModel(input.model) + if (input.model) return ProviderTransform.parseModel(input.model) return await lastModel(input.sessionID) })() @@ -1854,7 +1854,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const userAgent = isSubtask ? (input.agent ?? (await Agent.defaultAgent())) : agentName const userModel = isSubtask ? input.model - ? Provider.parseModel(input.model) + ? ProviderTransform.parseModel(input.model) : await lastModel(input.sessionID) : taskModel diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 98cd49c02fd..9cb3663849d 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -4,6 +4,7 @@ import path from "path" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" +import { ProviderTransform } from "../../src/provider/transform" import { Env } from "../../src/env" test("provider loaded from env variable", async () => { @@ -350,13 +351,13 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => { }) test("parseModel correctly parses provider/model string", () => { - const result = Provider.parseModel("anthropic/claude-sonnet-4") + const result = ProviderTransform.parseModel("anthropic/claude-sonnet-4") expect(result.providerID).toBe("anthropic") expect(result.modelID).toBe("claude-sonnet-4") }) test("parseModel handles model IDs with slashes", () => { - const result = Provider.parseModel("openrouter/anthropic/claude-3-opus") + const result = ProviderTransform.parseModel("openrouter/anthropic/claude-3-opus") expect(result.providerID).toBe("openrouter") expect(result.modelID).toBe("anthropic/claude-3-opus") }) From 5a6907ddbfab7e87bf2bd385908a7e531a301a71 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Thu, 5 Feb 2026 16:30:46 -0800 Subject: [PATCH 3/4] remove low-value tests --- .../opencode/test/provider/transform.test.ts | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index b0ffd82ce1b..9116da59aac 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -187,18 +187,6 @@ describe("ProviderTransform.providerOptions", () => { ...overrides, }) - test("wraps options under SDK key for normal providers", () => { - const model = createMockModel({ - api: { id: "claude-sonnet-4", url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" }, - }) - const result = ProviderTransform.providerOptions(model, { - thinking: { type: "enabled", budgetTokens: 16000 }, - }) - expect(result).toEqual({ - anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, - }) - }) - test("does not extract providerOptions key for non-gateway providers", () => { const model = createMockModel({ api: { id: "claude-sonnet-4", url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" }, @@ -259,15 +247,6 @@ describe("ProviderTransform.providerOptions", () => { gateway: { order: ["vertex"] }, }) }) - - test("gateway with empty options", () => { - const model = createMockModel({ - providerID: "vercel", - api: { id: "anthropic/claude-sonnet-4", url: "https://ai-gateway.vercel.sh", npm: "@ai-sdk/gateway" }, - }) - const result = ProviderTransform.providerOptions(model, {}) - expect(result).toEqual({}) - }) }) describe("ProviderTransform.schema - gemini array items", () => { @@ -1558,30 +1537,6 @@ describe("ProviderTransform.variants", () => { }) }) - test("google gemini-2.5 models return thinkingConfig variants with providerOptions wrapper", () => { - const model = createMockModel({ - id: "google/gemini-2.5-flash", - providerID: "vercel", - api: { - id: "google/gemini-2.5-flash", - url: "https://ai-gateway.vercel.sh", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - providerOptions: { - google: { - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 16000, - }, - }, - }, - }) - }) - test("unknown provider prefix returns empty variants", () => { const model = createMockModel({ id: "meta/llama-4-scout", From c59d674748b38d276ce5757d034592bcc890d698 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Thu, 5 Feb 2026 17:00:16 -0800 Subject: [PATCH 4/4] Update documentation --- packages/web/src/content/docs/providers.mdx | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index db473ad36bb..7fa34a4fd98 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1675,6 +1675,30 @@ Some useful routing options: | `only` | Restrict to specific providers | | `zeroDataRetention` | Only use providers with zero data retention policies | +To pass provider-specific settings through the gateway, use the `providerOptions` key. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "vercel": { + "models": { + "openai/gpt-5.2": { + "options": { + "order": ["openai", "azure"], + "providerOptions": { + "openai": { + "textVerbosity": "high" + } + } + } + } + } + } + } +} +``` + --- ### xAI