この記事について
LLM を組み込んだアプリやエージェントを運用していると、「今月いくら使ったか」は請求書で分かっても、「どの機能が・なぜ・いくら使ったか」は意外と分かりません。この記事では、LLM 呼び出しのコスト計測を自前で作るとき、あるいはツールを選ぶときに踏みがちな落とし穴を 5 つ紹介します。どれも、私が観測サービス(Argosvix(アーゴスヴィクス))を作る過程で実際に踏んだものです。
落とし穴 1: ストリーミングだとトークン使用量(usage)が返ってこない(ことがある)
OpenAI の Chat Completions は、ストリーミング時に stream_options: { include_usage: true } を付けないと usage(トークン数)を一切返しません。これを知らずに計測すると、ストリーミング呼び出しがすべて「0 トークン・$0」として記録され、コストが静かに過少計上されます。チャット UI はほぼ例外なくストリーミングなので、影響はほぼ全面に及びます。
対策: 計測側で include_usage を自動で補います。ただし補った場合、ストリームの末尾に追加される usage 専用のチャンク(choices が空)がアプリ側にそのまま流れると、choices[0] を前提にした素朴なコードが落ちます。このチャンクは計測側で取り除いておく必要があります。
落とし穴 2: キャッシュトークンを無視すると過大計上になる
OpenAI の prompt_tokens はキャッシュ読み取り分を含む合計で、キャッシュ分は割引価格です。prompt_tokens × 単価 という素朴な計算だと、プロンプトキャッシュがよく効いている処理ほど実際より高く見積もってしまいます。逆に Anthropic はキャッシュの書き込みと読み取りが別フィールドなので、対応しないと今度は過少になります。「プロバイダーごとに usage の意味が違う」という前提で設計する必要があります。
落とし穴 3: サーバーレスでは「送ったつもり」のデータが消える
Cloudflare Workers や AWS Lambda は、レスポンスを返した瞬間に実行が止まります。計測データを送りっぱなし(送信結果を待たない実装)にしていると、エラーも出ないままデータが欠損します。私の場合はこれが一番怖い障害でした。計測が失敗しているのに、失敗したことにすら気づけないからです。
対策: Workers なら ctx.waitUntil() を使う、送信完了を待つ処理を明示的に呼べるようにしておく、などの方法で送信が終わるまで実行環境を生かします。計測 SDK を選ぶときは、サーバーレス対応が明記されているかを確認してください。
落とし穴 4: 途中で切られたストリームのコストが消える
ユーザーがストリーミングの途中でページを閉じたりキャンセルしたりするのは、ごく普通に起きます。このとき「最後まで読み切ったら記録する」という設計だと、課金は発生しているのに記録は 0 になります。プロバイダーは切断までのトークンをそのまま課金してくるからです。
対策: 正常終了時だけでなく、中断時やエラー時にも「そこまでの分」を記録する経路を必ず作っておきます。
落とし穴 5: モデル別単価表は「作った日から腐る」
モデルごとの単価をコードに直書きすると、新モデルが出るたびに古くなります。単価表にないモデルを $0 として扱う実装だと、新モデルに切り替えた瞬間、コストが消えたように見えます。
対策: 未知のモデルを黙って $0 で計上しないこと。警告を出して「単価が取れていないモデルがある」と気づける状態にしておきます。黙った 0 と警告付きの 0 は、運用上まったくの別物です。
まとめ: 自作するなら、この 5 つをチェックリストに
- ストリーミングでのトークン使用量(usage)の取得(
include_usageの自動補完と usage チャンクの隔離) - プロバイダーごとのキャッシュトークンの扱いの違い
- サーバーレスでの送信完了の保証
- 中断されたストリームの部分記録
- 未知モデルの扱い(黙って $0 にしない)
これらを全部自前でやるのは意外と重いので、既製の観測サービスに任せるのも手です。私が作っている Argosvix は上記を全部 SDK 側で処理します(wrap(new OpenAI()) の 1 行、無料枠あり)。開発の経緯や中身の話は Zenn の記事に書いたので、興味があればそちらへ。