[extractCostBreakdown() 类型不匹配导致成本显示 $0] - OpenRouter cost tracking returns $0 due to type mismatch in extractCostBreakdown()
extractCostBreakdown() 函数期望 usage.cost 为对象类型,但 OpenRouter 返回的是数字类型,导致成本追踪静默失败。
🔍 症状
主要表现: OpenRouter API 请求返回的 usage.cost 为标量数值(例如 0.0045),但成本跟踪逻辑尝试将其作为嵌套对象属性来访问。
错误输出:
// 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
技术检测:
// 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)
🧠 根因分析
架构差异: extractCostBreakdown() 函数是针对 OpenAI 兼容的成本结构设计的,其中 usage.cost 始终是一个包含 total、input 和 output 键的对象。
故障序列:
- OpenRouter API 返回
usage.cost = 0.0045(标量数字) extractCostBreakdown()执行:const total = toFiniteNumber(cost.total)- 当
cost是数字时,cost.total计算结果为undefined toFiniteNumber(undefined)返回NaN- 成本聚合逻辑将
NaN视为$0进行显示 - 没有抛出错误——故障是静默发生的
受影响的代码路径:
// 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 | 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) |
🛠️ 逐步修复
目标文件: dist/pi-embedded-*.js(或源文件对应位置)
所需修改: 更新 extractCostBreakdown() 函数以同时处理 usage.cost 的标量数字和对象类型。
修改前:
function extractCostBreakdown(cost) {
const total = toFiniteNumber(cost.total);
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
修改后:
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 };
}
替代的健壮实现:
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 };
}
源文件修复(如有):
导航至包含 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
在重新构建分发包之前,直接在源文件中应用类型保护修复。
🧪 验证
测试用例 1:OpenRouter 平面数字成本
// 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
测试用例 2:OpenAI 兼容对象成本
// 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 }
集成验证:
# Run cost tracking test suite
npm test -- --grep "cost"
# Or specific to OpenRouter
npm test -- --grep "OpenRouter"
# Expected: All cost extraction tests pass
手动端到端验证:
# 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
修复后的预期日志输出:
[OpenClaw] OpenRouter Request Complete
Model: anthropic/claude-3.5-sonnet
Cost: $0.0045 ← CORRECT
Tokens: 2453 input, 892 output
⚠️ 常见陷阱
- 静默失败模式: 此缺陷不会产生抛出的错误。请务必使用日志记录来检测
NaN或$0异常,以便发现成本提取问题。 - 其他平面数字提供商: 任何未来返回标量
usage.cost的 OpenAI 兼容提供商都将触发相同的失败。请在代码库中记录此假设。 - 构建产物不匹配: 修复源文件后需要重新构建
dist/pi-embedded-*.js。请验证分发包反映了源文件的更改。# Common mistake: editing dist file without rebuilding # Always rebuild after source changes npm run build npm run dist - toFiniteNumber 边缘情况: 辅助函数
toFiniteNumber()可能对undefined或null返回0,从而掩盖底层问题。// Verify toFiniteNumber behavior toFiniteNumber(undefined); // Returns NaN or 0 depending on implementation toFiniteNumber(null); // Returns 0 toFiniteNumber(NaN); // Returns NaN - 令牌成本与总成本: 当成本是平面数字时,会丢失
input和output的详细分解。请考虑是否需要细粒度的成本报告。 - OpenRouter /auto 路由: 使用
/auto模型选择时,成本计算在响应后进行。请确保修复同时适用于显式和自动路由的请求。
🔗 相关错误
TypeError: Cannot read property 'total' of undefined
当usage.cost本身是undefined而非数字时发生。不同的失败模式但位于同一函数位置。NaNappearing in cost aggregation logs
当成本提取结果通过算术运算传播时,这是当前缺陷的症状。- GitHub Issue #142: "Cost tracking broken for provider X returns $0"
历史模式,其他具有非标准成本格式的提供商导致相同的症状。 TypeError: cost.total is not a function(misleading message)
如果cost是数字的字符串表示形式(例如"0.0045")时会触发。- Missing cost data in webhook/payload
相关的下游问题,成本跟踪失败导致导出报告中cost_breakdown字段为空。 - OpenRouter API Cost Discrepancy
OpenRouter 可能对某些免费或未缓存的模型返回null作为成本。请同时考虑空值处理。