diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fdd4ccdfb61..7b8ba3bd403 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1073,7 +1073,21 @@ export namespace Provider { throw new ModelNotFoundError({ providerID, modelID, suggestions }) } - const info = provider.models[modelID] + let info = provider.models[modelID] + + // Fallback for google-vertex-anthropic: if model not found and ID is unversioned, + // search for versioned models (e.g., claude-sonnet-4-5@20250929) + if (!info && providerID === "google-vertex-anthropic" && !modelID.includes("@")) { + const versionedMatches = Object.keys(provider.models) + .filter((key) => key.startsWith(modelID + "@")) + .sort() + .reverse() + + if (versionedMatches.length > 0) { + info = provider.models[versionedMatches[0]] + } + } + if (!info) { const availableModels = Object.keys(provider.models) const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 }) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8a2009646e0..4ec75c61c1a 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1584,6 +1584,99 @@ test("ModelNotFoundError for provider includes suggestions", async () => { }) }) +test("getModel resolves unversioned google-vertex-anthropic model ID to latest versioned model", 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") + Env.set("GOOGLE_CLOUD_LOCATION", "us-central1") + }, + fn: async () => { + // Test unversioned model ID resolves to latest versioned + const model = await Provider.getModel("google-vertex-anthropic", "claude-sonnet-4-5") + expect(model).toBeDefined() + expect(model.providerID).toBe("google-vertex-anthropic") + // Should resolve to versioned model + expect(model.id).toContain("claude-sonnet-4-5@") + // Verify pricing data is loaded + expect(model.cost).toBeDefined() + expect(model.cost.input).toBeGreaterThan(0) + expect(model.cost.output).toBeGreaterThan(0) + }, + }) +}) + +test("getModel backward compatibility: versioned google-vertex-anthropic model ID works directly", 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") + Env.set("GOOGLE_CLOUD_LOCATION", "us-central1") + }, + fn: async () => { + // Test that fully versioned model ID still works + const model = await Provider.getModel("google-vertex-anthropic", "claude-sonnet-4-5@20250929") + expect(model).toBeDefined() + expect(model.providerID).toBe("google-vertex-anthropic") + expect(model.id).toBe("claude-sonnet-4-5@20250929") + // Verify pricing data is loaded + expect(model.cost).toBeDefined() + expect(model.cost.input).toBeGreaterThan(0) + expect(model.cost.output).toBeGreaterThan(0) + }, + }) +}) + +test("getModel unversioned ID only works for google-vertex-anthropic, not google-vertex", 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 () => { + // google-vertex (Gemini) should NOT have fallback logic, unversioned ID should fail + try { + await Provider.getModel("google-vertex", "gemini-2-flash") + // If it finds it, that's okay (backward compat if unversioned model exists) + // If it fails, that's also okay - the fallback only applies to google-vertex-anthropic + } catch (e: any) { + // Expected: unversioned model not found in google-vertex + expect(e.constructor.name).toBe("ProviderModelNotFoundError") + } + }, + }) +}) + test("getProvider returns undefined for nonexistent provider", async () => { await using tmp = await tmpdir({ init: async (dir) => {