diff --git a/packages/types/src/providers/gemini.ts b/packages/types/src/providers/gemini.ts index a7225c7330..70fcc2cee3 100644 --- a/packages/types/src/providers/gemini.ts +++ b/packages/types/src/providers/gemini.ts @@ -6,25 +6,6 @@ export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001" export const geminiModels = { - "gemini-2.5-flash-preview-04-17:thinking": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.15, - outputPrice: 3.5, - maxThinkingTokens: 24_576, - supportsReasoningBudget: true, - requiredReasoningBudget: true, - }, - "gemini-2.5-flash-preview-04-17": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.15, - outputPrice: 0.6, - }, "gemini-2.5-flash-preview-05-20:thinking": { maxTokens: 65_535, contextWindow: 1_048_576, @@ -60,16 +41,8 @@ export const geminiModels = { maxThinkingTokens: 24_576, supportsReasoningBudget: true, }, - "gemini-2.5-pro-exp-03-25": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.5-pro-preview-03-25": { - maxTokens: 65_535, + "gemini-2.5-pro": { + maxTokens: 64_000, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, @@ -77,6 +50,9 @@ export const geminiModels = { outputPrice: 15, cacheReadsPrice: 0.625, cacheWritesPrice: 4.5, + maxThinkingTokens: 32_768, + supportsReasoningBudget: true, + requiredReasoningBudget: true, tiers: [ { contextWindow: 200_000, @@ -92,7 +68,48 @@ export const geminiModels = { }, ], }, - "gemini-2.5-pro-preview-05-06": { + "gemini-2.0-flash-001": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.1, + outputPrice: 0.4, + cacheReadsPrice: 0.025, + cacheWritesPrice: 1.0, + }, + "gemini-2.0-flash-lite-preview-02-05": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-flash-lite-preview-06-17": { + maxTokens: 64_000, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.1, + outputPrice: 0.4, + cacheReadsPrice: 0.025, + cacheWritesPrice: 1.0, + supportsReasoningBudget: true, + maxThinkingTokens: 24_576, + }, +} as const satisfies Record + +export const legacyGeminiModels = { + "gemini-2.5-pro-exp-03-25": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-pro-preview-03-25": { maxTokens: 65_535, contextWindow: 1_048_576, supportsImages: true, @@ -116,7 +133,7 @@ export const geminiModels = { }, ], }, - "gemini-2.5-pro-preview-06-05": { + "gemini-2.5-pro-preview-05-06": { maxTokens: 65_535, contextWindow: 1_048_576, supportsImages: true, @@ -125,8 +142,6 @@ export const geminiModels = { outputPrice: 15, cacheReadsPrice: 0.625, cacheWritesPrice: 4.5, - maxThinkingTokens: 32_768, - supportsReasoningBudget: true, tiers: [ { contextWindow: 200_000, @@ -142,8 +157,8 @@ export const geminiModels = { }, ], }, - "gemini-2.5-pro": { - maxTokens: 64_000, + "gemini-2.5-pro-preview-06-05": { + maxTokens: 65_535, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, @@ -153,7 +168,6 @@ export const geminiModels = { cacheWritesPrice: 4.5, maxThinkingTokens: 32_768, supportsReasoningBudget: true, - requiredReasoningBudget: true, tiers: [ { contextWindow: 200_000, @@ -169,56 +183,6 @@ export const geminiModels = { }, ], }, - "gemini-2.0-flash-001": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 0.1, - outputPrice: 0.4, - cacheReadsPrice: 0.025, - cacheWritesPrice: 1.0, - }, - "gemini-2.0-flash-lite-preview-02-05": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.0-pro-exp-02-05": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.0-flash-thinking-exp-01-21": { - maxTokens: 65_536, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.0-flash-thinking-exp-1219": { - maxTokens: 8192, - contextWindow: 32_767, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.0-flash-exp": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, "gemini-1.5-flash-002": { maxTokens: 8192, contextWindow: 1_048_576, @@ -283,16 +247,105 @@ export const geminiModels = { inputPrice: 0, outputPrice: 0, }, - "gemini-2.5-flash-lite-preview-06-17": { - maxTokens: 64_000, + "gemini-2.0-pro-exp-02-05": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.0-flash-thinking-exp-1219": { + maxTokens: 8192, + contextWindow: 32_767, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.0-flash-thinking-exp-01-21": { + maxTokens: 65_536, contextWindow: 1_048_576, supportsImages: true, - supportsPromptCache: true, - inputPrice: 0.1, - outputPrice: 0.4, - cacheReadsPrice: 0.025, - cacheWritesPrice: 1.0, - supportsReasoningBudget: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-flash-preview-04-17:thinking": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 3.5, maxThinkingTokens: 24_576, + supportsReasoningBudget: true, + requiredReasoningBudget: true, + }, + "gemini-2.5-flash-preview-04-17": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.6, + }, + "gemini-2.0-flash-exp": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, }, } as const satisfies Record + +/** + * Maps legacy Gemini model IDs to current supported models + */ +export function mapLegacyGeminiModel(modelId: string): GeminiModelId { + if (modelId in geminiModels) { + return modelId as GeminiModelId + } + + if (modelId in legacyGeminiModels) { + if (modelId.startsWith("gemini-2.5-pro-preview-")) { + return "gemini-2.5-pro" + } + + if (modelId.startsWith("gemini-1.5-pro-")) { + return geminiDefaultModelId + } + + if (modelId.startsWith("gemini-1.5-flash-")) { + return geminiDefaultModelId + } + + if (modelId.startsWith("gemini-2.5-pro-exp-")) { + return "gemini-2.5-pro" + } + + if (modelId === "gemini-exp-1206") { + return geminiDefaultModelId + } + + if (modelId === "gemini-2.0-pro-exp-02-05") { + return "gemini-2.5-pro" + } + + if ( + modelId === "gemini-2.0-flash-thinking-exp-1219" || + modelId === "gemini-2.0-flash-thinking-exp-01-21" || + modelId === "gemini-2.5-flash-preview-04-17" || + modelId === "gemini-2.5-flash-preview-04-17:thinking" + ) { + return "gemini-2.5-flash-preview-05-20" + } + + if (modelId === "gemini-2.0-flash-exp") { + return geminiDefaultModelId + } + } + + return geminiDefaultModelId +} diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index ee8a56ae2c..cf2af9f8ee 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -37,51 +37,6 @@ export const vertexModels = { maxThinkingTokens: 24_576, supportsReasoningBudget: true, }, - "gemini-2.5-flash-preview-04-17:thinking": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.15, - outputPrice: 3.5, - maxThinkingTokens: 24_576, - supportsReasoningBudget: true, - requiredReasoningBudget: true, - }, - "gemini-2.5-flash-preview-04-17": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.15, - outputPrice: 0.6, - }, - "gemini-2.5-pro-preview-03-25": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, - }, - "gemini-2.5-pro-preview-05-06": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, - }, - "gemini-2.5-pro-preview-06-05": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, - maxThinkingTokens: 32_768, - supportsReasoningBudget: true, - }, "gemini-2.5-pro": { maxTokens: 64_000, contextWindow: 1_048_576, @@ -107,22 +62,6 @@ export const vertexModels = { }, ], }, - "gemini-2.5-pro-exp-03-25": { - maxTokens: 65_535, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.0-pro-exp-02-05": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, "gemini-2.0-flash-001": { maxTokens: 8192, contextWindow: 1_048_576, @@ -139,30 +78,6 @@ export const vertexModels = { inputPrice: 0.075, outputPrice: 0.3, }, - "gemini-2.0-flash-thinking-exp-01-21": { - maxTokens: 8192, - contextWindow: 32_768, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-1.5-flash-002": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 0.075, - outputPrice: 0.3, - }, - "gemini-1.5-pro-002": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 1.25, - outputPrice: 5, - }, "claude-sonnet-4@20250514": { maxTokens: 8192, contextWindow: 200_000, @@ -330,3 +245,133 @@ export const VERTEX_REGIONS = [ { value: "me-central1", label: "me-central1" }, { value: "africa-south1", label: "africa-south1" }, ] + +export const legacyVertexModels = { + "gemini-1.5-flash-002": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.075, + outputPrice: 0.3, + }, + "gemini-1.5-pro-002": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.25, + outputPrice: 5, + }, + "gemini-2.5-pro-exp-03-25": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-pro-preview-03-25": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.5, + outputPrice: 15, + }, + "gemini-2.5-pro-preview-05-06": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.5, + outputPrice: 15, + }, + "gemini-2.5-pro-preview-06-05": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.5, + outputPrice: 15, + maxThinkingTokens: 32_768, + supportsReasoningBudget: true, + }, + "gemini-2.0-pro-exp-02-05": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.0-flash-thinking-exp-01-21": { + maxTokens: 8192, + contextWindow: 32_768, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-flash-preview-04-17:thinking": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 3.5, + maxThinkingTokens: 24_576, + supportsReasoningBudget: true, + requiredReasoningBudget: true, + }, + "gemini-2.5-flash-preview-04-17": { + maxTokens: 65_535, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.6, + }, +} as const satisfies Record + +/** + * Maps legacy Vertex model IDs to current supported models + */ +export function mapLegacyVertexModel(modelId: string): VertexModelId { + if (modelId in vertexModels) { + return modelId as VertexModelId + } + + if (modelId in legacyVertexModels) { + if (modelId.startsWith("gemini-2.5-pro-preview-")) { + return "gemini-2.5-pro" + } + + if (modelId.startsWith("gemini-1.5-pro-")) { + return "gemini-2.0-flash-001" + } + + if (modelId.startsWith("gemini-1.5-flash-")) { + return "gemini-2.0-flash-001" + } + + if (modelId.startsWith("gemini-2.5-pro-exp-")) { + return "gemini-2.5-pro" + } + + if (modelId === "gemini-2.0-pro-exp-02-05") { + return "gemini-2.5-pro" + } + + if ( + modelId === "gemini-2.0-flash-thinking-exp-1219" || + modelId === "gemini-2.0-flash-thinking-exp-01-21" || + modelId === "gemini-2.5-flash-preview-04-17" || + modelId === "gemini-2.5-flash-preview-04-17:thinking" + ) { + return "gemini-2.5-flash-preview-05-20" + } + } + + return vertexDefaultModelId +} diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index 812c1ae1a6..743581276f 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -7,7 +7,7 @@ import { type ModelInfo, geminiDefaultModelId } from "@roo-code/types" import { t } from "i18next" import { GeminiHandler } from "../gemini" -const GEMINI_20_FLASH_THINKING_NAME = "gemini-2.0-flash-thinking-exp-1219" +const GEMINI_25_FLASH_PREVIEW_05_20_NAME = "gemini-2.5-flash-preview-05-20" describe("GeminiHandler", () => { let handler: GeminiHandler @@ -20,7 +20,7 @@ describe("GeminiHandler", () => { handler = new GeminiHandler({ apiKey: "test-key", - apiModelId: GEMINI_20_FLASH_THINKING_NAME, + apiModelId: GEMINI_25_FLASH_PREVIEW_05_20_NAME, geminiApiKey: "test-key", }) @@ -37,7 +37,7 @@ describe("GeminiHandler", () => { describe("constructor", () => { it("should initialize with provided config", () => { expect(handler["options"].geminiApiKey).toBe("test-key") - expect(handler["options"].apiModelId).toBe(GEMINI_20_FLASH_THINKING_NAME) + expect(handler["options"].apiModelId).toBe(GEMINI_25_FLASH_PREVIEW_05_20_NAME) }) }) @@ -76,12 +76,19 @@ describe("GeminiHandler", () => { expect(chunks.length).toBe(3) expect(chunks[0]).toEqual({ type: "text", text: "Hello" }) expect(chunks[1]).toEqual({ type: "text", text: " world!" }) - expect(chunks[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 5 }) + expect(chunks[2]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: undefined, + reasoningTokens: undefined, + totalCost: expect.any(Number), + }) // Verify the call to generateContentStream expect(handler["client"].models.generateContentStream).toHaveBeenCalledWith( expect.objectContaining({ - model: GEMINI_20_FLASH_THINKING_NAME, + model: GEMINI_25_FLASH_PREVIEW_05_20_NAME, config: expect.objectContaining({ temperature: 0, systemInstruction: systemPrompt, @@ -116,7 +123,7 @@ describe("GeminiHandler", () => { // Verify the call to generateContent expect(handler["client"].models.generateContent).toHaveBeenCalledWith({ - model: GEMINI_20_FLASH_THINKING_NAME, + model: GEMINI_25_FLASH_PREVIEW_05_20_NAME, contents: [{ role: "user", parts: [{ text: "Test prompt" }] }], config: { httpOptions: undefined, @@ -148,10 +155,10 @@ describe("GeminiHandler", () => { describe("getModel", () => { it("should return correct model info", () => { const modelInfo = handler.getModel() - expect(modelInfo.id).toBe(GEMINI_20_FLASH_THINKING_NAME) + expect(modelInfo.id).toBe(GEMINI_25_FLASH_PREVIEW_05_20_NAME) expect(modelInfo.info).toBeDefined() - expect(modelInfo.info.maxTokens).toBe(8192) - expect(modelInfo.info.contextWindow).toBe(32_767) + expect(modelInfo.info.maxTokens).toBe(65_535) + expect(modelInfo.info.contextWindow).toBe(1_048_576) }) it("should return default model if invalid model specified", () => { @@ -164,6 +171,116 @@ describe("GeminiHandler", () => { }) }) + describe("legacy model migration", () => { + it("should map gemini-2.5-pro-preview-{dates} to gemini-2.5-pro", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.5-pro-preview-03-25", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should map gemini-1.5-pro-{variants} to gemini-2.0-flash-001", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-1.5-pro-002", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.0-flash-001") + }) + + it("should map gemini-1.5-flash-{variants} to gemini-2.0-flash-001", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-1.5-flash-002", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.0-flash-001") + }) + + it("should map experimental gemini-2.5-pro-exp-03-25 to gemini-2.5-pro", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.5-pro-exp-03-25", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should map gemini-exp-1206 to gemini-2.0-flash-001", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-exp-1206", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.0-flash-001") + }) + + it("should map gemini-2.0-pro-exp-02-05 to gemini-2.5-pro", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.0-pro-exp-02-05", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should map gemini-2.0-flash-thinking-exp-1219 to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.0-flash-thinking-exp-1219", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should map gemini-2.0-flash-thinking-exp-01-21 to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.0-flash-thinking-exp-01-21", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should map gemini-2.5-flash-preview-04-17 to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.5-flash-preview-04-17", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should map gemini-2.0-flash-exp to gemini-2.0-flash-001", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.0-flash-exp", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.0-flash-001") + }) + + it("should map gemini-2.5-flash-preview-04-17:thinking to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new GeminiHandler({ + apiModelId: "gemini-2.5-flash-preview-04-17:thinking", + geminiApiKey: "test-key", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should keep current models as-is", () => { + const currentHandler = new GeminiHandler({ + apiModelId: "gemini-2.5-pro", + geminiApiKey: "test-key", + }) + const modelInfo = currentHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + }) + describe("calculateCost", () => { // Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens) // Removed 'id' and 'name' as they are not part of ModelInfo type directly diff --git a/src/api/providers/__tests__/vertex.spec.ts b/src/api/providers/__tests__/vertex.spec.ts index d147e79ba8..5aa76ff0f7 100644 --- a/src/api/providers/__tests__/vertex.spec.ts +++ b/src/api/providers/__tests__/vertex.spec.ts @@ -138,4 +138,106 @@ describe("VertexHandler", () => { expect(modelInfo.info.contextWindow).toBe(1048576) }) }) + + describe("legacy model migration", () => { + it("should map gemini-2.5-pro-preview-{dates} to gemini-2.5-pro", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-2.5-pro-preview-03-25", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should map gemini-1.5-pro-{variants} to gemini-2.0-flash-001", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-1.5-pro-002", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.0-flash-001") + }) + + it("should map gemini-1.5-flash-{variants} to gemini-2.0-flash-001", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-1.5-flash-002", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.0-flash-001") + }) + + it("should map experimental gemini-2.5-pro-exp-03-25 to gemini-2.5-pro", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-2.5-pro-exp-03-25", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should keep current vertex models as-is", () => { + const currentHandler = new VertexHandler({ + apiModelId: "gemini-2.5-pro", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = currentHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should map gemini-2.0-pro-exp-02-05 to gemini-2.5-pro", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-2.0-pro-exp-02-05", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-pro") + }) + + it("should map gemini-2.0-flash-thinking-exp-01-21 to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-2.0-flash-thinking-exp-01-21", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should map gemini-2.5-flash-preview-04-17 to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-2.5-flash-preview-04-17", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should map gemini-2.5-flash-preview-04-17:thinking to gemini-2.5-flash-preview-05-20", () => { + const legacyHandler = new VertexHandler({ + apiModelId: "gemini-2.5-flash-preview-04-17:thinking", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = legacyHandler.getModel() + expect(modelInfo.id).toBe("gemini-2.5-flash-preview-05-20") + }) + + it("should keep claude models as-is", () => { + const claudeHandler = new VertexHandler({ + apiModelId: "claude-sonnet-4@20250514", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = claudeHandler.getModel() + expect(modelInfo.id).toBe("claude-sonnet-4@20250514") + }) + }) }) diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 5e547edbdc..64b7323b68 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -8,7 +8,7 @@ import { } from "@google/genai" import type { JWTInput } from "google-auth-library" -import { type ModelInfo, type GeminiModelId, geminiDefaultModelId, geminiModels } from "@roo-code/types" +import { type ModelInfo, geminiDefaultModelId, geminiModels, mapLegacyGeminiModel } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" import { safeJsonParse } from "../../shared/safeJsonParse" @@ -164,8 +164,14 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl override getModel() { const modelId = this.options.apiModelId - let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId + let id = modelId ? mapLegacyGeminiModel(modelId) : geminiDefaultModelId + + if (modelId && modelId !== id) { + this.options.apiModelId = id + } + let info: ModelInfo = geminiModels[id] + const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) // The `:thinking` suffix indicates that the model is a "Hybrid" diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts index 2c077d97b7..dee7f8c062 100644 --- a/src/api/providers/vertex.ts +++ b/src/api/providers/vertex.ts @@ -1,4 +1,4 @@ -import { type ModelInfo, type VertexModelId, vertexDefaultModelId, vertexModels } from "@roo-code/types" +import { type ModelInfo, vertexDefaultModelId, vertexModels, mapLegacyVertexModel } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -14,7 +14,12 @@ export class VertexHandler extends GeminiHandler implements SingleCompletionHand override getModel() { const modelId = this.options.apiModelId - let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId + let id = modelId ? mapLegacyVertexModel(modelId) : vertexDefaultModelId + + if (modelId && id && modelId !== id) { + this.options.apiModelId = id + } + const info: ModelInfo = vertexModels[id] const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 2738b82632..242d007d7b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -26,6 +26,7 @@ import { } from "lucide-react" import type { ProviderSettings, ExperimentId } from "@roo-code/types" +import { mapLegacyGeminiModel, mapLegacyVertexModel } from "@roo-code/types" import { TelemetrySetting } from "@roo/TelemetrySetting" @@ -218,6 +219,38 @@ const SettingsView = forwardRef(({ onDone, t }) }, []) + const handleLegacyGeminiModelMapping = useCallback( + ( + field: K, + previousValue: ProviderSettings[K], + value: ProviderSettings[K], + provider: string | undefined, + updatedApiConfiguration: ProviderSettings, + ): boolean => { + if (field !== "apiModelId" || typeof previousValue !== "string" || typeof value !== "string") { + return false + } + + let isLegacyMapping = false + if (provider === "gemini") { + isLegacyMapping = mapLegacyGeminiModel(previousValue) === value + } else if (provider === "vertex") { + isLegacyMapping = mapLegacyVertexModel(previousValue) === value + } + + if (isLegacyMapping && currentApiConfigName) { + vscode.postMessage({ + type: "upsertApiConfiguration", + text: currentApiConfigName, + apiConfiguration: updatedApiConfiguration, + }) + } + + return isLegacyMapping + }, + [currentApiConfigName], + ) + const setApiConfigurationField = useCallback( (field: K, value: ProviderSettings[K], isUserAction: boolean = true) => { setCachedState((prevState) => { @@ -231,13 +264,24 @@ const SettingsView = forwardRef(({ onDone, t // This prevents the dirty state when the component initializes and auto-syncs values const isInitialSync = !isUserAction && previousValue === undefined && value !== undefined - if (!isInitialSync) { + const updatedApiConfiguration = { ...prevState.apiConfiguration, [field]: value } + + const isLegacyMapping = handleLegacyGeminiModelMapping( + field, + previousValue, + value, + prevState.apiConfiguration?.apiProvider, + updatedApiConfiguration, + ) + + if (!isInitialSync && !isLegacyMapping) { setChangeDetected(true) } - return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } } + + return { ...prevState, apiConfiguration: updatedApiConfiguration } }) }, - [], + [handleLegacyGeminiModelMapping], ) const setExperimentEnabled: SetExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => { diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 75a4a968ad..24b0e72f6c 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -40,6 +40,8 @@ import { sambaNovaDefaultModelId, doubaoModels, doubaoDefaultModelId, + mapLegacyGeminiModel, + mapLegacyVertexModel, internationalZAiDefaultModelId, mainlandZAiDefaultModelId, internationalZAiModels, @@ -202,12 +204,14 @@ function getSelectedModel({ return { id, info: baseInfo } } case "vertex": { - const id = apiConfiguration.apiModelId ?? vertexDefaultModelId + const rawId = apiConfiguration.apiModelId ?? vertexDefaultModelId + const id = mapLegacyVertexModel(rawId) const info = vertexModels[id as keyof typeof vertexModels] return { id, info } } case "gemini": { - const id = apiConfiguration.apiModelId ?? geminiDefaultModelId + const rawId = apiConfiguration.apiModelId ?? geminiDefaultModelId + const id = mapLegacyGeminiModel(rawId) const info = geminiModels[id as keyof typeof geminiModels] return { id, info } }