From b110291d38867ac7d264257216af32d29211a418 Mon Sep 17 00:00:00 2001 From: Michael Yochpaz <8832013+MichaelYochpaz@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:12:26 +0200 Subject: [PATCH 1/2] fix(provider): enable thinking for google-vertex-anthropic models Fixes reasoning/thinking not working for Claude models on GCP Vertex AI by correcting the npm package identifier and provider options key mapping. The issue had two root causes: 1. models.dev API returns npm: '@ai-sdk/google-vertex' for google-vertex-anthropic provider, but variant generation expects '@ai-sdk/google-vertex/anthropic' (subpath import) 2. sdkKey() function didn't map '@ai-sdk/google-vertex/anthropic' to 'anthropic' key, causing thinking options to be wrapped with wrong provider key Changes: - Transform npm package to '@ai-sdk/google-vertex/anthropic' for google-vertex-anthropic provider in fromModelsDevModel() - Add '@ai-sdk/google-vertex/anthropic' case to sdkKey() to return 'anthropic' key - Add comprehensive tests for npm transformation, variant generation, and providerOptions key mapping --- packages/opencode/src/provider/provider.ts | 3 +- packages/opencode/src/provider/transform.ts | 1 + .../opencode/test/provider/provider.test.ts | 63 ++++++ .../opencode/test/provider/transform.test.ts | 193 ++++++++++++++++++ 4 files changed, 259 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fdd4ccdfb61..2811825b133 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -592,7 +592,7 @@ export namespace Provider { }) export type Info = z.infer - function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { + export function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const m: Model = { id: model.id, providerID: provider.id, @@ -603,6 +603,7 @@ export namespace Provider { url: provider.api!, npm: iife(() => { if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" + if (provider.id === "google-vertex-anthropic") return "@ai-sdk/google-vertex/anthropic" return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" }), }, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index acccbd1c09f..6538848fead 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -26,6 +26,7 @@ export namespace ProviderTransform { case "@ai-sdk/amazon-bedrock": return "bedrock" case "@ai-sdk/anthropic": + case "@ai-sdk/google-vertex/anthropic": return "anthropic" case "@ai-sdk/google-vertex": case "@ai-sdk/google": diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8a2009646e0..fcca38f9133 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -2013,6 +2013,69 @@ test("all variants can be disabled via config", async () => { }) }) +test("google-vertex-anthropic transforms npm package to subpath import", () => { + // This test verifies that even though models.dev returns "@ai-sdk/google-vertex" as the npm package, + // we correctly transform it to "@ai-sdk/google-vertex/anthropic" for proper variant generation + const provider = { + id: "google-vertex-anthropic", + npm: "@ai-sdk/google-vertex", + api: "https://vertexai.googleapis.com", + } + const modelData = { + id: "claude-opus-4-5@20251101", + name: "Claude Opus 4.5", + family: "claude-opus", + reasoning: true, + attachment: true, + tool_call: true, + temperature: true, + limit: { context: 200000, output: 64000 }, + } + + const model = Provider.fromModelsDevModel(provider as any, modelData as any) + + expect(model.api.npm).toBe("@ai-sdk/google-vertex/anthropic") + expect(model.providerID).toBe("google-vertex-anthropic") +}) + +test("google-vertex-anthropic generates thinking variants from transformed npm package", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("GOOGLE_CLOUD_PROJECT", "test-project") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["google-vertex-anthropic"]).toBeDefined() + const model = providers["google-vertex-anthropic"].models["claude-opus-4-5@20251101"] + expect(model).toBeDefined() + expect(model.api.npm).toBe("@ai-sdk/google-vertex/anthropic") + expect(model.capabilities.reasoning).toBe(true) + expect(model.variants).toBeDefined() + expect(model.variants!["high"]).toBeDefined() + expect(model.variants!["max"]).toBeDefined() + expect(model.variants!["high"].thinking).toEqual({ + type: "enabled", + budgetTokens: 16000, + }) + expect(model.variants!["max"].thinking).toEqual({ + type: "enabled", + budgetTokens: 31999, + }) + }, + }) +}) + test("variant config merges with generated variants", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 037083d5e30..7830de91132 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -195,6 +195,42 @@ describe("ProviderTransform.maxOutputTokens", () => { expect(result).toBe(OUTPUT_TOKEN_MAX) }) }) + + describe("anthropic with thinking options - google-vertex/anthropic", () => { + test("returns 32k when budgetTokens + 32k <= modelLimit", () => { + const modelLimit = 100000 + const options = { + thinking: { + type: "enabled", + budgetTokens: 10000, + }, + } + const result = ProviderTransform.maxOutputTokens( + "@ai-sdk/google-vertex/anthropic", + options, + modelLimit, + OUTPUT_TOKEN_MAX, + ) + expect(result).toBe(OUTPUT_TOKEN_MAX) + }) + + test("returns modelLimit - budgetTokens when budgetTokens + 32k > modelLimit", () => { + const modelLimit = 50000 + const options = { + thinking: { + type: "enabled", + budgetTokens: 30000, + }, + } + const result = ProviderTransform.maxOutputTokens( + "@ai-sdk/google-vertex/anthropic", + options, + modelLimit, + OUTPUT_TOKEN_MAX, + ) + expect(result).toBe(20000) + }) + }) }) describe("ProviderTransform.schema - gemini array items", () => { @@ -1669,6 +1705,34 @@ describe("ProviderTransform.variants", () => { }) }) + describe("@ai-sdk/google-vertex/anthropic", () => { + test("returns high and max with thinking budgetTokens", () => { + const model = createMockModel({ + id: "google-vertex-anthropic/claude-opus-4-5@20251101", + providerID: "google-vertex-anthropic", + api: { + id: "claude-opus-4-5@20251101", + url: "https://vertexai.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "enabled", + budgetTokens: 16000, + }, + }) + expect(result.max).toEqual({ + thinking: { + type: "enabled", + budgetTokens: 31999, + }, + }) + }) + }) + describe("@ai-sdk/cohere", () => { test("returns empty object", () => { const model = createMockModel({ @@ -1725,3 +1789,132 @@ describe("ProviderTransform.variants", () => { }) }) }) + +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", + }, + name: "Test Model", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", + ...overrides, + }) + + describe("anthropic providers", () => { + test("wraps options with 'anthropic' key for @ai-sdk/anthropic", () => { + const model = createMockModel({ + id: "anthropic/claude-3-5-sonnet", + providerID: "anthropic", + api: { + id: "claude-3-5-sonnet-20241022", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + }) + const options = { thinking: { type: "enabled", budgetTokens: 16000 } } + const result = ProviderTransform.providerOptions(model, options) + expect(result).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }) + }) + + test("wraps options with 'anthropic' key for @ai-sdk/google-vertex/anthropic", () => { + const model = createMockModel({ + id: "google-vertex-anthropic/claude-opus-4-5@20251101", + providerID: "google-vertex-anthropic", + api: { + id: "claude-opus-4-5@20251101", + url: "https://vertexai.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + }) + const options = { thinking: { type: "enabled", budgetTokens: 16000 } } + const result = ProviderTransform.providerOptions(model, options) + expect(result).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }) + }) + }) + + describe("google providers", () => { + test("wraps options with 'google' key for @ai-sdk/google-vertex", () => { + const model = createMockModel({ + id: "google-vertex/gemini-2.5-pro", + providerID: "google-vertex", + api: { + id: "gemini-2.5-pro", + url: "https://vertexai.googleapis.com", + npm: "@ai-sdk/google-vertex", + }, + }) + const options = { thinkingConfig: { thinkingBudget: 16000 } } + const result = ProviderTransform.providerOptions(model, options) + expect(result).toEqual({ + google: { thinkingConfig: { thinkingBudget: 16000 } }, + }) + }) + }) + + describe("openai providers", () => { + test("wraps options with 'openai' key for @ai-sdk/openai", () => { + const model = createMockModel({ + id: "openai/gpt-5", + providerID: "openai", + api: { + id: "gpt-5", + url: "https://api.openai.com", + npm: "@ai-sdk/openai", + }, + }) + const options = { reasoningEffort: "high" } + const result = ProviderTransform.providerOptions(model, options) + expect(result).toEqual({ + openai: { reasoningEffort: "high" }, + }) + }) + }) + + describe("custom providers", () => { + test("wraps options with providerID when npm package has no sdkKey mapping", () => { + const model = createMockModel({ + id: "custom/model", + providerID: "custom-provider", + api: { + id: "model", + url: "https://api.custom.com", + npm: "@ai-sdk/custom", + }, + }) + const options = { customOption: true } + const result = ProviderTransform.providerOptions(model, options) + expect(result).toEqual({ + "custom-provider": { customOption: true }, + }) + }) + }) +}) From ea3e0d1be46c3e7fea0c128b23b0174a227dcbe7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 24 Jan 2026 23:27:53 -0500 Subject: [PATCH 2/2] revert non-transform provider changes --- packages/opencode/src/provider/provider.ts | 3 +- .../opencode/test/provider/provider.test.ts | 63 ------ .../opencode/test/provider/transform.test.ts | 193 ------------------ 3 files changed, 1 insertion(+), 258 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2811825b133..fdd4ccdfb61 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -592,7 +592,7 @@ export namespace Provider { }) export type Info = z.infer - export function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { + function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const m: Model = { id: model.id, providerID: provider.id, @@ -603,7 +603,6 @@ export namespace Provider { url: provider.api!, npm: iife(() => { if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" - if (provider.id === "google-vertex-anthropic") return "@ai-sdk/google-vertex/anthropic" return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" }), }, diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index fcca38f9133..8a2009646e0 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -2013,69 +2013,6 @@ test("all variants can be disabled via config", async () => { }) }) -test("google-vertex-anthropic transforms npm package to subpath import", () => { - // This test verifies that even though models.dev returns "@ai-sdk/google-vertex" as the npm package, - // we correctly transform it to "@ai-sdk/google-vertex/anthropic" for proper variant generation - const provider = { - id: "google-vertex-anthropic", - npm: "@ai-sdk/google-vertex", - api: "https://vertexai.googleapis.com", - } - const modelData = { - id: "claude-opus-4-5@20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - reasoning: true, - attachment: true, - tool_call: true, - temperature: true, - limit: { context: 200000, output: 64000 }, - } - - const model = Provider.fromModelsDevModel(provider as any, modelData as any) - - expect(model.api.npm).toBe("@ai-sdk/google-vertex/anthropic") - expect(model.providerID).toBe("google-vertex-anthropic") -}) - -test("google-vertex-anthropic generates thinking variants from transformed npm package", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GOOGLE_CLOUD_PROJECT", "test-project") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers["google-vertex-anthropic"]).toBeDefined() - const model = providers["google-vertex-anthropic"].models["claude-opus-4-5@20251101"] - expect(model).toBeDefined() - expect(model.api.npm).toBe("@ai-sdk/google-vertex/anthropic") - expect(model.capabilities.reasoning).toBe(true) - expect(model.variants).toBeDefined() - expect(model.variants!["high"]).toBeDefined() - expect(model.variants!["max"]).toBeDefined() - expect(model.variants!["high"].thinking).toEqual({ - type: "enabled", - budgetTokens: 16000, - }) - expect(model.variants!["max"].thinking).toEqual({ - type: "enabled", - budgetTokens: 31999, - }) - }, - }) -}) - test("variant config merges with generated variants", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 7830de91132..037083d5e30 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -195,42 +195,6 @@ describe("ProviderTransform.maxOutputTokens", () => { expect(result).toBe(OUTPUT_TOKEN_MAX) }) }) - - describe("anthropic with thinking options - google-vertex/anthropic", () => { - test("returns 32k when budgetTokens + 32k <= modelLimit", () => { - const modelLimit = 100000 - const options = { - thinking: { - type: "enabled", - budgetTokens: 10000, - }, - } - const result = ProviderTransform.maxOutputTokens( - "@ai-sdk/google-vertex/anthropic", - options, - modelLimit, - OUTPUT_TOKEN_MAX, - ) - expect(result).toBe(OUTPUT_TOKEN_MAX) - }) - - test("returns modelLimit - budgetTokens when budgetTokens + 32k > modelLimit", () => { - const modelLimit = 50000 - const options = { - thinking: { - type: "enabled", - budgetTokens: 30000, - }, - } - const result = ProviderTransform.maxOutputTokens( - "@ai-sdk/google-vertex/anthropic", - options, - modelLimit, - OUTPUT_TOKEN_MAX, - ) - expect(result).toBe(20000) - }) - }) }) describe("ProviderTransform.schema - gemini array items", () => { @@ -1705,34 +1669,6 @@ describe("ProviderTransform.variants", () => { }) }) - describe("@ai-sdk/google-vertex/anthropic", () => { - test("returns high and max with thinking budgetTokens", () => { - const model = createMockModel({ - id: "google-vertex-anthropic/claude-opus-4-5@20251101", - providerID: "google-vertex-anthropic", - api: { - id: "claude-opus-4-5@20251101", - url: "https://vertexai.googleapis.com", - npm: "@ai-sdk/google-vertex/anthropic", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 16000, - }, - }) - expect(result.max).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 31999, - }, - }) - }) - }) - describe("@ai-sdk/cohere", () => { test("returns empty object", () => { const model = createMockModel({ @@ -1789,132 +1725,3 @@ describe("ProviderTransform.variants", () => { }) }) }) - -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", - }, - name: "Test Model", - capabilities: { - temperature: true, - reasoning: true, - attachment: true, - toolcall: true, - input: { text: true, audio: false, image: true, video: false, pdf: false }, - output: { text: true, audio: false, image: false, video: false, pdf: false }, - interleaved: false, - }, - cost: { - input: 0.001, - output: 0.002, - cache: { read: 0.0001, write: 0.0002 }, - }, - limit: { - context: 128000, - output: 8192, - }, - status: "active", - options: {}, - headers: {}, - release_date: "2024-01-01", - ...overrides, - }) - - describe("anthropic providers", () => { - test("wraps options with 'anthropic' key for @ai-sdk/anthropic", () => { - const model = createMockModel({ - id: "anthropic/claude-3-5-sonnet", - providerID: "anthropic", - api: { - id: "claude-3-5-sonnet-20241022", - url: "https://api.anthropic.com", - npm: "@ai-sdk/anthropic", - }, - }) - const options = { thinking: { type: "enabled", budgetTokens: 16000 } } - const result = ProviderTransform.providerOptions(model, options) - expect(result).toEqual({ - anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, - }) - }) - - test("wraps options with 'anthropic' key for @ai-sdk/google-vertex/anthropic", () => { - const model = createMockModel({ - id: "google-vertex-anthropic/claude-opus-4-5@20251101", - providerID: "google-vertex-anthropic", - api: { - id: "claude-opus-4-5@20251101", - url: "https://vertexai.googleapis.com", - npm: "@ai-sdk/google-vertex/anthropic", - }, - }) - const options = { thinking: { type: "enabled", budgetTokens: 16000 } } - const result = ProviderTransform.providerOptions(model, options) - expect(result).toEqual({ - anthropic: { thinking: { type: "enabled", budgetTokens: 16000 } }, - }) - }) - }) - - describe("google providers", () => { - test("wraps options with 'google' key for @ai-sdk/google-vertex", () => { - const model = createMockModel({ - id: "google-vertex/gemini-2.5-pro", - providerID: "google-vertex", - api: { - id: "gemini-2.5-pro", - url: "https://vertexai.googleapis.com", - npm: "@ai-sdk/google-vertex", - }, - }) - const options = { thinkingConfig: { thinkingBudget: 16000 } } - const result = ProviderTransform.providerOptions(model, options) - expect(result).toEqual({ - google: { thinkingConfig: { thinkingBudget: 16000 } }, - }) - }) - }) - - describe("openai providers", () => { - test("wraps options with 'openai' key for @ai-sdk/openai", () => { - const model = createMockModel({ - id: "openai/gpt-5", - providerID: "openai", - api: { - id: "gpt-5", - url: "https://api.openai.com", - npm: "@ai-sdk/openai", - }, - }) - const options = { reasoningEffort: "high" } - const result = ProviderTransform.providerOptions(model, options) - expect(result).toEqual({ - openai: { reasoningEffort: "high" }, - }) - }) - }) - - describe("custom providers", () => { - test("wraps options with providerID when npm package has no sdkKey mapping", () => { - const model = createMockModel({ - id: "custom/model", - providerID: "custom-provider", - api: { - id: "model", - url: "https://api.custom.com", - npm: "@ai-sdk/custom", - }, - }) - const options = { customOption: true } - const result = ProviderTransform.providerOptions(model, options) - expect(result).toEqual({ - "custom-provider": { customOption: true }, - }) - }) - }) -})