Skip to content

Commit 1eecf1f

Browse files
committed
fix: calculate OpenRouter costs locally to avoid API miscalculation
- Calculate costs using model pricing info instead of relying on OpenRouter API response - Fixes issue where OpenRouter returns incorrect cost values (e.g., $0.46 instead of $1.50+ for 527k tokens) - Falls back to API cost if model pricing info is unavailable - Added test case to verify fix for reported issue #8650
1 parent 6b8c21f commit 1eecf1f

File tree

2 files changed

+80
-2
lines changed

2 files changed

+80
-2
lines changed

src/api/providers/__tests__/openrouter.spec.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,15 @@ describe("OpenRouterHandler", () => {
157157
// Verify stream chunks
158158
expect(chunks).toHaveLength(2) // One text chunk and one usage chunk
159159
expect(chunks[0]).toEqual({ type: "text", text: "test response" })
160-
expect(chunks[1]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20, totalCost: 0.001 })
160+
// Cost is now calculated locally: (3/1M * 10) + (15/1M * 20) = 0.00003 + 0.0003 = 0.00033
161+
expect(chunks[1]).toEqual({
162+
type: "usage",
163+
inputTokens: 10,
164+
outputTokens: 20,
165+
cacheReadTokens: undefined,
166+
reasoningTokens: undefined,
167+
totalCost: expect.closeTo(0.00033, 5),
168+
})
161169

162170
// Verify OpenAI client was called with correct parameters.
163171
expect(mockCreate).toHaveBeenCalledWith(
@@ -267,6 +275,57 @@ describe("OpenRouterHandler", () => {
267275
const generator = handler.createMessage("test", [])
268276
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error")
269277
})
278+
279+
it("calculates cost locally when OpenRouter API returns incorrect cost (issue #8650)", async () => {
280+
const handler = new OpenRouterHandler({
281+
...mockOptions,
282+
openRouterModelId: "anthropic/claude-3.5-sonnet", // Use Claude 3.5 Sonnet as in the issue
283+
})
284+
285+
const mockStream = {
286+
async *[Symbol.asyncIterator]() {
287+
yield {
288+
id: "test-id",
289+
choices: [{ delta: { content: "test" } }],
290+
}
291+
// Simulate the issue: OpenRouter returns incorrect cost ($0.46) for 527k input tokens
292+
// Actual cost should be: (527000 * 3 / 1M) + (7700 * 15 / 1M) = 1.581 + 0.1155 = 1.6965
293+
yield {
294+
id: "test-id",
295+
choices: [{ delta: {} }],
296+
usage: {
297+
prompt_tokens: 527000,
298+
completion_tokens: 7700,
299+
cost: 0.46, // OpenRouter's incorrect cost value
300+
},
301+
}
302+
},
303+
}
304+
305+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
306+
;(OpenAI as any).prototype.chat = {
307+
completions: { create: mockCreate },
308+
} as any
309+
310+
const generator = handler.createMessage("test", [])
311+
const chunks = []
312+
313+
for await (const chunk of generator) {
314+
chunks.push(chunk)
315+
}
316+
317+
// Verify that we calculate the correct cost locally
318+
// Model pricing: inputPrice: 3, outputPrice: 15
319+
// Cost = (527000 / 1M * 3) + (7700 / 1M * 15) = 1.581 + 0.1155 = 1.6965
320+
expect(chunks[1]).toEqual({
321+
type: "usage",
322+
inputTokens: 527000,
323+
outputTokens: 7700,
324+
cacheReadTokens: undefined,
325+
reasoningTokens: undefined,
326+
totalCost: expect.closeTo(1.6965, 5),
327+
})
328+
})
270329
})
271330

272331
describe("completePrompt", () => {

src/api/providers/openrouter.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "@roo-code/types"
1111

1212
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
13+
import { calculateApiCostOpenAI } from "../../shared/cost"
1314

1415
import { convertToOpenAiMessages } from "../transform/openai-format"
1516
import { ApiStreamChunk } from "../transform/stream"
@@ -196,13 +197,31 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
196197
}
197198

198199
if (lastUsage) {
200+
// Get model info to calculate cost locally
201+
const modelInfo = this.getModel().info
202+
203+
// Calculate cost locally using model pricing information
204+
// OpenRouter uses OpenAI-style token counting (input tokens include cached tokens)
205+
const localCost = calculateApiCostOpenAI(
206+
modelInfo,
207+
lastUsage.prompt_tokens || 0,
208+
lastUsage.completion_tokens || 0,
209+
undefined, // cache creation tokens - OpenRouter doesn't distinguish this
210+
lastUsage.prompt_tokens_details?.cached_tokens,
211+
)
212+
213+
// Use locally calculated cost, but fall back to API response if our calculation fails
214+
// or if model pricing info is not available
215+
const apiCost = (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0)
216+
const totalCost = modelInfo.inputPrice && modelInfo.outputPrice ? localCost : apiCost
217+
199218
yield {
200219
type: "usage",
201220
inputTokens: lastUsage.prompt_tokens || 0,
202221
outputTokens: lastUsage.completion_tokens || 0,
203222
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
204223
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
205-
totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0),
224+
totalCost,
206225
}
207226
}
208227
}

0 commit comments

Comments
 (0)