mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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
This commit is contained in:
parent
6b8c21f873
commit
1eecf1fb43
2 changed files with 80 additions and 2 deletions
|
|
@ -157,7 +157,15 @@ describe("OpenRouterHandler", () => {
|
|||
// Verify stream chunks
|
||||
expect(chunks).toHaveLength(2) // One text chunk and one usage chunk
|
||||
expect(chunks[0]).toEqual({ type: "text", text: "test response" })
|
||||
expect(chunks[1]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20, totalCost: 0.001 })
|
||||
// Cost is now calculated locally: (3/1M * 10) + (15/1M * 20) = 0.00003 + 0.0003 = 0.00033
|
||||
expect(chunks[1]).toEqual({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
cacheReadTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
totalCost: expect.closeTo(0.00033, 5),
|
||||
})
|
||||
|
||||
// Verify OpenAI client was called with correct parameters.
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
|
|
@ -267,6 +275,57 @@ describe("OpenRouterHandler", () => {
|
|||
const generator = handler.createMessage("test", [])
|
||||
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error")
|
||||
})
|
||||
|
||||
it("calculates cost locally when OpenRouter API returns incorrect cost (issue #8650)", async () => {
|
||||
const handler = new OpenRouterHandler({
|
||||
...mockOptions,
|
||||
openRouterModelId: "anthropic/claude-3.5-sonnet", // Use Claude 3.5 Sonnet as in the issue
|
||||
})
|
||||
|
||||
const mockStream = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [{ delta: { content: "test" } }],
|
||||
}
|
||||
// Simulate the issue: OpenRouter returns incorrect cost ($0.46) for 527k input tokens
|
||||
// Actual cost should be: (527000 * 3 / 1M) + (7700 * 15 / 1M) = 1.581 + 0.1155 = 1.6965
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [{ delta: {} }],
|
||||
usage: {
|
||||
prompt_tokens: 527000,
|
||||
completion_tokens: 7700,
|
||||
cost: 0.46, // OpenRouter's incorrect cost value
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
|
||||
;(OpenAI as any).prototype.chat = {
|
||||
completions: { create: mockCreate },
|
||||
} as any
|
||||
|
||||
const generator = handler.createMessage("test", [])
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of generator) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Verify that we calculate the correct cost locally
|
||||
// Model pricing: inputPrice: 3, outputPrice: 15
|
||||
// Cost = (527000 / 1M * 3) + (7700 / 1M * 15) = 1.581 + 0.1155 = 1.6965
|
||||
expect(chunks[1]).toEqual({
|
||||
type: "usage",
|
||||
inputTokens: 527000,
|
||||
outputTokens: 7700,
|
||||
cacheReadTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
totalCost: expect.closeTo(1.6965, 5),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt", () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../shared/cost"
|
||||
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStreamChunk } from "../transform/stream"
|
||||
|
|
@ -196,13 +197,31 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
}
|
||||
|
||||
if (lastUsage) {
|
||||
// Get model info to calculate cost locally
|
||||
const modelInfo = this.getModel().info
|
||||
|
||||
// Calculate cost locally using model pricing information
|
||||
// OpenRouter uses OpenAI-style token counting (input tokens include cached tokens)
|
||||
const localCost = calculateApiCostOpenAI(
|
||||
modelInfo,
|
||||
lastUsage.prompt_tokens || 0,
|
||||
lastUsage.completion_tokens || 0,
|
||||
undefined, // cache creation tokens - OpenRouter doesn't distinguish this
|
||||
lastUsage.prompt_tokens_details?.cached_tokens,
|
||||
)
|
||||
|
||||
// Use locally calculated cost, but fall back to API response if our calculation fails
|
||||
// or if model pricing info is not available
|
||||
const apiCost = (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0)
|
||||
const totalCost = modelInfo.inputPrice && modelInfo.outputPrice ? localCost : apiCost
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: lastUsage.prompt_tokens || 0,
|
||||
outputTokens: lastUsage.completion_tokens || 0,
|
||||
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
|
||||
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
|
||||
totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0),
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue