[型の不一致でコストが$0を返す] - OpenRouter cost tracking returns $0 due to type mismatch in extractCostBreakdown()
extractCostBreakdown()関数はusage.costをオブジェクトとして期待していますが、OpenRouterは数値として返すため、成本のトラッキングがsilentに失敗します。
🔍 症状
主な症状: OpenRouter APIリクエストがusage.costをスカラー数値(例:0.0045)として返すにもかかわらず、成本追跡ロジックがその値をネストされたオブジェクトプロパティとしてアクセスしようとします。
エラー出力:
// pi-embedded-*.jsのextractCostBreakdown()関数内:
const total = toFiniteNumber(cost.total); // costは数値0.0045
// 結果: total = NaN(cost.total === undefinedのため)
// 記録されるコスト: $0.00
// コンソール出力の例:
[OpenClaw] OpenRouter Request Complete
Model: anthropic/claude-3.5-sonnet
Cost: $0.00 ← 不正確(~$0.0045であるべき)
Tokens: 2453 input, 892 output
技術的な検出方法:
// OpenRouterレスポンスのデバッグ検査
console.log(response.usage.cost);
// 出力: 0.0045 (数値)
// 内部コスト抽出のデバッグ検査
console.log(typeof response.usage.cost);
// 出力: "number"
// 他のプロバイダーでの想定構造
console.log(response.usage.cost);
// 出力: { total: 0.0045, input: 0.001, output: 0.0035 } (オブジェクト)
🧠 原因
アーキテクチャの相違: extractCostBreakdown()関数は、usage.costが常にtotal、input、outputキーを含むオブジェクトであるOpenAI互換のコスト構造を前提として設計されていました。
失敗のシーケンス:
- OpenRouter APIが
usage.cost = 0.0045(スカラー数値)を返す extractCostBreakdown()を実行:const total = toFiniteNumber(cost.total)costが数値の場合、cost.totalはundefinedと評価されるtoFiniteNumber(undefined)がNaNを返す- コスト集計ロジックが
NaNを表示 목적으로$0として扱う - エラーはスローされない—失敗はサイレントに発生
影響を受けるコードパス:
// ファイル: dist/pi-embedded-*.js
// 位置: extractCostBreakdown()関数
function extractCostBreakdown(cost) {
const total = toFiniteNumber(cost.total); // ← costが数値の場合に失敗
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
プロバイダー比較:
| Provider | usage.cost 型 | 構造 |
|---|---|---|
| OpenAI | Object | { total, input, output } |
| Azure OpenAI | Object | { total, input, output } |
| Anthropic | Object | { total, input, output } |
| OpenRouter | Number | 0.0045 (単一値) |
🛠️ 解決手順
対象ファイル: 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) {
// OpenRouterの単一数値 vs OpenAI互換のオブジェクトを処理
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) {
// 防御的: 入力タイプに関係なくコストをオブジェクト構造に正規化
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()を含むソースファイルに移動します:
# 標準的なプロジェクト構造を想定
find . -name "*.ts" -o -name "*.js" | xargs grep -l "extractCostBreakdown"
# 出力: src/providers/openrouter.ts または src/utils/cost.ts
distribuitionバンドルを再構築する前に、ソースファイルに直接タイプガードの修正を適用します。
🧪 検証
テストケース1: OpenRouterの単一数値コスト
// OpenRouterレスポンスをシミュレート
const mockOpenRouterCost = 0.0045;
const result = extractCostBreakdown(mockOpenRouterCost);
console.log(result);
// 期待値: { total: 0.0045, input: NaN or 0, output: NaN or 0 }
// totalが正しく抽出されていることを確認
console.log(Number.isFinite(result.total));
// 期待値: true
テストケース2: OpenAI互換のオブジェクトコスト
// 標準的なプロバイダーのレスポンスをシミュレート
const mockStandardCost = { total: 0.012, input: 0.006, output: 0.006 };
const result = extractCostBreakdown(mockStandardCost);
console.log(result);
// 期待値: { total: 0.012, input: 0.006, output: 0.006 }
統合検証:
# コスト追跡テストスイートを実行
npm test -- --grep "cost"
# またはOpenRouter固有の場合
npm test -- --grep "OpenRouter"
# 期待値: すべてのコスト抽出テストがパス
手動のエンドツーエンド検証:
# 1. シンプルなOpenRouterリクエストを実行
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. OpenClawログで抽出されたコストを確認
# $0.00ではなく実際のコスト値が表示されるべき
修正後の期待されるログ出力:
[OpenClaw] OpenRouter Request Complete
Model: anthropic/claude-3.5-sonnet
Cost: $0.0045 ← 正確
Tokens: 2453 input, 892 output
⚠️ よくある落とし穴
- サイレント失敗パターン: このバグはスローされるエラーを生成しません。
NaNや$0の異常を検出するために、成本抽出に常にロギングを組み込んでください。 - その他の単一数値を返すプロバイダー: 将来的にスカラー
usage.costを返すOpenAI互換のプロバイダーが登場した場合、同じ失敗が発生します。コードベースでこの前提条件を文書化してください。 - ビルド成果物の不一致: ソースファイルを修正する場合は
dist/pi-embedded-*.jsを再構築する必要があります。 distribuitionバンドルがソースの変更を反映していることを確認してください。# 一般的なミス: 再構築せずにdistファイルを編集 # ソース変更後は常に再構築 npm run build npm run dist - toFiniteNumberのエッジケース: ヘルパー関数
toFiniteNumber()はundefinedやnullに対して0を返す場合があり、根本的な問題を隠す可能性があります。// toFiniteNumberの動作を確認 toFiniteNumber(undefined); // 実装に応じてNaNまたは0を返す toFiniteNumber(null); // 0を返す toFiniteNumber(NaN); // NaNを返す - トークンコスト vs 合計コスト: コストが単一数値の場合、
inputとoutputの内訳は失われます。きめ細かいコストレポートが必要かどうかを検討してください。 - OpenRouterの/autoルーティング:
/autoモデル選択を使用する場合、コスト計算はレスポンス後に行われます。修正が明示的なルーティングと自動ルーティングの両方のリクエストに適用されることを確認してください。
🔗 関連するエラー
TypeError: Cannot read property 'total' of undefinedusage.cost自体が数値ではなくundefinedの場合に発生します。異なる失敗モードですが、同じ関数の位置で発生します。- コスト集計ログに
NaNが表示される
成本抽出の結果が算術演算を通じて伝播する際に、現在のバグの症状が発生します。 - GitHub Issue #142: "Cost tracking broken for provider X returns $0"
他のプロバイダーが非標準のコスト形式で同じ症状を引き起こした歴史的なパターン。 TypeError: cost.total is not a function(誤解を招くメッセージ)costが数値の文字列表現(例:"0.0045")の場合にトリガーされます。- webhook/ペイロードのコストデータ欠落
コスト追跡の失敗がエクスポートされたレポートの空のcost_breakdownフィールドの原因となる、関連する下游の問題。 - OpenRouter APIのコスト不一致
OpenRouterは特定の無料モデルやキャッシュされていないモデルのコストをnullとして返す場合があります。null処理も考慮してください。