April 21, 2026

[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 始终是一个包含 totalinputoutput 键的对象。

故障序列:

  1. OpenRouter API 返回 usage.cost = 0.0045(标量数字)
  2. extractCostBreakdown() 执行:const total = toFiniteNumber(cost.total)
  3. cost 是数字时,cost.total 计算结果为 undefined
  4. toFiniteNumber(undefined) 返回 NaN
  5. 成本聚合逻辑将 NaN 视为 $0 进行显示
  6. 没有抛出错误——故障是静默发生的

受影响的代码路径:

// 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 };
}

提供商对比:

Providerusage.cost TypeStructure
OpenAIObject{ total, input, output }
Azure OpenAIObject{ total, input, output }
AnthropicObject{ total, input, output }
OpenRouterNumber0.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() 可能对 undefinednull 返回 0,从而掩盖底层问题。
    // Verify toFiniteNumber behavior
    toFiniteNumber(undefined);  // Returns NaN or 0 depending on implementation
    toFiniteNumber(null);       // Returns 0
    toFiniteNumber(NaN);        // Returns NaN
    
  • 令牌成本与总成本: 当成本是平面数字时,会丢失 inputoutput 的详细分解。请考虑是否需要细粒度的成本报告。
  • OpenRouter /auto 路由: 使用 /auto 模型选择时,成本计算在响应后进行。请确保修复同时适用于显式和自动路由的请求。

🔗 相关错误

  • TypeError: Cannot read property 'total' of undefined
    usage.cost 本身是 undefined 而非数字时发生。不同的失败模式但位于同一函数位置。
  • NaN appearing 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 作为成本。请同时考虑空值处理。

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。