From 3ac6634140b3559303aa1833d99da48a7a0f9fa1 Mon Sep 17 00:00:00 2001 From: nordicnode Date: Mon, 31 Aug 2026 12:15:01 -0700 Subject: [PATCH] fix(sdk): take max of OpenRouter cost and upstream_inference_cost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit usage.cost is the total amount charged and already contains the upstream portion reported separately in cost_details.upstream_inference_cost, so summing the two roughly doubles the credits on normal (non-BYOK) OpenRouter routes — the shape behind the inflated-usage reports. The ledger side already takes the max (see the Solar Pro 4 BYOK note in common/src/constants/freebuff-models.ts, where cost is 0 and upstream carries the real spend); this fixes the user-facing credit path the same way at all three sites (stream, generateText, structured). Fixes #1164 --- sdk/src/impl/__tests__/usage-receipts.test.ts | 73 +++++++++++++++++++ sdk/src/impl/llm.ts | 24 ++++-- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/sdk/src/impl/__tests__/usage-receipts.test.ts b/sdk/src/impl/__tests__/usage-receipts.test.ts index 7e4bba3b96..026a628467 100644 --- a/sdk/src/impl/__tests__/usage-receipts.test.ts +++ b/sdk/src/impl/__tests__/usage-receipts.test.ts @@ -22,6 +22,79 @@ const logger = { }, } +describe('credit calculation from OpenRouter usage accounting', () => { + test('takes max of cost and upstream_inference_cost, never the sum', async () => { + // usage.cost is the total amount charged and already includes the + // upstream portion reported separately in cost_details + // (issue #1164): summing them roughly doubles credits on normal + // OpenRouter routes. BYOK routes carry the spend in upstream with + // cost = 0, which max handles and sum would zero out. + const costs: number[] = [] + const chunks = [ + { + id: 'chatcmpl-credits-1', + object: 'chat.completion.chunk', + created: 1, + model: 'test-model', + choices: [ + { + index: 0, + delta: { content: 'hello' }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-credits-1', + object: 'chat.completion.chunk', + created: 1, + model: 'test-model', + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + usage: { + prompt_tokens: 100, + completion_tokens: 20, + total_tokens: 120, + cost: 0.01, + cost_details: { upstream_inference_cost: 0.02 }, + }, + }, + ] + globalThis.fetch = (() => + Promise.resolve( + new Response( + `${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join('')}data: [DONE]\n\n`, + { headers: { 'Content-Type': 'text/event-stream' } }, + ), + )) as unknown as typeof fetch + + const stream = promptAiSdkStream({ + apiKey: 'test-key', + runId: 'run-credits-1', + messages: [{ role: 'user', content: 'hello' }], + clientSessionId: 'session-credits-1', + fingerprintId: 'fingerprint-credits-1', + model: 'openai/gpt-5.6-luna', + userId: 'user-1', + userInputId: 'input-credits-1', + onCostCalculated: async (credits: number) => { + costs.push(credits) + }, + sendAction: async () => undefined, + logger, + trackEvent: async () => undefined, + signal: new AbortController().signal, + } as unknown as Parameters[0]) + + for await (const chunk of stream) { + void chunk + } + + expect(costs).toHaveLength(1) + // max(0.01, 0.02) = 0.02 → * 1.055 margin * 100 credits-per-dollar. + expect(costs[0]).toBe(Math.round(0.02 * 1.055 * 100)) + }) +}) + describe('stream usage receipts', () => { test('reports final usage and cost before yielding an output-limit recovery', async () => { const usage: Array> = [] diff --git a/sdk/src/impl/llm.ts b/sdk/src/impl/llm.ts index 2ce4cdb443..bd32b8f5e7 100644 --- a/sdk/src/impl/llm.ts +++ b/sdk/src/impl/llm.ts @@ -391,9 +391,15 @@ export async function* promptAiSdkStream( const openrouterUsage = providerMetadata?.codebuff?.usage as | OpenRouterUsageAccounting | undefined + // usage.cost is the total charged and already contains the upstream + // portion reported separately in cost_details.upstream_inference_cost + // (see the Solar Pro 4 BYOK note in common/src/constants/freebuff-models.ts, + // where cost is 0 and upstream carries the real spend): max, never sum. const costOverrideDollars = openrouterUsage - ? (openrouterUsage.cost ?? 0) + - (openrouterUsage.costDetails?.upstreamInferenceCost ?? 0) + ? Math.max( + openrouterUsage.cost ?? 0, + openrouterUsage.costDetails?.upstreamInferenceCost ?? 0, + ) : undefined if (!params.onCostCalculated || !costOverrideDollars) return costReported = true @@ -732,9 +738,10 @@ export async function promptAiSdk( const openrouterUsage = providerMetadata.codebuff .usage as OpenRouterUsageAccounting - costOverrideDollars = - (openrouterUsage.cost ?? 0) + - (openrouterUsage.costDetails?.upstreamInferenceCost ?? 0) + costOverrideDollars = Math.max( + openrouterUsage.cost ?? 0, + openrouterUsage.costDetails?.upstreamInferenceCost ?? 0, + ) } } @@ -803,9 +810,10 @@ export async function promptAiSdkStructured( const openrouterUsage = providerMetadata.codebuff .usage as OpenRouterUsageAccounting - costOverrideDollars = - (openrouterUsage.cost ?? 0) + - (openrouterUsage.costDetails?.upstreamInferenceCost ?? 0) + costOverrideDollars = Math.max( + openrouterUsage.cost ?? 0, + openrouterUsage.costDetails?.upstreamInferenceCost ?? 0, + ) } }