結論から言うと、マルチテナントの AI プロダクトで顧客ごとにモデル使用量を課金する最もシンプルで確実な方法は、テナント(またはテナントのプラン)ごとに専用の API キーを発行することです。そのキーに USD の利用上限と同時実行数の上限を設定し、トークン数から推計するのではなく、ゲートウェイがリクエストごとに返す実際の請求額を記録します。HopBase では、課金対象となった非ストリーミングのレスポンスごとに X-HopBase-Cost-USD と usage.cost_usd が付与されます。完了した動画・画像タスクごとに usage.cost_usd が返され、GET /v1/usage ではキーの残り利用可能額が返されます。テナントの上限に達すると、そのテナントのリクエストには 402 insufficient_quota が返りますが、他のテナントは引き続き利用できます。同時実行数の上限を超えたキーには 429 apikey_concurrency_limit が返るため、一部の騒がしい顧客がキャパシティを使い切ってしまうことはありません。ストリーミングのレスポンスにはコストヘッダーが付かないため、キーごとの CSV エクスポートで照合します。CSV のすべての行は再計算可能です。
課題:プロバイダーの請求書は 1 枚、顧客は多数
AI エージェントプラットフォーム、AI 機能を備えたバーティカル SaaS、あるいはホワイトラベルの AI サービスを運営している場合、モデルプロバイダーからの請求書は 1 枚です。一方で、顧客はそれぞれ自分専用の請求書を求めます。この 2 つの間には、どのチームもいずれ答えを出すことになる一連の問いがあります:
- 今月、テナント A にはモデル別にいくらかかったのか?
- あるテナントのエージェントループが暴走して、全員の予算を使い果たすのをどう防ぐか?
- 自社のダッシュボードで顧客に残りクレジットをどう表示するか?
- 経理から求められたとき、請求書の各行を裏付けられるか?
トークン数だけではこれらに答えられません。価格はモデルによって異なり、キャッシュされた入力は安く、推論トークンを出力として課金するモデルもあれば、コンテキストがしきい値を超えると高くなるモデルもあり、動画は秒単位や動画トークン単位で課金されます。トークン数に自前の価格表を掛け合わせる方式では、新しいモデルが出るたびにその価格表が古くなっていきます。
3 つのアーキテクチャの比較
| 方式 | テナントの分離方法 | コストの数値の出どころ | 制限の適用 |
|---|---|---|---|
| リクエストにタグを付け、自前で台帳を管理 | アプリ内のテナント ID。プロバイダーのキーは 1 本を共有 | 自前の価格表 × トークン数 | 自社コードで、各呼び出しの前に |
| セルフホストのプロキシ | プロキシ内の仮想キーまたは顧客 ID フィールド | プロキシの価格表 | プロキシ |
| 実際の請求額を返すゲートウェイでテナントごとにキーを発行 | テナントまたはプランごとにゲートウェイのキーを 1 本 | リクエストごとのゲートウェイの請求額 | ゲートウェイ:キーごとの利用上限と同時実行数の上限 |
3 つともうまく機能します。このガイドの残りでは 3 つ目を扱います。最も難しい 2 つの部分、つまり価格表を正しく保つことと、リクエストを送る前に制限を適用することが不要になるからです。
ステップ 1:テナントごとに上限付きのキーを 1 本
HopBase のコンソールで APIキー を開き、テナントごとにキーを作成します。キーの作成と編集は公開 API ではなくコンソールで行うため、キーの作成をテナントのオンボーディング手順の一つに組み込んでください。キーごとの 3 つの設定が、ほとんどの役割を担います:
- グループは、そのキーで呼び出せるモデルを決めます。1 本のキーにはモデルファミリーをまたいで 1〜10 個のグループを含められるため、「Basic」テナントには DeepSeek と Qwen だけを、「Pro」テナントには Claude と動画も提供する、といった設定ができます。キーのグループに含まれないモデルを呼び出すと 404
model_not_foundが返ります。 - クォータは、キーの USD 建て利用上限です。使い切ると、そのキーのリクエストには「API key quota exhausted」とともに 402
insufficient_quotaが返ります。アカウント内の他のキーには影響しません。 - 同時実行数は、このキーで同時に処理中にできるリクエストの数です。上限を超えると、リクエストには
Retry-After: 1とともに 429apikey_concurrency_limitが返ります。空欄にするとキー単位の上限はなくなります。その上にアカウントの上限も適用されます。デフォルトはアカウントあたり同時 5 リクエストで、申請により引き上げられます。
キーはテナントテーブルに暗号化して保存し、テナントのブラウザには決して公開しないでください。モデルの呼び出しは、テナントに代わって自社のバックエンドが行います。
ステップ 2:すべてのリクエストで実際の請求額を記録する
同期呼び出し(チャット、画像、音声)では、課金されたレスポンスに残高から差し引かれた金額が返されます:
- ヘッダー
X-HopBase-Cost-USD。例:0.012。 - 非ストリーミングの JSON では、通常のトークン数と並んで、同じ値が
usage.cost_usdとして返されます。
薄いラッパーを 1 つ用意すれば、これを台帳のエントリに変換できます:
import os, sqlite3
from openai import OpenAI
db = sqlite3.connect("ledger.db")
db.execute("""CREATE TABLE IF NOT EXISTS usage_ledger (
ts TEXT DEFAULT CURRENT_TIMESTAMP, tenant_id TEXT, model TEXT,
prompt_tokens INT, completion_tokens INT, cost_usd REAL, request_id TEXT)""")
def client_for(tenant):
# tenant["hopbase_key"] is that tenant's own key, decrypted server-side
return OpenAI(base_url="https://api.hop-base.com/v1", api_key=tenant["hopbase_key"])
def chat(tenant, model, messages, **kw):
raw = client_for(tenant).chat.completions.with_raw_response.create(
model=model, messages=messages, **kw)
resp = raw.parse()
cost = float(raw.headers.get("X-HopBase-Cost-USD") or 0)
db.execute("INSERT INTO usage_ledger (tenant_id, model, prompt_tokens, completion_tokens, cost_usd, request_id) VALUES (?,?,?,?,?,?)",
(tenant["id"], model, resp.usage.prompt_tokens,
resp.usage.completion_tokens, cost, resp.id))
db.commit()
return resp
この数値について、課金上重要なルールがいくつかあります:
- 出力を生成せずに失敗したリクエストは課金されず、コストヘッダーも付きません。
- ゲートウェイが内部でリクエストをリトライした場合、課金されるのは完了した試行のみで、報告されるのもその金額です。複数のグループを持つキーは、リクエストを処理したグループの料金で課金されます。
- クライアントが途中で切断した場合でも、すでに生成された出力は課金されます。その請求はヘッダーを受け取っていなくても、コンソールの使用記録に表示されます。
- ストリーミング(SSE)のレスポンスにはコストヘッダーが付きません。ヘッダーは生成開始前に送信されるためです。また、ストリームの最後の usage イベントにはトークン数はありますが金額はありません。トークン数を記録し(
stream_options: {"include_usage": true}を送信)、保留中のエントリとしておき、金額は CSV エクスポートで確定させてください(ステップ 4)。
ステップ 3:非同期の動画・画像タスク
動画タスクは POST /v1/video/generate で送信し、GET /v1/video/tasks/{task_id} でポーリングします。タスクが最終状態に達すると、単一タスクのクエリ結果にトップレベルの usage オブジェクトが含まれます:
{
"id": "vt-1",
"status": "completed",
"usage": { "cost_usd": 0.5 }
}
タスクが完了したら usage.cost_usd を台帳に書き込みます。課金されなかった失敗タスクは 0 を返します。単一タスクのクエリでは、このフィールドを見られるのはタスクを作成したキーだけです。タスク一覧(ID なしの GET /v1/video/tasks)は、他のキーで作成されたタスクも含めアカウント全体が対象になるため、各タスク ID をテナントと紐づけて保存し、生の一覧をそのままテナントに渡さないでください。また、動画の送信時には見積もりコストが事前に確保されるため、上限に近いテナントは処理の途中ではなく送信時点で 402 を受け取ることがあります。
ステップ 4:照合と請求
請求期間の終わりに、コンソールの使用記録ページをキーと期間で絞り込んで CSV でエクスポートします(1 ファイルあたり最大 50,000 行、400 日)。各行には日時、モデル、使用量、公式定価の通貨、公式コスト、割引率、為替レート、請求額、備考が含まれるため、すべての行を再計算できます:
official cost × discount ÷ exchange rate = charged amount
失敗したリクエストには課金なしと記され、中断されたリクエストにはすでに生成された出力分を課金したと記されます。キーごとに請求額を合計して台帳と照合し、ステップ 2 で保留にしたストリーミングのエントリを確定させてください。コンソールでは、キーごとの利用額とリクエスト数の推移もグラフで確認できます。MCP クライアントで作業している場合は、読み取り専用の get_usage ツールで、呼び出し元キーのリクエスト数、トークン数、コストの合計をモデル別に取得できます。期間はデフォルトで直近 7 日間、最大 92 日間です。
テナントにいくら請求するかは御社の判断です。よくあるのは、原価にマージンを上乗せする方式や、想定コストより高い価格設定のクレジットパックです。台帳には実際の請求額が記録されているので、どちらの方式も掛け算 1 回で済みます。
ステップ 5:テナントに残りクレジットを表示する
ダッシュボードで「残りクレジット」をリアルタイムに表示するには、テナントのキーで GET /v1/usage を呼び出します:
curl https://api.hop-base.com/v1/usage \
-H "Authorization: Bearer sk-tenant-key"
クォータが $10 で $2 使用済みのキーの場合、balance は 8、quota.total は 10、quota.used は 2 で、単位はすべて USD です。キー自体にクォータが設定されていない場合、balance はアカウントの残高になるため、この数値を表示する予定があるなら、すべてのテナントキーにクォータを設定してください。クォータが残っているキーでも、先にアカウント残高が尽きれば 402 で停止します。この呼び出し自体は課金されません。
テナントの境界を尊重したエラー処理
- 402
insufficient_quota:このテナントの上限に達したか、アカウント残高が空です。GET /v1/usageでどちらかを確認し、そのテナントにだけ「クレジットを使い切りました」と表示してください。 - 429
apikey_concurrency_limit:このテナントが自身の同時実行数の上限を超えています。他のテナントを遅くすることなく、このテナントだけをキューに入れるかバックオフしてください。 - 429
user_concurrency_limit:アカウント全体が上限に達しています。テナント単位で対処する問題ではなく、キャパシティについての相談が必要です。 - 404
model_not_found:テナントがプラン外のモデルをリクエストしました。アップグレードの案内につなげてください。
リリース前のチェックリスト
- すべてのテナントキーにクォータと同時実行数の上限が設定されている。
- 台帳への書き込みには返された請求額を使い、ローカルの価格表は決して使わない。
- ストリーミングと切断されたリクエストは CSV エクスポートで確定させる。
- 非同期タスクの請求額は、タスクが最終状態に達した時点で、テナントごとに保存したタスク ID をキーにして書き込む。
- 毎月の照合で、キーごとの台帳の合計をエクスポートと比較する。
よくある質問
価格表を保守せずに顧客ごとの LLM コストを追跡するにはどうすればよいですか?
ゲートウェイが返す請求額を読み取ってください。HopBase では、同期呼び出しなら X-HopBase-Cost-USD ヘッダーまたは usage.cost_usd、完了した非同期タスクなら usage.cost_usd です。いずれも、そのリクエストで差し引かれた正確な USD 金額です。
1 人の顧客が使える金額に上限を設けられますか?
はい。その顧客の API キーにクォータを設定してください。使い切ると、そのキーのリクエストには 402 insufficient_quota が返り、他のキーは引き続き利用できます。
ストリーミングのレスポンスにコストヘッダーがないのはなぜですか?
ヘッダーは生成開始前に送信されるため、その時点では最終的な請求額がまだ確定していないからです。ストリームの最後の usage イベントにあるトークン数を保留中のエントリとして記録し、金額はコンソールの使用記録または CSV エクスポートで確定させてください。
顧客ごとに異なるモデルを提供できますか?
はい。各キーには選んだグループだけを 1〜10 個含められるので、プランはキー自体によって適用されます。キーのグループに含まれないモデルを呼び出すと 404 が返ります。
詳細はどこで確認できますか?
コストのフィールドと /v1/usage についてはアカウント・カタログ API、上限については同時実行数・タイムアウト・課金、CSV による照合方法については使用量照合ガイドをご覧ください。定価は hop-base.com/pricing に掲載しています。