Skip to content

Commit 7ca5357

Browse files
harrisonyclaudemcowger
authored
feat: capture provider cost details (#521)
## Summary - **fix**: add `upstream_inference_cost` fallback for total cost so BYOK requests (where `usage.cost=0`) report the real spend - **refactor**: stop aliasing `upstream_inference_prompt_cost` → `input_cost`; keep upstream fields separate since `upstream_inference_prompt_cost = input_cost + cached_input_cost` (aliasing silently zeroed out `costCached`) - **feat**: add three-tier cost dispatch in `applyUsageCostDetails` — superset (per-bucket gateway fields) → normal (upstream prompt/completions split with cache ratio) → minimal (proportional fallback) - **test**: expand coverage with edge cases — zero-cost non-BYOK, OpenRouter markup (cost >> upstream sum), zero prompt tokens, heavy-cache-hit ratio split, end-to-end BYOK and non-BYOK flows ## Test plan - [x] All 1378 backend tests pass (`bun run test:force-all`) - [x] Biome lint clean (`bunx biome lint .`) - [x] Each commit independently passes lint + tests (verified via `git rebase --exec`) > **Note:** I mixed up the branches in #443, and also mixed up #520. That PR couldn't be reopened due to a force-push. This replaces that PR. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Matt Cowger <mcowger@users.noreply.github.com> Co-authored-by: Matt Cowger <matt@cowger.us>
1 parent fec6004 commit 7ca5357

6 files changed

Lines changed: 1210 additions & 87 deletions

File tree

packages/backend/src/services/inspectors/usage-logging.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { calculateCosts } from '../../utils/calculate-costs';
77
import { DebugManager } from '../debug-manager';
88
import { estimateTokensFromReconstructed, estimateInputTokens } from '../../utils/estimate-tokens';
99
import {
10+
normalizeAnthropicUsage,
1011
normalizeGeminiUsage,
1112
normalizeOpenAIChatUsage,
1213
normalizeOpenAIResponsesUsage,
@@ -148,6 +149,15 @@ export class UsageInspector extends PassThrough {
148149
// Some providers emit `: cost {"request_cost_usd": ...}` as SSE comments
149150
if (reconstructed?.providerReportedCost) {
150151
applyProviderReportedCost(this.usageRecord, reconstructed.providerReportedCost);
152+
if (reconstructed?.usage) {
153+
const usageCostDetails = extractUsageCostDetails(reconstructed.usage);
154+
if (usageCostDetails) {
155+
logger.debug(
156+
`[ProviderCost] Both SSE :cost and usage.cost_details present for ${this.usageRecord.requestId}; ` +
157+
`SSE value ($${this.usageRecord.providerReportedCost}) takes priority over cost_details total ($${usageCostDetails.total_cost})`
158+
);
159+
}
160+
}
151161
}
152162

153163
// Override with provider-reported cost from usage.cost_details if available
@@ -318,15 +328,17 @@ export class UsageInspector extends PassThrough {
318328
};
319329
}
320330
case 'messages':
321-
return reconstructed.usage
322-
? {
323-
inputTokens: reconstructed.usage.input_tokens || 0,
324-
outputTokens: reconstructed.usage.output_tokens || 0,
325-
cachedTokens: reconstructed.usage.cache_read_input_tokens || 0,
326-
cacheWriteTokens: reconstructed.usage.cache_creation_input_tokens || 0,
327-
reasoningTokens: 0,
328-
}
329-
: null;
331+
if (!reconstructed.usage) return null;
332+
{
333+
const usage = normalizeAnthropicUsage(reconstructed.usage);
334+
return {
335+
inputTokens: usage.input_tokens,
336+
outputTokens: usage.output_tokens,
337+
cachedTokens: usage.cached_tokens,
338+
cacheWriteTokens: usage.cache_creation_tokens,
339+
reasoningTokens: usage.reasoning_tokens,
340+
};
341+
}
330342
case 'gemini':
331343
if (!reconstructed.usageMetadata) return null;
332344
{

packages/backend/src/services/response-handler.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,15 @@ async function finalizeUsage(
502502
const reconstructed = debugManager.getReconstructedRawResponse(usageRecord.requestId!);
503503
if (reconstructed?.providerReportedCost) {
504504
applyProviderReportedCost(usageRecord, reconstructed.providerReportedCost);
505+
if (reconstructed?.usage) {
506+
const usageCostDetails = extractUsageCostDetails(reconstructed.usage);
507+
if (usageCostDetails) {
508+
logger.debug(
509+
`[ProviderCost] Both SSE :cost and usage.cost_details present for ${usageRecord.requestId}; ` +
510+
`SSE value ($${usageRecord.providerReportedCost}) takes priority over cost_details total ($${usageCostDetails.total_cost})`
511+
);
512+
}
513+
}
505514
}
506515

507516
// Also check for cost_details in the usage block (some providers embed costs there)

0 commit comments

Comments
 (0)