April 23, 2026

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:

  1. OpenRouter API returns usage.cost = 0.0045 (scalar number)
  2. extractCostBreakdown() executes: const total = toFiniteNumber(cost.total)
  3. When cost is a number, cost.total evaluates to undefined
  4. toFiniteNumber(undefined) returns NaN
  5. Cost aggregation logic treats NaN as $0 for display purposes
  6. 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:

Providerusage.cost TypeStructure
OpenAIObject{ total, input, output }
Azure OpenAIObject{ total, input, output }
AnthropicObject{ total, input, output }
OpenRouterNumber0.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 NaN or $0 anomalies.
  • Other Flat-Number Providers: Any future OpenAI-compatible provider returning a scalar usage.cost will 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 return 0 for undefined or null, 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, input and output breakdown is lost. Consider if granular cost reporting is required.
  • OpenRouter /auto Routing: When using /auto model selection, the cost calculation occurs post-response. Ensure the fix applies to both explicit and auto-routed requests.
  • TypeError: Cannot read property 'total' of undefined
    Occurs when usage.cost itself is undefined rather than a number. Different failure mode but same function location.
  • NaN appearing 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 if cost is a string representation of a number (e.g., "0.0045").
  • Missing cost data in webhook/payload
    Related downstream issue where cost tracking failures cause empty cost_breakdown fields in exported reports.
  • OpenRouter API Cost Discrepancy
    OpenRouter may return cost as null for certain free or uncached models. Ensure null handling is also considered.

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.