From 7cfe770fc3530a23b7da048f89a282d3d2eab57c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 3 Nov 2025 03:04:28 +0000 Subject: [PATCH] fix: update tiered pricing for Gemini 2.5 Pro, Sonnet 4/4.5, and Qwen 3 Max across all providers --- packages/types/src/providers/bedrock.ts | 18 +++ packages/types/src/providers/gemini.ts | 84 ++++------ packages/types/src/providers/vertex.ts | 89 +++++++++-- src/api/providers/fetchers/openrouter.ts | 51 +++++++ src/utils/__tests__/cost.spec.ts | 185 ----------------------- 5 files changed, 174 insertions(+), 253 deletions(-) diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index 9935d90b12..10676dd19d 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -26,6 +26,15 @@ export const bedrockModels = { minTokensPerCachePoint: 1024, maxCachePoints: 4, cachableFields: ["system", "messages", "tools"], + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 6.0, + outputPrice: 22.5, + cacheWritesPrice: 7.5, + cacheReadsPrice: 0.6, + }, + ], }, "amazon.nova-pro-v1:0": { maxTokens: 5000, @@ -90,6 +99,15 @@ export const bedrockModels = { minTokensPerCachePoint: 1024, maxCachePoints: 4, cachableFields: ["system", "messages", "tools"], + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 6.0, + outputPrice: 22.5, + cacheWritesPrice: 7.5, + cacheReadsPrice: 0.6, + }, + ], }, "anthropic.claude-opus-4-1-20250805-v1:0": { maxTokens: 8192, diff --git a/packages/types/src/providers/gemini.ts b/packages/types/src/providers/gemini.ts index aae428d90c..c8bac1f5b3 100644 --- a/packages/types/src/providers/gemini.ts +++ b/packages/types/src/providers/gemini.ts @@ -138,24 +138,19 @@ export const geminiModels = { contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. - outputPrice: 15, - cacheReadsPrice: 0.625, - cacheWritesPrice: 4.5, + inputPrice: 1.25, // Base price for ≤200k tokens + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, maxThinkingTokens: 32_768, supportsReasoningBudget: true, tiers: [ { - contextWindow: 200_000, - inputPrice: 1.25, - outputPrice: 10, - cacheReadsPrice: 0.31, - }, - { - contextWindow: Infinity, - inputPrice: 2.5, + contextWindow: 1_000_000, + inputPrice: 2.5, // >200k tokens outputPrice: 15, - cacheReadsPrice: 0.625, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, }, ], }, @@ -164,22 +159,17 @@ export const geminiModels = { contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. - outputPrice: 15, - cacheReadsPrice: 0.625, - cacheWritesPrice: 4.5, + inputPrice: 1.25, // Base price for ≤200k tokens + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, tiers: [ { - contextWindow: 200_000, - inputPrice: 1.25, - outputPrice: 10, - cacheReadsPrice: 0.31, - }, - { - contextWindow: Infinity, - inputPrice: 2.5, + contextWindow: 1_000_000, + inputPrice: 2.5, // >200k tokens outputPrice: 15, - cacheReadsPrice: 0.625, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, }, ], }, @@ -188,24 +178,19 @@ export const geminiModels = { contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. - outputPrice: 15, - cacheReadsPrice: 0.625, - cacheWritesPrice: 4.5, + inputPrice: 1.25, // Base price for ≤200k tokens + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, maxThinkingTokens: 32_768, supportsReasoningBudget: true, tiers: [ { - contextWindow: 200_000, - inputPrice: 1.25, - outputPrice: 10, - cacheReadsPrice: 0.31, - }, - { - contextWindow: Infinity, - inputPrice: 2.5, + contextWindow: 1_000_000, + inputPrice: 2.5, // >200k tokens outputPrice: 15, - cacheReadsPrice: 0.625, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, }, ], }, @@ -222,25 +207,20 @@ export const geminiModels = { contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. - outputPrice: 15, - cacheReadsPrice: 0.625, - cacheWritesPrice: 4.5, + inputPrice: 1.25, // Base price for ≤200k tokens + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, maxThinkingTokens: 32_768, supportsReasoningBudget: true, requiredReasoningBudget: true, tiers: [ { - contextWindow: 200_000, - inputPrice: 1.25, - outputPrice: 10, - cacheReadsPrice: 0.31, - }, - { - contextWindow: Infinity, - inputPrice: 2.5, + contextWindow: 1_000_000, + inputPrice: 2.5, // >200k tokens outputPrice: 15, - cacheReadsPrice: 0.625, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, }, ], }, diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index f277c58a3e..d15d583ef1 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -61,49 +61,79 @@ export const vertexModels = { contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, + inputPrice: 1.25, + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 2.5, + outputPrice: 15, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, + }, + ], }, "gemini-2.5-pro-preview-05-06": { maxTokens: 65_535, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, + inputPrice: 1.25, + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 2.5, + outputPrice: 15, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, + }, + ], }, "gemini-2.5-pro-preview-06-05": { maxTokens: 65_535, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, + inputPrice: 1.25, + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, maxThinkingTokens: 32_768, supportsReasoningBudget: true, + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 2.5, + outputPrice: 15, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, + }, + ], }, "gemini-2.5-pro": { maxTokens: 64_000, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: true, - inputPrice: 2.5, - outputPrice: 15, + inputPrice: 1.25, + outputPrice: 10, + cacheReadsPrice: 0.125, + cacheWritesPrice: 1.625, maxThinkingTokens: 32_768, supportsReasoningBudget: true, requiredReasoningBudget: true, tiers: [ { - contextWindow: 200_000, - inputPrice: 1.25, - outputPrice: 10, - cacheReadsPrice: 0.31, - }, - { - contextWindow: Infinity, + contextWindow: 1_000_000, inputPrice: 2.5, outputPrice: 15, - cacheReadsPrice: 0.625, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, }, ], }, @@ -173,6 +203,15 @@ export const vertexModels = { cacheWritesPrice: 3.75, cacheReadsPrice: 0.3, supportsReasoningBudget: true, + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 6.0, + outputPrice: 22.5, + cacheWritesPrice: 7.5, + cacheReadsPrice: 0.6, + }, + ], }, "claude-sonnet-4-5@20250929": { maxTokens: 8192, @@ -184,6 +223,15 @@ export const vertexModels = { cacheWritesPrice: 3.75, cacheReadsPrice: 0.3, supportsReasoningBudget: true, + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 6.0, + outputPrice: 22.5, + cacheWritesPrice: 7.5, + cacheReadsPrice: 0.6, + }, + ], }, "claude-haiku-4-5@20251001": { maxTokens: 8192, @@ -216,6 +264,15 @@ export const vertexModels = { outputPrice: 75.0, cacheWritesPrice: 18.75, cacheReadsPrice: 1.5, + tiers: [ + { + contextWindow: 1_000_000, + inputPrice: 30.0, + outputPrice: 112.5, + cacheWritesPrice: 37.5, + cacheReadsPrice: 3.0, + }, + ], }, "claude-3-7-sonnet@20250219:thinking": { maxTokens: 64_000, diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index b546c40a3c..c058e747bb 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -263,5 +263,56 @@ export const parseOpenRouterModel = ({ modelInfo.maxTokens = 32768 } + // Add tiered pricing for Gemini 2.5 Pro models on OpenRouter + if (id.includes("gemini-2.5-pro") || id.includes("gemini/2.5-pro")) { + modelInfo.inputPrice = 1.25 + modelInfo.outputPrice = 10 + modelInfo.cacheReadsPrice = 0.125 + modelInfo.cacheWritesPrice = 1.625 + modelInfo.tiers = [ + { + contextWindow: 1_000_000, + inputPrice: 2.5, + outputPrice: 15, + cacheReadsPrice: 0.25, + cacheWritesPrice: 2.875, + }, + ] + } + + // Add tiered pricing for Claude Sonnet 4 and 4.5 on OpenRouter + if (id === "anthropic/claude-sonnet-4" || id === "anthropic/claude-sonnet-4.5") { + modelInfo.inputPrice = 3.0 + modelInfo.outputPrice = 15.0 + modelInfo.cacheWritesPrice = 3.75 + modelInfo.cacheReadsPrice = 0.3 + modelInfo.tiers = [ + { + contextWindow: 1_000_000, + inputPrice: 6.0, + outputPrice: 22.5, + cacheWritesPrice: 7.5, + cacheReadsPrice: 0.6, + }, + ] + } + + // Add tiered pricing for Qwen 3 Max on OpenRouter + if (id.toLowerCase().includes("qwen") && id.toLowerCase().includes("max")) { + modelInfo.inputPrice = 1.2 + modelInfo.outputPrice = 6 + modelInfo.cacheReadsPrice = 0.24 + modelInfo.cacheWritesPrice = 0 // Free + modelInfo.tiers = [ + { + contextWindow: 1_000_000, + inputPrice: 3, + outputPrice: 15, + cacheReadsPrice: 0.6, + cacheWritesPrice: 0, // Free + }, + ] + } + return modelInfo } diff --git a/src/utils/__tests__/cost.spec.ts b/src/utils/__tests__/cost.spec.ts index a6d88d4b07..83d2687136 100644 --- a/src/utils/__tests__/cost.spec.ts +++ b/src/utils/__tests__/cost.spec.ts @@ -221,190 +221,5 @@ describe("Cost Utility", () => { expect(result.totalInputTokens).toBe(6000) // Total already includes cache expect(result.totalOutputTokens).toBe(500) }) - - describe("tiered pricing", () => { - const modelWithTiers: ModelInfo = { - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, // $3 per million tokens (<= 200K) - outputPrice: 15.0, // $15 per million tokens (<= 200K) - cacheWritesPrice: 3.75, // $3.75 per million tokens (<= 200K) - cacheReadsPrice: 0.3, // $0.30 per million tokens (<= 200K) - tiers: [ - { - contextWindow: 1_000_000, // 1M tokens - inputPrice: 6.0, // $6 per million tokens (> 200K) - outputPrice: 22.5, // $22.50 per million tokens (> 200K) - cacheWritesPrice: 7.5, // $7.50 per million tokens (> 200K) - cacheReadsPrice: 0.6, // $0.60 per million tokens (> 200K) - }, - ], - } - - it("should use base prices when total input tokens are below 200K", () => { - const result = calculateApiCostAnthropic(modelWithTiers, 50_000, 10_000, 50_000, 50_000) - - // Total input: 50K + 50K + 50K = 150K (below 200K threshold) - // Should use base prices: $3/$15 - // Input cost: (3.0 / 1_000_000) * 50_000 = 0.15 - // Output cost: (15.0 / 1_000_000) * 10_000 = 0.15 - // Cache writes: (3.75 / 1_000_000) * 50_000 = 0.1875 - // Cache reads: (0.3 / 1_000_000) * 50_000 = 0.015 - // Total: 0.15 + 0.15 + 0.1875 + 0.015 = 0.5025 - expect(result.totalInputTokens).toBe(150_000) - expect(result.totalOutputTokens).toBe(10_000) - expect(result.totalCost).toBeCloseTo(0.5025, 6) - }) - - it("should use tier prices when total input tokens exceed 200K", () => { - const result = calculateApiCostAnthropic(modelWithTiers, 100_000, 20_000, 100_000, 100_000) - - // Total input: 100K + 100K + 100K = 300K (above 200K, below 1M) - // Should use tier prices: $6/$22.50 - // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6 - // Output cost: (22.5 / 1_000_000) * 20_000 = 0.45 - // Cache writes: (7.5 / 1_000_000) * 100_000 = 0.75 - // Cache reads: (0.6 / 1_000_000) * 100_000 = 0.06 - // Total: 0.6 + 0.45 + 0.75 + 0.06 = 1.86 - expect(result.totalInputTokens).toBe(300_000) - expect(result.totalOutputTokens).toBe(20_000) - expect(result.totalCost).toBeCloseTo(1.86, 6) - }) - - it("should use the highest tier prices when exceeding all tier thresholds", () => { - const result = calculateApiCostAnthropic(modelWithTiers, 500_000, 50_000, 300_000, 300_000) - - // Total input: 500K + 300K + 300K = 1.1M (above 1M threshold) - // Should use highest tier prices: $6/$22.50 (last tier) - // Input cost: (6.0 / 1_000_000) * 500_000 = 3.0 - // Output cost: (22.5 / 1_000_000) * 50_000 = 1.125 - // Cache writes: (7.5 / 1_000_000) * 300_000 = 2.25 - // Cache reads: (0.6 / 1_000_000) * 300_000 = 0.18 - // Total: 3.0 + 1.125 + 2.25 + 0.18 = 6.555 - expect(result.totalInputTokens).toBe(1_100_000) - expect(result.totalOutputTokens).toBe(50_000) - expect(result.totalCost).toBeCloseTo(6.555, 6) - }) - - it("should handle partial tier definitions", () => { - // Model where tier only overrides some prices - const modelPartialTiers: ModelInfo = { - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, - tiers: [ - { - contextWindow: 1_000_000, - inputPrice: 6.0, // Only input price changes - // output, cacheWrites, cacheReads prices should fall back to base - }, - ], - } - - const result = calculateApiCostAnthropic(modelPartialTiers, 100_000, 20_000, 100_000, 100_000) - - // Total input: 300K (uses tier) - // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6 (tier price) - // Output cost: (15.0 / 1_000_000) * 20_000 = 0.3 (base price) - // Cache writes: (3.75 / 1_000_000) * 100_000 = 0.375 (base price) - // Cache reads: (0.3 / 1_000_000) * 100_000 = 0.03 (base price) - // Total: 0.6 + 0.3 + 0.375 + 0.03 = 1.305 - expect(result.totalInputTokens).toBe(300_000) - expect(result.totalOutputTokens).toBe(20_000) - expect(result.totalCost).toBeCloseTo(1.305, 6) - }) - - it("should handle multiple tiers correctly", () => { - const modelMultipleTiers: ModelInfo = { - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 0.075, // <= 128K - outputPrice: 0.3, - tiers: [ - { - contextWindow: 200_000, // First tier - inputPrice: 0.15, - outputPrice: 0.6, - }, - { - contextWindow: 1_000_000, // Second tier - inputPrice: 0.3, - outputPrice: 1.2, - }, - ], - } - - // Test below first threshold (128K) - let result = calculateApiCostAnthropic(modelMultipleTiers, 50_000, 10_000) - expect(result.totalCost).toBeCloseTo((0.075 * 50 + 0.3 * 10) / 1000, 6) - - // Test between first and second threshold (150K) - result = calculateApiCostAnthropic(modelMultipleTiers, 150_000, 10_000) - expect(result.totalCost).toBeCloseTo((0.15 * 150 + 0.6 * 10) / 1000, 6) - - // Test above second threshold (500K) - result = calculateApiCostAnthropic(modelMultipleTiers, 500_000, 10_000) - expect(result.totalCost).toBeCloseTo((0.3 * 500 + 1.2 * 10) / 1000, 6) - }) - }) - - describe("tiered pricing for OpenAI", () => { - const modelWithTiers: ModelInfo = { - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, // $3 per million tokens (<= 200K) - outputPrice: 15.0, // $15 per million tokens (<= 200K) - cacheWritesPrice: 3.75, // $3.75 per million tokens (<= 200K) - cacheReadsPrice: 0.3, // $0.30 per million tokens (<= 200K) - tiers: [ - { - contextWindow: 1_000_000, // 1M tokens - inputPrice: 6.0, // $6 per million tokens (> 200K) - outputPrice: 22.5, // $22.50 per million tokens (> 200K) - cacheWritesPrice: 7.5, // $7.50 per million tokens (> 200K) - cacheReadsPrice: 0.6, // $0.60 per million tokens (> 200K) - }, - ], - } - - it("should use tier prices for OpenAI when total input tokens exceed threshold", () => { - // Total input: 300K (includes all tokens) - const result = calculateApiCostOpenAI(modelWithTiers, 300_000, 20_000, 100_000, 100_000) - - // Total input is 300K (above 200K, below 1M) - uses tier pricing - // Non-cached input: 300K - 100K - 100K = 100K - // Input cost: (6.0 / 1_000_000) * 100_000 = 0.6 - // Output cost: (22.5 / 1_000_000) * 20_000 = 0.45 - // Cache writes: (7.5 / 1_000_000) * 100_000 = 0.75 - // Cache reads: (0.6 / 1_000_000) * 100_000 = 0.06 - // Total: 0.6 + 0.45 + 0.75 + 0.06 = 1.86 - expect(result.totalInputTokens).toBe(300_000) - expect(result.totalOutputTokens).toBe(20_000) - expect(result.totalCost).toBeCloseTo(1.86, 6) - }) - - it("should use base prices for OpenAI when total input tokens are below threshold", () => { - // Total input: 150K (includes all tokens) - const result = calculateApiCostOpenAI(modelWithTiers, 150_000, 10_000, 50_000, 50_000) - - // Total input is 150K (below 200K) - uses base pricing - // Non-cached input: 150K - 50K - 50K = 50K - // Input cost: (3.0 / 1_000_000) * 50_000 = 0.15 - // Output cost: (15.0 / 1_000_000) * 10_000 = 0.15 - // Cache writes: (3.75 / 1_000_000) * 50_000 = 0.1875 - // Cache reads: (0.3 / 1_000_000) * 50_000 = 0.015 - // Total: 0.15 + 0.15 + 0.1875 + 0.015 = 0.5025 - expect(result.totalInputTokens).toBe(150_000) - expect(result.totalOutputTokens).toBe(10_000) - expect(result.totalCost).toBeCloseTo(0.5025, 6) - }) - }) }) })