AI コストが SaaS コストより難しい理由
伝統的な SaaS 課金は月額シート単価のフラットな数字。ユニットエコノミクスはスプレッドシートで済む。AI コストはそのどれでもない。
1回のチャット補完リクエストが運ぶもの:
- トークン単価 — 入力トークン、出力トークン、推論トークン、キャッシュトークン、それぞれ別レート。
- プロバイダー毎の単価 — OpenAI、Anthropic、Google、Groq、DeepSeek はすべて異なる価格、異なる課金単位、異なる割引メカニズム (プロンプトキャッシュ、バッチ、ボリューム階層)。
- 為替エクスポージャー — プロバイダー公開価格は USD。日本・EU・UK の顧客は JPY・EUR・GBP で払う。レートは日々動く。
- ワークロード駆動のばらつき — 同じプロンプトでモデル選択、出力長、ツール使用によって $0.0003 にも $3.00 にもなる。
結果: コストはリクエストだけからは予測できない。上流呼び出しが戻り、プロバイダーが利用量を報告した後にのみ判明する。
これは「プロバイダーに請求された額を請求する」という素朴な課金モデルを壊す。コストが分かる時点ではリクエストは終わっている。顧客に予算がなければ、あなたが既にそれを被っている。
表現: micro-USD、整数
SchneeAI はコストを BIGINT の micro-USD で保存。1 US ドル = 100万 micros。典型的な GPT-4o 補完は 1,000-3,000 micros (中規模チャットターンで $0.001-$0.003)。DeepSeek-V3 補完は 100-300 micros。
なぜ NUMERIC(10,4) や FLOAT ではなく整数か?
float は金を失う。 小さな浮動小数点値の反復加算は丸め誤差を蓄積する。1日百万リクエスト規模では誤差は現実のものになる。さらに悪いことに、決定論的ではない — 同じワークロードを2回課金すると微妙に異なる合計になる。課金システムには許容できない。
decimal は正確だが遅い。 NUMERIC は厳密だが、その演算は BIGINT より意味fully遅く、Postgres でロック取らずにアトミックにインクリメントできない。リクエスト毎に台帳を更新するホットパスでは問題になる。
整数としての micros がスイートスポット。 小数点以下6桁は現実的な per-request 課金 (最小規模の入出力各10トークンの DeepSeek リクエストで約14 micros、クリーンに丸まる) に十分な解像度。整数演算は厳密かつ高速。アトミックな UPDATE ... SET balance = balance + $1 がロックのサプライズなく動く。BIGINT の 2^63 天井は約 $9.2 兆 — 米GDPの約3分の1、いかなるアカウントも到達しない規模。
同じ慣習を顧客向け JPY 金額 (amount_jpy BIGINT) とクレジット (credits BIGINT、デフォルトで 1 クレジット = 1 micro-USD) に適用。為替レートは NUMERIC(20,10) で保存 — 金額ではなく比率だから。
タイミング問題
基本的な設計問題はこれ:
顧客が呼び出しを支払えるかを呼び出しの 前に 判断しなければならない。呼び出しのコストが分かるのは 後に 返ってきた時だけ。
前払いチェックをスキップすれば、クレジット $0 の顧客からの $5 リクエストは $5 の損失。素朴に最大可能コストを予約すれば、残高を何時間もロックし、顧客は文句を言う。
これを解くパターンは 2相 reserve-and-settle — 決済処理の「オーソリ」と「キャプチャ」に相当。
Phase 1: Reserve (Tx1)
上流呼び出しの前に、SchneeAI は Postgres トランザクションを開き:
- モデル、プロンプトサイズ、max_tokens に基づき コスト見積り。見積りは厳密でなくてよい — 高信頼の上限であれば。
type=reservationamount=見積り の行をクレジット台帳に挿入し クレジットを予約。実行残高は確定済みエントリからオープン予約を引いたもの。- 予算強制をチェック — サービスレベル、テナントレベル、ユーザーレベル。予算は予約済額を既に消費済とみなす。
- コミット。
顧客がクレジット不足か予算上限に達している場合、トランザクションはロールバックされ、リクエストはプロバイダーに届かない。呼び出し元は構造化された insufficient_credit または budget_exceeded エラーを見る。
予約行はインタラクション ID と短い TTL を持つ。現実と照合されるプレースホルダー。
Phase 2: Settle (Tx2)
上流呼び出しが完了。プロバイダーのレスポンスにはトークン利用量が含まれる。SchneeAI は実際コストを知る。
2番目の Postgres トランザクション:
type=chargeと実際コスト (プロバイダーのトークン数 × モデルのトークン単価) で 確定済みエントリを挿入。- 予約行を削除 — 同等には
release補正エントリを挿入。削除を選んだのは、予約は定義上エフェメラルで、テーブルに存在することはリクエストが進行中を意味するから。 - 実際のトークン数とコストで 利用レコードを更新。
- コミット。
台帳が残高の真実源。利用レコードは なぜ 残高が変わったかの真実源。
なぜこの分割が重要か
2つのトランザクション、間に外部 LLM 呼び出し、は1トランザクションより複雑。なぜ面倒か?
LLM 呼び出しを Postgres トランザクション内に入れられないから。 30秒のストリーミングレスポンスは30秒間トランザクションを開いたままにし、ロックを保持しコネクションプールを枯渇させる。同時リクエストを掛ければ DB は倒れる。
失敗が非対称だから。 上流呼び出しは失敗、タイムアウト、部分出力、顧客が拒否する出力を返しうる。各ケースに異なる財務的帰結がある。
| 失敗モード | 予約 | 課金 | 起きること |
|---|---|---|---|
| プロバイダー成功 | 解放 | 挿入 | 顧客が実際コストを支払う |
| 生成前にプロバイダーエラー | 解放 | 挿入されず | 顧客は支払わず |
| ストリーム途中タイムアウト | 解放 | 部分挿入 | 報告されれば生成トークン分を支払う |
| プロバイダー成功後にネットワーク切断 | 解放 | 照合で挿入 | 顧客は支払う; 残高は後で照合 |
| Tx1 と Tx2 の間で Gateway クラッシュ | TTL 後期限切れ | プロバイダー成功が確認されれば照合で挿入 | 予約は自動解放; 課金は後で挿入される場合あり |
2相パターンがこれらのケースを扱えるようにする。「呼び出し後に課金」の1相モデルは、DB 利用可能までレスポンスをブロックする (ひどいレイテンシ) か、一部の課金を失うことを許容する (ひどい収益) のいずれか。
冪等性
クレジット台帳には idempotency_key 列がありユニークインデックス付き。キーはインタラクション ID と操作タイプから派生するので、同じ論理操作の再試行はユニーク制約に当たり安全に拒否される。
これが重要なのはリトライがどこにでもあるから:
- Gateway は一時的なプロバイダー失敗をリトライする。リトライは新しい ID の新しいインタラクション — 2つの予約、2つの潜在的課金、しかしプロバイダー成功は1つだけ。照合ワーカーが重複を見つけ孤立課金を逆転。
- クライアントはネットワークドロップ後にリトライする。同じ論理: クライアントリクエストキーでの冪等性は2回目の試行を no-op に。
- 照合ワーカー自身もリトライする。冪等挿入なので安全に再処理できる。
台帳レベルの冪等性が、上流コンポーネントのいずれにも exactly-once 配信を要求せずに exactly-once 課金セマンティクスを堅牢にする。
制御レイヤーとしての予算
予算はウィンドウ上のソフト上限: 1日、1月、フィーチャー毎。クレジット台帳の上のレイヤー。
予約が作られると予算カウンタがインクリメントされる。予約が決済されると見積りと実際の差分で予算カウンタが調整される。予約が解放されると予算カウンタがデクリメントされる。
予算には閾値アクションがある:
- 50% / 80% / 100% で通知 — メールまたは webhook。
- 90% でスロットル — 低優先度リクエストの拒否を開始。
- 100% でハードストップ — 全リクエストが
budget_exceededを返す。
ハードストップはクレジット不足と同じコードパスを使う。呼び出し元から見ればエラーは同じ — 違いは上限が財務的 (お金なし) か運用的 (今月このフィーチャーに予算なし) か。
興味深い設計選択: 予算は決済時の 実際 コストではなく予約時の 見積り コストでチェックする。これは一貫して見積りを下回るワークロードが徐々に上限を超えることを意味する。これを受け入れる — 予算はハード壁ではなくソフト制御。ハード壁が必要な顧客は実際の天井より低く閾値を設定する。
返金、修正、append-only ルール
クレジット台帳は append-only。台帳行への UPDATE はない。修正は新しい行:
type=refund負の金額 — 課金を逆転。type=adjustment正負の金額 — 運用の手動修正。type=grant正の金額 — 月次クレジット付与、プロモーション等。
各エントリは訂正する元エントリを、理由とオペレーター ID 付きで参照。現在残高は常に SUM(amount) WHERE account_id = $1。ドリフトするキャッシュ残高列はない。
このパターン — 財務会計からの流用 — は重要な3つの性質を持つ:
- 監査可能性。 顧客の残高への全変更はタイムスタンプ・理由・アクター付きの行。「残高は X だった、今は Y」ではなく、それを証明するエントリの連鎖。
- 再現性。 現在残高はエントリ履歴の関数。スキーマが変わっても再再生可能。
- ドリフトなし。 キャッシュ残高列は最終的にエントリ合計から逸脱する、大抵は請求サイクル締めの午前3時。Append-only はドリフトするものを置かない。
コストは残高読み取り毎の SUM()。account_id の部分インデックスとホットリード用マテリアライズドビューで、SchneeAI のスケールでは十分に高速。遥かに大規模になれば次は定期スナップショットテーブル + 差分計算。
照合ワーカー
リアルタイムシステムはすべての課金を初回で正しくはしない。SchneeAI は照合ワーカーを実行:
- プロバイダー呼び出しの Gateway 記録をプロバイダーの利用量 API と比較。
- 不一致を発見: Gateway が課金を記録したがプロバイダーが異なる利用量を報告、Gateway が課金前にクラッシュ、プロバイダーが Gateway が見ていない利用量を報告。
type=adjustmentとreason=reconciliationメモ付きで修正エントリを挿入。
ワーカーは冪等で時間毎に実行。2相パターンが捉えないケースのセーフティネット — 主に Gateway と DB 間、または Gateway とプロバイダー間のネットワーク分断。
照合は、プロバイダーの報告利用量が事後で 減少 する稀なケース (中断された生成に対して下方修正するプロバイダーあり) も捉える。負の調整は自然に流れる。
ゼロから構築する人に伝えること
- 整数を使う。
BIGINTとしての micro-USD。整数演算をベンチマークして不十分と分かるまでNUMERICに手を伸ばさない。 - 予約を課金から分離。 外部呼び出しが今日高速で信頼できても、パターンは同じで前もって構築するコストは小さい。後からのレトロフィットはデータ移行を意味する。
- Append-only 台帳、常に。 キャッシュ残高はドリフトする。残高列を置かない。
- 全書き込みに冪等キー。 リトライはどこにでもある; DB でデデュープ。
- 必要になる前に照合ワーカーを構築。 課金が消え始める時点で既に金と信頼を失っている。
- 予算はソフト、クレジットはハード。 予算に支払いクリティカルなパスをブロックさせない。クレジット台帳が真実源; 予算は使い勝手レイヤー。
2相 reserve-and-settle パターンは新規性なし — 決済プロセッサが何十年も前から使ってきたもの。AI に特有なのは ばらつき: 見積りと実際のギャップが 100倍になりうる、呼び出しに 30秒かかりうる、失敗モードが HTTP エラーコードより奇妙。台帳設計はペニーを失わずにこれらすべてを吸収しなければならない。
クレジット台帳は SchneeAI が同梱する運用プリミティブの一つ。Gateway・予算・照合との統合は プロダクト概要 で、課金要件についての会話は お問い合わせ で。