DEV Community

Cover image for OpenAI API利用料金を機能別に追跡する方法:コスト配分プレイブック
Akira
Akira

Posted on • Originally published at apidog.com

OpenAI API利用料金を機能別に追跡する方法:コスト配分プレイブック

OpenAIの請求書には、先月4,237ドル使ったと書かれています。しかし、そのうち3,100ドルは暴走した要約エンドポイントから、700ドルは月に50ドル支払っている顧客から、437ドルは誰も使わない機能から発生したことは書かれていません。ダッシュボードでは、価格設定、キャパシティ、ロードマップの判断に必要な情報が隠れています。

今すぐApidogを試す

このガイドでは、OpenAI APIのコストを機能、ルート、顧客、環境ごとに割り当てる実装方法を説明します。すべてのリクエストにメタデータを付与し、トークン数とコストを構造化ログとして出力し、ウェアハウスで集計し、予算上限とアラートを設定します。

💡 Apidogは、コスト追跡ラッパーを本番環境に出す前に、リクエストレベルの可視性とシナリオテストを提供します。タグ付きリクエストの再生、ログ形式のアサート、すべての呼び出しがウェアハウスの期待するメタデータを持つことの検証に使えます。

TL;DR

OpenAI API呼び出しごとに、以下を必ず記録します。

  • feature
  • route
  • customer_id
  • environment
  • model
  • トークン数
  • 計算済みのcost_usd

そのうえで、ウェアハウスでタグごとに集計し、OpenAI側ではキーごとの予算上限を設定します。さらに、時間ごとの支出異常を検知し、リリース前にApidogのシナリオテストでラッパーを検証します。

はじめに

火曜日に新しいAI機能をリリースしました。金曜日の朝、CFOから「OpenAIの利用料が40%も跳ね上がったのはなぜだ」とDMが来ます。OpenAIダッシュボードを見ると、合計支出が増えていることは分かります。しかし、どの機能、どの顧客、どのルートが原因かは分かりません。

これは、本番環境でLLMワークロードを運用するチームが必ず直面する問題です。OpenAIの請求インターフェースは経理向けであり、エンジニアリングやプロダクトの帰属分析向けではありません。

この記事では、次を実装します。

  1. OpenAIクライアントのラッパー
  2. コスト帰属用のイベントスキーマ
  3. トークン数からのコスト計算
  4. 構造化ログ出力
  5. ウェアハウス集計SQL
  6. ApidogによるE2E検証
  7. 予算上限と異常検知

価格計算の前提については、GPT-5.5の価格内訳を参照してください。開発者ツール側の請求帰属については、APIチーム向けのGitHub Copilot利用料請求も参考になります。OpenAI APIの基本は公式のOpenAI APIリファレンスを確認してください。

OpenAIの課金ダッシュボードでは不十分な理由

OpenAIの課金ページでは、主に次が確認できます。

  • 日別の支出
  • モデル別の使用量
  • 組織レベルの使用制限

単一アプリ、単一機能、単一顧客なら十分です。しかし、実際のプロダクトでは複数の機能、顧客、環境、開発者が同じOpenAI組織を使います。

不足する情報は次のとおりです。

コンテキストのない総支出

ダッシュボードに「昨日GPT-5.5に312ドル使った」と表示されても、それがサポートチャットの大量呼び出しなのか、バックグラウンド要約ジョブの暴走なのかは分かりません。

機能ごとの内訳がない

OpenAIはAPIキーやモデル単位では集計できますが、あなたのプロダクト上のfeatureroutecustomer_idenvironmentでは集計しません。

レポートに遅延がある

使用状況データは数十分から数時間遅れて表示されます。暴走ループの検知には遅すぎます。

アラートが粗い

OpenAI側の予算通知だけでは、「チャット機能が1時間に50ドルを超えたら通知する」といった制御はできません。

顧客帰属がない

B2B SaaSでAI機能を提供している場合、顧客ごとのAI原価を把握しないと粗利益を計算できません。

プロジェクトキーだけでは粒度が足りない

OpenAIのプロジェクトキーは有用ですが、機能、顧客、ルート単位の帰属には不十分です。OpenAI usage APIも、基本的には集計済みデータを返すため、リクエスト単位のメタデータはアプリケーション側で持つ必要があります。

この問題は多くのチームに共通しています。Dev.toでも「OpenAIはいくら使ったかは教えてくれる。どこで使ったかは教えてくれない」という文脈で議論されています。

コスト帰属のデータモデル

まず、OpenAIリクエスト1回につき1つのイベントを記録します。このイベントが分析単位です。

最小スキーマは次のとおりです。

カラム 目的
request_id uuid 7a91... 冪等性、重複排除、リトライ
timestamp timestamptz 2026-05-06T14:23:01Z 時系列分析、異常検知
feature text support-chat 呼び出し元のプロダクト機能
route text /api/v1/chat/answer HTTPルートまたはジョブID
customer_id text cust_4291 顧客ごとの支出
environment text prod 本番、ステージング、開発の分離
model text gpt-5.5 モデル別価格計算
prompt_tokens int 15234 入力トークン数
completion_tokens int 812 出力トークン数
reasoning_tokens int 4500 推論トークン
cached_tokens int 12000 キャッシュ済み入力トークン
latency_ms int 2341 レイテンシ分析
cost_usd numeric 0.045672 書き込み時に計算したコスト
prompt_cache_key text system-v3 キャッシュヒット率の追跡
error_code text null / 429 エラーとリトライ分析

重要なのは、cost_usdをクエリ時ではなく書き込み時に計算することです。価格は変更されるため、履歴イベントには「その時点のレート」で計算した値を固定して保存します。

コスト計算を実装する

GPT-5.5系の価格表をコードに固定します。

PRICING = {  # USD per 1M tokens, as of May 2026
    "gpt-5.5":      {"input": 5.00,  "cached": 2.50,  "output": 30.00},
    "gpt-5.5-pro":  {"input": 30.00, "cached": 15.00, "output": 180.00},
    "gpt-5.4":      {"input": 2.50,  "cached": 1.25, "output": 15.00},
    "gpt-5.4-mini": {"input": 0.25,  "cached": 0.125, "output": 2.00},
}

def compute_cost_usd(
    model,
    prompt_tokens,
    cached_tokens,
    completion_tokens,
    reasoning_tokens
):
    rates = PRICING[model]

    uncached = max(0, prompt_tokens - cached_tokens)

    input_cost = (uncached * rates["input"]) / 1_000_000
    cache_cost = (cached_tokens * rates["cached"]) / 1_000_000
    output_cost = (
        (completion_tokens + reasoning_tokens) * rates["output"]
    ) / 1_000_000

    return round(input_cost + cache_cost + output_cost, 6)
Enter fullscreen mode Exit fullscreen mode

推論トークンは出力として扱います。OpenAI APIではusage.completion_tokens_details.reasoning_tokensとして返されますが、課金上は出力レートです。ここを間違えると、Thinking系の呼び出しコストを過小評価します。

詳細な価格計算はGPT-5.5の価格内訳を参照してください。

OpenAIクライアントをラップする

すべてのOpenAI呼び出しを1つの関数に集約します。

import time
import uuid
import json
import logging
from openai import OpenAI

client = OpenAI()
logger = logging.getLogger("llm.cost")

def call_with_attribution(
    *,
    feature,
    route,
    customer_id,
    environment,
    model,
    messages,
    **openai_kwargs
):
    if not feature or not route or not customer_id or not environment:
        raise ValueError("feature, route, customer_id, environment are required")

    request_id = str(uuid.uuid4())
    started = time.time()
    error_code = None
    response = None

    try:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            **openai_kwargs
        )
        return response

    except Exception as e:
        error_code = getattr(e, "code", "unknown_error")
        raise

    finally:
        latency_ms = int((time.time() - started) * 1000)

        u = response.usage if response else None

        prompt_tokens = getattr(u, "prompt_tokens", 0)
        completion_tokens = getattr(u, "completion_tokens", 0)

        cached_tokens = (
            getattr(
                getattr(u, "prompt_tokens_details", None),
                "cached_tokens",
                0
            ) or 0
        )

        reasoning_tokens = (
            getattr(
                getattr(u, "completion_tokens_details", None),
                "reasoning_tokens",
                0
            ) or 0
        )

        cost_usd = compute_cost_usd(
            model,
            prompt_tokens,
            cached_tokens,
            completion_tokens,
            reasoning_tokens
        )

        logger.info(json.dumps({
            "event": "openai.request",
            "request_id": request_id,
            "feature": feature,
            "route": route,
            "customer_id": customer_id,
            "environment": environment,
            "model": model,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "reasoning_tokens": reasoning_tokens,
            "cached_tokens": cached_tokens,
            "latency_ms": latency_ms,
            "cost_usd": cost_usd,
            "error_code": error_code,
        }))
Enter fullscreen mode Exit fullscreen mode

このラッパーを、コスト帰属の唯一の入口にします。

やることは明確です。

  1. コードベースでOpenAI(を検索する
  2. client.chat.completions.createの直接呼び出しを禁止する
  3. すべてcall_with_attribution(...)に置き換える
  4. featureroutecustomer_idenvironmentを必須にする
  5. 不明な値をunknownで埋めず、呼び出し時に失敗させる

Node.jsでも構造は同じです。OpenAI SDKを関数で包み、response.usageを読み取り、JSONイベントを書き込みます。Kafka、NATS、Pub/Subなどのイベントバスがある場合は、stdoutではなくそこに発行しても構いません。

コスト追跡を構築し、Apidogでテストする

実装手順は6ステップです。

1. 直接のOpenAI呼び出しをラッパーに置き換える

コードベースで次を検索します。

grep -R "OpenAI(" .
grep -R "chat.completions.create" .
Enter fullscreen mode Exit fullscreen mode

見つかった呼び出しをすべてcall_with_attribution(...)に置き換えます。

呼び出し例:

response = call_with_attribution(
    feature="support-chat",
    route="/api/v1/chat/answer",
    customer_id=current_user.customer_id,
    environment="prod",
    model="gpt-5.5",
    messages=[
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_question},
    ],
)
Enter fullscreen mode Exit fullscreen mode

2. 構造化ログを出力する

各イベントは1行のJSONで出力します。

{
  "event": "openai.request",
  "request_id": "7a91...",
  "feature": "support-chat",
  "route": "/api/v1/chat/answer",
  "customer_id": "cust_4291",
  "environment": "prod",
  "model": "gpt-5.5",
  "prompt_tokens": 15234,
  "completion_tokens": 812,
  "reasoning_tokens": 4500,
  "cached_tokens": 12000,
  "latency_ms": 2341,
  "cost_usd": 0.045672,
  "error_code": null
}
Enter fullscreen mode Exit fullscreen mode

このログを既存のパイプラインでBigQuery、ClickHouse、Snowflake、Postgresなどに送ります。

3. ウェアハウスで機能ごとに集計する

SELECT
  feature,
  DATE_TRUNC(timestamp, DAY) AS day,
  COUNT(*) AS requests,
  SUM(cost_usd) AS spend_usd,
  SUM(prompt_tokens + completion_tokens) AS tokens,
  AVG(latency_ms) AS avg_latency_ms,
  SUM(cached_tokens) / NULLIF(SUM(prompt_tokens), 0) AS cache_hit_rate
FROM openai_events
WHERE environment = 'prod'
  AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY feature, day
ORDER BY day DESC, spend_usd DESC;
Enter fullscreen mode Exit fullscreen mode

次のビューを作ると運用しやすくなります。

  • 機能ごとの日次支出
  • 顧客ごとの日次支出
  • ルート別の上位支出
  • モデル別の支出
  • キャッシュヒット率
  • 平均プロンプトトークン数
  • 平均出力トークン数

4. ルートごとの支出をグラフ化する

Grafana、Metabase、Looker、Supersetなどで可視化します。

最低限、次の3つは作ってください。

  1. 機能別支出の時系列
  2. 顧客別支出の時系列
  3. 昨日の支出が多い上位20ルート

これが、OpenAIダッシュボードの代わりに毎日見る運用ダッシュボードになります。

5. リリース前にApidogでラッパーをテストする

ラッパーのバグは静かにダッシュボードを壊します。特に危険なのは、ログが出ているように見えて、customer_idfeatureが欠落している状態です。

Apidogで次を検証します。

  1. 既知のcustomer_idfeatureを持つリクエストをAIエンドポイントに送る
  2. レスポンスを検証する
  3. stdout、OTLP、ログエンドポイントなどのサイドチャネルを確認する
  4. ログペイロードにfeatureroutecustomer_idが含まれることをアサートする
  5. cost_usd > 0prompt_tokens > 0をアサートする
  6. ステージングと本番で同じシナリオを実行する
  7. リトライ時にコストが二重計上されないことを確認する

APIテスト全般については、QAエンジニア向けのAPIテストツールを参照してください。契約優先でAPIを設計する場合は、契約優先API開発も参考になります。

6. キーごとの予算上限とアラートを設定する

OpenAI側では、環境や主要機能ごとにプロジェクトキーを分けます。

例:

  • prod-support-chat
  • prod-summarization
  • prod-agent
  • staging-all

それぞれにOpenAIダッシュボードで上限を設定します。

ただし、ネイティブの上限だけでは不十分です。ウェアハウス側でも異常検知します。

例:10分ごとに実行する監視SQL

WITH hourly AS (
  SELECT
    feature,
    TIMESTAMP_TRUNC(timestamp, HOUR) AS hour,
    SUM(cost_usd) AS spend_usd
  FROM openai_events
  WHERE environment = 'prod'
    AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 8 DAY)
  GROUP BY feature, hour
),
baseline AS (
  SELECT
    feature,
    AVG(spend_usd) AS avg_hourly_spend
  FROM hourly
  WHERE hour < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
  GROUP BY feature
),
current_hour AS (
  SELECT
    feature,
    SUM(cost_usd) AS current_spend
  FROM openai_events
  WHERE environment = 'prod'
    AND timestamp >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), HOUR)
  GROUP BY feature
)
SELECT
  c.feature,
  c.current_spend,
  b.avg_hourly_spend
FROM current_hour c
JOIN baseline b USING (feature)
WHERE c.current_spend > b.avg_hourly_spend * 3;
Enter fullscreen mode Exit fullscreen mode

結果が返ったらSlack、PagerDuty、Opsgenieなどに通知します。

ネイティブ上限は最後の防衛線、ウェアハウスアラートは早期検知です。

高度なテクニック

プロンプトキャッシングを前提にプロンプトを設計する

GPT-5.5では、キャッシュされたトークンは入力レートの50%で課金されます。システムプロンプトを安定したプレフィックスとして配置し、リクエストごとの変数を末尾に置きます。

追跡すべき指標:

SELECT
  feature,
  SUM(cached_tokens) / NULLIF(SUM(prompt_tokens), 0) AS cache_hit_rate
FROM openai_events
WHERE environment = 'prod'
GROUP BY feature
ORDER BY cache_hit_rate ASC;
Enter fullscreen mode Exit fullscreen mode

公式のOpenAIプロンプトキャッシングドキュメントも確認してください。

オフライン処理はBatch APIに寄せる

同期応答が不要な処理はBatch APIに回します。

対象例:

  • 夜間要約
  • 評価実行
  • 埋め込みのバックフィル
  • ドキュメント再処理

Batch呼び出しにも同じコスト帰属を適用し、イベントにbatch_job_idを追加します。

推論努力をチューニングする

GPT-5.5 Thinkingでは、reasoning.effortによって推論トークンが変わります。mediumで動かしている機能が、lowでも品質基準を満たすか確認してください。

やること:

  1. reasoning.effort別にA/Bテストする
  2. 品質指標を比較する
  3. cost_usdを比較する
  4. 品質が維持される最安設定を採用する

詳細はGPT-5.5 APIの使用方法を参照してください。

コンテキストウィンドウを管理する

プロンプトが長いほどコストは増えます。RAGでは、知識ベース全体を入れるのではなく、取得件数とトークン予算を明示的に制限します。

監視SQL:

SELECT
  feature,
  DATE_TRUNC(timestamp, WEEK) AS week,
  AVG(prompt_tokens) AS avg_prompt_tokens
FROM openai_events
WHERE environment = 'prod'
GROUP BY feature, week
ORDER BY week DESC, avg_prompt_tokens DESC;
Enter fullscreen mode Exit fullscreen mode

機能変更がないのにavg_prompt_tokensが増えている場合、プロンプトが肥大化しています。

GPT-5.5の272Kトークンクリフを監視する

GPT-5.5では、272Kトークンを超えるリクエストに対して、入力に2倍、出力に1.5倍の乗数が適用されます。

ラッパーにガードを追加します。

if prompt_tokens > 250_000:
    logger.warning(json.dumps({
        "event": "openai.prompt_token_warning",
        "request_id": request_id,
        "feature": feature,
        "route": route,
        "customer_id": customer_id,
        "prompt_tokens": prompt_tokens,
    }))
Enter fullscreen mode Exit fullscreen mode

価格の詳細はGPT-5.5の価格に関する投稿を参照してください。

顧客ごとの支出上限を設定する

B2B SaaSでは、顧客ごとのAI原価を制御する必要があります。

実装方針:

  1. ウェアハウスまたは高速ストアでcustomer_idごとの月次支出を集計
  2. 各OpenAI呼び出し前に上限をチェック
  3. 上限超過時は429を返す
  4. レスポンスに課金CTAを含める

例:

def assert_customer_budget(customer_id):
    spend = get_monthly_ai_spend(customer_id)
    limit = get_customer_ai_limit(customer_id)

    if spend >= limit:
        raise AIQuotaExceeded(
            "月間AIクォータを超過しました。プランのアップグレードを検討してください。"
        )
Enter fullscreen mode Exit fullscreen mode

避けるべきミス

  • 推論トークンを入力として課金する
  • リアルタイム監視にOpenAIダッシュボードだけを使う
  • 呼び出しサイトではなくSDKレベルで雑にタグ付けする
  • cron、キューワーカー、Webhookのタグ付けを忘れる
  • リクエストログをサンプリングする
  • customer_idnullのままにする
  • リトライ時にrequest_idを再利用せず二重計上する

バックグラウンドジョブには、次のような合成routeを付けます。

  • cron:nightly-summarize
  • queue:image-caption
  • webhook:customer-import

customer_idが存在しない内部処理では、nullではなくinternalsystemを使います。

代替手段とツール

自前実装以外の選択肢もあります。

アプローチ 得意な点 コスト 向いているケース
OpenAI usage API ネイティブ、セットアップ不要 無料 1プロジェクト、1機能、顧客帰属不要
Helicone ドロップインプロキシ、ダッシュボード、キャッシュ 無料枠あり、月額20ドル〜 早く可視化したい、プロキシを許容できる
Langfuse OSS、セルフホスト、トレース + コスト セルフホスト無料、クラウド月額29ドル〜 トレースとコストを一体で管理したい
LangSmith LangChain統合、評価 + コスト 月額39ドル/ユーザー〜 LangChainをすでに使っている
カスタムウェアハウス 完全制御、既存スタックに統合 エンジニアリング時間 大規模、独自ディメンション、データ所在地要件あり

プロキシ型のHeliconeは導入が速い一方、クリティカルパスにホップが増えます。Langfuseは制御しやすいですが、セルフホストする場合は運用が必要です。カスタムウェアハウスは実装コストがありますが、大規模チームでは最終的にこの形に寄ることが多いです。

LLMコスト可観測性の実装例として、HeliconeチームのLLMコスト追跡に関するガイドLangfuseのコスト追跡に関するドキュメントも参考になります。

プラットフォーム規模でこのパターンを運用する場合は、マイクロサービスアーキテクチャのためのAPIプラットフォームも参照してください。

実世界のユースケース

顧客ごとのLLM支出を持つB2B SaaS

あるセールスインテリジェンス製品では、顧客がブリーフィングを要求するたびにGPT-5.5呼び出しが発生します。帰属なしでは、月8万ドルのOpenAI支出しか分かりません。

顧客ごとの帰属を入れると、顧客の12%が支出の71%を占めていることが分かりました。そこで段階的価格、ソフトクォータ、超過料金を導入し、AI機能の粗利益を改善できます。

社内開発ツールの追跡

エンジニア向けの社内GPTアシスタントでも同じです。customer_idに開発者メールを入れると、誰がどれだけ使っているかが分かります。

異常な支出を見つけることで、放置された自動エージェントループを停止できます。一方、正当な高利用者にはより高いクォータを割り当てる判断もできます。

AI機能の支出予測

新しい要約機能を出す前に、過去の機能別データから次を見積もります。

  • 呼び出しあたりの平均入力トークン
  • 呼び出しあたりの平均出力トークン
  • アクティブユーザーあたりの想定呼び出し回数
  • 想定アクティブユーザー数

これにより、機能単位の原価を事前に計算できます。価格設定やリリース可否の判断が推測ではなくなります。

結論

測定できないものは管理できません。OpenAIの課金ダッシュボードは財務上の合計金額を示しますが、プロダクト運用には機能、顧客、ルートごとの帰属が必要です。

実装すべきことはシンプルです。

  • すべてのリクエストにfeatureroutecustomer_idenvironmentを付ける
  • OpenAIクライアントをラッパー経由に統一する
  • トークン数とcost_usdを構造化ログで出力する
  • ウェアハウスで集計する
  • OpenAIプロジェクトキーごとに上限を設定する
  • ウェアハウス側で異常検知する
  • リリース前にApidogでラッパーを検証する

Apidogをダウンロードして、コスト帰属ラッパーのE2E検証に使ってください。タグ付きリクエストでAIエンドポイントを実行し、ログペイロードの形状をアサートし、複数環境でシナリオを再生できます。

関連する読み物:

よくある質問

推論トークンは入力として課金されますか?出力として課金されますか?

出力レートで課金されます。OpenAI APIではusage.completion_tokens_details.reasoning_tokensとして返されるため、completion_tokensに加算してコスト計算してください。詳細はGPT-5.5の価格内訳を参照してください。

response.usageはOpenAIダッシュボードと一致しますか?

トークン数はダッシュボードと一致します。ただし、古い料金表でコストを計算していると、価格変更によってずれます。モデルごとのレートはコードまたは設定で固定し、価格変更日に更新してください。

OpenAIのプロジェクトキーだけで帰属できますか?

一部は可能です。プロジェクト単位の帰属や予算上限には有効です。ただし、機能、顧客、ルート単位の帰属にはアプリケーションレベルのメタデータが必要です。

リトライでコストが二重計上されませんか?

モデル実行前に失敗したリクエストは通常usageを返さないため、コストは記録されません。成功後にアプリケーション側でリトライすると、request_idを再利用しない限り二重計上されます。冪等なリトライでは同じrequest_idを使い、書き込み時に重複排除してください。

OpenAI usage APIはリアルタイム監視に使えますか?

リアルタイム監視には不向きです。数十分の遅延があります。アラートやキルスイッチには自分のログとウェアハウスを使い、月次調整にはusage APIを使うのが現実的です。

ログ量を減らすためにサンプリングしてもよいですか?

いいえ。リクエストごとに1行のJSONで済むため、データ量は小さいです。サンプリングすると顧客別、ルート別の正確な帰属が壊れます。すべて記録してください。

他のLLMプロバイダーにも使えますか?

使えます。providerカラムを追加し、openaianthropicgoogledeepseekなどを入れます。プロバイダーごとに料金表とラッパーは変わりますが、ウェアハウスのスキーマとダッシュボードは共通化できます。比較としてDeepSeek V4 APIの価格設定も参照してください。

埋め込みや画像生成にも使えますか?

使えます。ただし、コスト計算はエンドポイントごとに分岐します。埋め込みは入力トークン単位、画像生成は画像枚数や解像度単位で計算します。スキーマにendpointを追加し、chatembeddingsimageなどで分けてください。

Top comments (0)