|
3 | 3 | import { Anthropic } from "@anthropic-ai/sdk" |
4 | 4 |
|
5 | 5 | import { GeminiHandler } from "../gemini" |
6 | | -import { geminiDefaultModelId } from "../../../shared/api" |
| 6 | +import { geminiDefaultModelId, type ModelInfo } from "../../../shared/api" |
7 | 7 |
|
8 | 8 | const GEMINI_20_FLASH_THINKING_NAME = "gemini-2.0-flash-thinking-exp-1219" |
9 | 9 |
|
@@ -168,4 +168,89 @@ describe("GeminiHandler", () => { |
168 | 168 | expect(modelInfo.id).toBe(geminiDefaultModelId) // Default model |
169 | 169 | }) |
170 | 170 | }) |
| 171 | + |
| 172 | + describe("calculateCost", () => { |
| 173 | + // Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens) |
| 174 | + // Removed 'id' and 'name' as they are not part of ModelInfo type directly |
| 175 | + const mockInfo: ModelInfo = { |
| 176 | + inputPrice: 0.125, // $/1M tokens |
| 177 | + outputPrice: 0.375, // $/1M tokens |
| 178 | + cacheWritesPrice: 0.125, // Assume same as input for test |
| 179 | + cacheReadsPrice: 0.125 * 0.25, // Assume 0.25x input for test |
| 180 | + contextWindow: 1_000_000, |
| 181 | + maxTokens: 8192, |
| 182 | + supportsPromptCache: true, // Enable cache calculations for tests |
| 183 | + } |
| 184 | + |
| 185 | + it("should calculate cost correctly based on input and output tokens", () => { |
| 186 | + const inputTokens = 10000 // Use larger numbers for per-million pricing |
| 187 | + const outputTokens = 20000 |
| 188 | + // Added non-null assertions (!) as mockInfo guarantees these values |
| 189 | + const expectedCost = |
| 190 | + (inputTokens / 1_000_000) * mockInfo.inputPrice! + (outputTokens / 1_000_000) * mockInfo.outputPrice! |
| 191 | + |
| 192 | + const cost = handler.calculateCost({ info: mockInfo, inputTokens, outputTokens }) |
| 193 | + expect(cost).toBeCloseTo(expectedCost) |
| 194 | + }) |
| 195 | + |
| 196 | + it("should return 0 if token counts are zero", () => { |
| 197 | + // Note: The method expects numbers, not undefined. Passing undefined would be a type error. |
| 198 | + // The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist. |
| 199 | + expect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0) |
| 200 | + }) |
| 201 | + |
| 202 | + it("should handle only input tokens", () => { |
| 203 | + const inputTokens = 5000 |
| 204 | + // Added non-null assertion (!) |
| 205 | + const expectedCost = (inputTokens / 1_000_000) * mockInfo.inputPrice! |
| 206 | + expect(handler.calculateCost({ info: mockInfo, inputTokens, outputTokens: 0 })).toBeCloseTo(expectedCost) |
| 207 | + }) |
| 208 | + |
| 209 | + it("should handle only output tokens", () => { |
| 210 | + const outputTokens = 15000 |
| 211 | + // Added non-null assertion (!) |
| 212 | + const expectedCost = (outputTokens / 1_000_000) * mockInfo.outputPrice! |
| 213 | + expect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens })).toBeCloseTo(expectedCost) |
| 214 | + }) |
| 215 | + |
| 216 | + it("should calculate cost with cache write tokens", () => { |
| 217 | + const inputTokens = 10000 |
| 218 | + const outputTokens = 20000 |
| 219 | + const cacheWriteTokens = 5000 |
| 220 | + const CACHE_TTL = 5 // Match the constant in gemini.ts |
| 221 | + |
| 222 | + // Added non-null assertions (!) |
| 223 | + const expectedInputCost = (inputTokens / 1_000_000) * mockInfo.inputPrice! |
| 224 | + const expectedOutputCost = (outputTokens / 1_000_000) * mockInfo.outputPrice! |
| 225 | + const expectedCacheWriteCost = |
| 226 | + mockInfo.cacheWritesPrice! * (cacheWriteTokens / 1_000_000) * (CACHE_TTL / 60) |
| 227 | + const expectedCost = expectedInputCost + expectedOutputCost + expectedCacheWriteCost |
| 228 | + |
| 229 | + const cost = handler.calculateCost({ info: mockInfo, inputTokens, outputTokens, cacheWriteTokens }) |
| 230 | + expect(cost).toBeCloseTo(expectedCost) |
| 231 | + }) |
| 232 | + |
| 233 | + it("should calculate cost with cache read tokens", () => { |
| 234 | + const inputTokens = 10000 // Total logical input |
| 235 | + const outputTokens = 20000 |
| 236 | + const cacheReadTokens = 8000 // Part of inputTokens read from cache |
| 237 | + |
| 238 | + const uncachedReadTokens = inputTokens - cacheReadTokens |
| 239 | + // Added non-null assertions (!) |
| 240 | + const expectedInputCost = (uncachedReadTokens / 1_000_000) * mockInfo.inputPrice! |
| 241 | + const expectedOutputCost = (outputTokens / 1_000_000) * mockInfo.outputPrice! |
| 242 | + const expectedCacheReadCost = mockInfo.cacheReadsPrice! * (cacheReadTokens / 1_000_000) |
| 243 | + const expectedCost = expectedInputCost + expectedOutputCost + expectedCacheReadCost |
| 244 | + |
| 245 | + const cost = handler.calculateCost({ info: mockInfo, inputTokens, outputTokens, cacheReadTokens }) |
| 246 | + expect(cost).toBeCloseTo(expectedCost) |
| 247 | + }) |
| 248 | + |
| 249 | + it("should return undefined if pricing info is missing", () => { |
| 250 | + // Create a copy and explicitly set a price to undefined |
| 251 | + const incompleteInfo: ModelInfo = { ...mockInfo, outputPrice: undefined } |
| 252 | + const cost = handler.calculateCost({ info: incompleteInfo, inputTokens: 1000, outputTokens: 1000 }) |
| 253 | + expect(cost).toBeUndefined() |
| 254 | + }) |
| 255 | + }) |
171 | 256 | }) |
0 commit comments