diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fdd4ccdfb6..2811825b13 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 acccbd1c09..6538848fea 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 8a2009646e..fcca38f913 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 037083d5e3..7830de9113 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 }, + }) + }) + }) +})