OpenRouter cost tracking returns $0 due to type mismatch in extractCostBreakdown()
The extractCostBreakdown() function expects usage.cost as an object but OpenRouter returns it as a flat number, causing silent cost tracking failures.
๐ Symptoms
Primary Manifestation: OpenRouter API requests return usage.cost as a scalar numeric value (e.g., 0.0045), but the cost tracking logic attempts to access it as a nested object property.
Error Output:
// In pi-embedded-*.js extractCostBreakdown() function:
const total = toFiniteNumber(cost.total); // cost is number 0.0045
// Result: total = NaN (since cost.total === undefined)
// Cost logged as: $0.00
// Console output example:
[OpenClaw] OpenRouter Request Complete
Model: anthropic/claude-3.5-sonnet
Cost: $0.00 โ INCORRECT (should be ~$0.0045)
Tokens: 2453 input, 892 output
Technical Detection:
// Debug inspection of OpenRouter response
console.log(response.usage.cost);
// Output: 0.0045 (number)
// Debug inspection of internal cost extraction
console.log(typeof response.usage.cost);
// Output: "number"
// Expected structure per other providers
console.log(response.usage.cost);
// Output: { total: 0.0045, input: 0.001, output: 0.0035 } (object)
๐ง Root Cause
Architectural Divergence: The extractCostBreakdown() function was designed against OpenAI-compatible cost structures where usage.cost is always an object containing total, input, and output keys.
Failure Sequence:
- OpenRouter API returns
usage.cost = 0.0045(scalar number) extractCostBreakdown()executes:const total = toFiniteNumber(cost.total)- When
costis a number,cost.totalevaluates toundefined toFiniteNumber(undefined)returnsNaN- Cost aggregation logic treats
NaNas$0for display purposes - No error is thrownโfailure is silent
Affected Code Path:
// File: dist/pi-embedded-*.js
// Location: extractCostBreakdown() function
function extractCostBreakdown(cost) {
const total = toFiniteNumber(cost.total); // โ FAILS when cost is number
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
Provider Comparison:
| Provider | usage.cost Type | Structure |
|---|---|---|
| OpenAI | Object | { total, input, output } |
| Azure OpenAI | Object | { total, input, output } |
| Anthropic | Object | { total, input, output } |
| OpenRouter | Number | 0.0045 (flat value) |
๐ ๏ธ Step-by-Step Fix
Target File: dist/pi-embedded-*.js (or source equivalent)
Required Change: Update the extractCostBreakdown() function to handle both scalar number and object types for usage.cost.
Before:
function extractCostBreakdown(cost) {
const total = toFiniteNumber(cost.total);
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
After:
function extractCostBreakdown(cost) {
// Handle OpenRouter flat number vs OpenAI-compatible object
const total = toFiniteNumber(typeof cost === 'number' ? cost : cost.total);
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
Alternative Robust Implementation:
function extractCostBreakdown(cost) {
// Defensive: normalize cost to object shape regardless of input type
const costObj = typeof cost === 'number'
? { total: cost, input: 0, output: 0 }
: cost;
const total = toFiniteNumber(costObj.total);
const input = toFiniteNumber(costObj.input);
const output = toFiniteNumber(costObj.output);
return { total, input, output };
}
Source File Fix (if available):
Navigate to the source file containing extractCostBreakdown():
# Assuming standard project structure
find . -name "*.ts" -o -name "*.js" | xargs grep -l "extractCostBreakdown"
# Output: src/providers/openrouter.ts or src/utils/cost.ts
Apply the type guard fix directly in the source file before rebuilding the distribution bundle.
๐งช Verification
Test Case 1: OpenRouter Flat Number Cost
// Simulate OpenRouter response
const mockOpenRouterCost = 0.0045;
const result = extractCostBreakdown(mockOpenRouterCost);
console.log(result);
// Expected: { total: 0.0045, input: NaN or 0, output: NaN or 0 }
// Verify total is correctly extracted
console.log(Number.isFinite(result.total));
// Expected: true
Test Case 2: OpenAI-Compatible Object Cost
// Simulate standard provider response
const mockStandardCost = { total: 0.012, input: 0.006, output: 0.006 };
const result = extractCostBreakdown(mockStandardCost);
console.log(result);
// Expected: { total: 0.012, input: 0.006, output: 0.006 }
Integration Verification:
# Run cost tracking test suite
npm test -- --grep "cost"
# Or specific to OpenRouter
npm test -- --grep "OpenRouter"
# Expected: All cost extraction tests pass
Manual End-to-End Verification:
# 1. Execute a simple OpenRouter request
curl -X POST https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-3.5-sonnet",
"messages": [{"role": "user", "content": "Hello"}]
}'
# 2. Check extracted cost in OpenClaw logs
# Should show actual cost value, not $0.00
Expected Log Output After Fix:
[OpenClaw] OpenRouter Request Complete
Model: anthropic/claude-3.5-sonnet
Cost: $0.0045 โ CORRECT
Tokens: 2453 input, 892 output
โ ๏ธ Common Pitfalls
- Silent Failure Pattern: This bug produces no thrown errors. Always instrument cost extraction with logging to detect
NaNor$0anomalies. - Other Flat-Number Providers: Any future OpenAI-compatible provider returning a scalar
usage.costwill trigger identical failure. Document this assumption in the codebase. - Build Artifact Mismatch: Fixing source files requires rebuilding
dist/pi-embedded-*.js. Verify the distribution bundle reflects the source changes.# Common mistake: editing dist file without rebuilding # Always rebuild after source changes npm run build npm run dist - toFiniteNumber Edge Cases: The helper function
toFiniteNumber()may return0forundefinedornull, masking the underlying issue.// Verify toFiniteNumber behavior toFiniteNumber(undefined); // Returns NaN or 0 depending on implementation toFiniteNumber(null); // Returns 0 toFiniteNumber(NaN); // Returns NaN - Token Cost vs Total Cost: When cost is a flat number,
inputandoutputbreakdown is lost. Consider if granular cost reporting is required. - OpenRouter /auto Routing: When using
/automodel selection, the cost calculation occurs post-response. Ensure the fix applies to both explicit and auto-routed requests.
๐ Related Errors
TypeError: Cannot read property 'total' of undefined
Occurs whenusage.costitself isundefinedrather than a number. Different failure mode but same function location.NaNappearing in cost aggregation logs
Symptom of the current bug when cost extraction result propagates through arithmetic operations.- GitHub Issue #142: "Cost tracking broken for provider X returns $0"
Historical pattern where other providers with non-standard cost formats caused identical symptoms. TypeError: cost.total is not a function(misleading message)
Triggered ifcostis a string representation of a number (e.g.,"0.0045").- Missing cost data in webhook/payload
Related downstream issue where cost tracking failures cause emptycost_breakdownfields in exported reports. - OpenRouter API Cost Discrepancy
OpenRouter may return cost asnullfor certain free or uncached models. Ensure null handling is also considered.