生成AIをWebサービスへ組み込むと、「今月OpenAI APIにいくら使ったか」だけでなく、「このユーザーの1リクエストはいくらだったのか」を確認したくなることがあります。
ユーザーごとの原価、機能ごとの採算、モデル変更前後の料金差を調べるには、APIレスポンスに含まれるトークン使用量をリクエスト単位で保存する必要があります。
ただし、単純にinput_tokens + output_tokensだけを保存する設計では不十分です。
現在のAI APIでは、通常の入力、キャッシュから読み込んだ入力、キャッシュへ書き込んだ入力、推論トークンなどで料金が異なる場合があります。
さらに、モデル価格そのものも変更されます。
2026年8月9日時点では、Claude Sonnet 5に8月31日までの期間限定料金が設定されており、9月1日から単価が変わる予定です。過去のトークン数だけをDBへ保存し、あとから最新料金を掛ける設計では、過去の原価を正しく再現できません。
そのため、本番環境では「トークン使用量」と「そのリクエストに適用した料金単価」の両方を保存します。
この記事では、OpenAI、Claude、Gemini APIの料金をリクエスト単位で計算し、PostgreSQLへ保存する方法をTypeScriptで解説します。
- AI APIの基本的な料金計算
- キャッシュを使うと計算式が変わる
- OpenAIのreasoning_tokensを二重計上しない
- トークン使用量だけでなく料金単価も保存する
- 浮動小数点数だけで料金を保存しない
- 共通の使用量形式を作る
- 料金スナップショットの型を作る
- Decimalで料金を計算する
- OpenAI Responses APIの使用量を変換する
- GPT-5.6 Lunaの料金設定例
- Claudeはinput_tokensだけを見ると料金を間違える
- Claudeの使用量を共通形式へ変換する
- Claude Sonnet 5の料金設定例
- Geminiではthinking tokensも出力料金へ含まれる
- Geminiの使用量を共通形式へ変換する
- Gemini 3.5 Flashの料金設定例
- DBには生のUsageも保存する
- user_idには個人情報そのものを保存しない
- リクエスト完了後にUsageを保存する
- DBへ保存するTypeScript実装
- 同じAPIリクエストを二重登録しない
- 失敗したリクエストも記録する
- リトライしたAPI料金もすべて保存する
- ユーザーごとの料金を集計する
- 機能ごとの料金を集計する
- 1リクエストの平均料金だけでは不十分
- キャッシュ率と料金を一緒に見る
- Price MasterとPrice Snapshotを分ける
- BatchやService Tierも保存する
- Web Searchなどのツール料金は別に加算する
- calculated_costと実際の請求額を区別する
- API料金と売上を同じ画面で比較する
- 料金アラートを設定する
- 日次・月次の予算をアプリ側でも管理する
- ストリーミングでも最終Usageを保存する
- プロンプトの文字数から料金を推測しない
- 料金表を自動取得するだけに依存しない
- AI API料金を確認するときの基本設計
- AI APIの料金計算に関するよくある質問
- まとめ
AI APIの基本的な料金計算
テキスト生成APIの基本料金は、入力料金と出力料金に分かれます。
たとえば、入力料金が100万トークンあたり1ドル、出力料金が100万トークンあたり6ドルのモデルで、入力が10,000トークン、出力が2,000トークンだった場合を考えます。
計算式は次のようになります。
入力料金 = 10,000 ÷ 1,000,000 × $1 = $0.01 出力料金 = 2,000 ÷ 1,000,000 × $6 = $0.012 合計 = $0.022
1リクエストでは数セント以下でも、1日10万リクエスト処理すれば大きな金額になります。
そのため、月末に請求額だけを見るのではなく、API呼び出し時点で原価を記録できる仕組みを作ります。
キャッシュを使うと計算式が変わる
現在の生成AI APIでは、すべての入力トークンを同じ料金で計算できるとは限りません。
OpenAIのGPT-5.6 Lunaは、2026年7月30日の値下げ以降、2026年8月時点で通常入力が100万トークンあたり0.20ドル、キャッシュ済み入力が0.02ドル、出力が1.20ドルです。
さらにGPT-5.6以降では、プロンプトキャッシュへ新しく書き込まれたトークンに、通常入力料金の1.25倍が適用されます。レスポンスでは読み込みがcached_tokens、書き込みがcache_write_tokensとして報告されます。
したがって、GPT-5.6系では入力を少なくとも三つに分けます。
通常入力 キャッシュ読み込み キャッシュ書き込み
たとえば、次の使用量だったとします。
{
"input_tokens": 10000,
"input_tokens_details": {
"cached_tokens": 4000,
"cache_write_tokens": 2000
},
"output_tokens": 1000
}
通常料金で処理された入力は次のように求めます。
10,000 - 4,000 - 2,000 = 4,000トークン
GPT-5.6 Lunaで計算すると、通常入力は0.0008ドルです。
4,000 ÷ 1,000,000 × $0.20 = $0.0008
キャッシュ読み込みは0.00008ドルです。
4,000 ÷ 1,000,000 × $0.02 = $0.00008
キャッシュ書き込みは0.0005ドルです。
2,000 ÷ 1,000,000 × $0.25 = $0.0005
出力は0.0012ドルです。
1,000 ÷ 1,000,000 × $1.20 = $0.0012
このリクエストの推定料金は0.00258ドルになります。
$0.0008 + $0.00008 + $0.0005 + $0.0012 = $0.00258
単純にinput_tokensへ通常入力価格を掛けると、キャッシュ割引を反映できません。
OpenAIのreasoning_tokensを二重計上しない
Reasoningモデルでは、レスポンスの使用量にreasoning_tokensが表示されます。
{
"usage": {
"input_tokens": 75,
"output_tokens": 1186,
"output_tokens_details": {
"reasoning_tokens": 1024
},
"total_tokens": 1261
}
}
ここで注意したいのは、reasoning_tokensをoutput_tokensへ追加して料金計算してはいけないことです。
OpenAIではReasoning Tokensはoutput_tokensに含まれており、出力トークンとして課金されます。reasoning_tokensは内訳を分析するために保存し、料金計算ではoutput_tokensだけを使います。
次の計算は二重計上になります。
const billableOutputTokens = outputTokens + reasoningTokens;
料金計算には次の値を使います。
const billableOutputTokens = outputTokens;
一方、DBにはreasoning_tokensも保存しておくと、Reasoning Effortを変更したときに料金が増えた原因を調べられます。
トークン使用量だけでなく料金単価も保存する
AI APIのモデル価格は固定ではありません。
料金改定、期間限定価格、Batch、Flex、Priority、長いコンテキスト、キャッシュなどによって単価が変化します。
特に分かりやすいのがClaude Sonnet 5です。
2026年8月31日までは通常入力が100万トークンあたり2ドル、5分キャッシュ書き込みが2.50ドル、1時間キャッシュ書き込みが4ドル、キャッシュ読み込みが0.20ドル、出力が10ドルです。
2026年9月1日からは通常入力3ドル、5分キャッシュ書き込み3.75ドル、1時間キャッシュ書き込み6ドル、キャッシュ読み込み0.30ドル、出力15ドルへ変わる予定です。
8月のリクエストを10月に再集計するときに、10月時点の価格を使えば原価が変わってしまいます。
したがって、DBには次のような料金スナップショットを保存します。
{
"currency": "USD",
"unit": "per_1m_tokens",
"input": "2",
"cached_input": "0.20",
"cache_write_5m": "2.50",
"cache_write_1h": "4",
"output": "10",
"effective_from": "2026-08-01"
}
これにより、価格改定後も当時の原価を再現できます。
浮動小数点数だけで料金を保存しない
JavaScriptのnumberはIEEE 754の浮動小数点数です。
金額計算では、次のような誤差が発生することがあります。
console.log(0.1 + 0.2);
結果は厳密な0.3にはなりません。
0.30000000000000004
AI APIの1リクエスト料金は非常に小さいため、大量のリクエストを集計すると丸め誤差が目立つ可能性があります。
PostgreSQLではNUMERIC、PrismaではDecimalを利用できます。PrismaのDecimalはPostgreSQLのdecimalまたはnumericへ対応しています。
この記事ではTypeScript側でもdecimal.jsを使います。
npm install decimal.js
共通の使用量形式を作る
OpenAI、Claude、Geminiでは使用量フィールドの名前が異なります。
DBまでプロバイダー固有形式にすると集計SQLが複雑になるため、アプリケーション内部で共通形式へ変換します。
type ProviderName =
| "openai"
| "anthropic"
| "gemini";
type NormalizedUsage = {
provider: ProviderName;
model: string;
inputTokens: number;
uncachedInputTokens: number;
cachedInputTokens: number;
cacheWriteTokens: number;
cacheWrite5mTokens: number;
cacheWrite1hTokens: number;
outputTokens: number;
reasoningTokens: number;
totalTokens: number;
};
inputTokensにはプロバイダーから報告された総入力相当量を保存します。
料金計算にはuncachedInputTokens、cachedInputTokens、cacheWrite5mTokensなど、分解した値を使用します。
料金スナップショットの型を作る
料金側も共通形式を定義します。
type PriceSnapshot = {
currency: "USD";
unitTokens: 1_000_000;
inputPerUnit: string;
cachedInputPerUnit: string;
cacheWritePerUnit: string;
cacheWrite5mPerUnit: string;
cacheWrite1hPerUnit: string;
outputPerUnit: string;
pricingVersion: string;
};
使わない料金は"0"を指定します。
文字列で保持しているのは、JavaScriptのnumberへ変換して精度を失わないためです。
Decimalで料金を計算する
共通化した使用量と料金表から原価を計算します。
import Decimal from "decimal.js";
type CostBreakdown = {
inputCostUsd: string;
cachedInputCostUsd: string;
cacheWriteCostUsd: string;
outputCostUsd: string;
totalCostUsd: string;
};
function tokenCost(
tokens: number,
pricePerMillion: string,
): Decimal {
return new Decimal(tokens)
.mul(pricePerMillion)
.div(1_000_000);
}
function calculateCost(
usage: NormalizedUsage,
price: PriceSnapshot,
): CostBreakdown {
const inputCost =
tokenCost(
usage.uncachedInputTokens,
price.inputPerUnit,
);
const cachedInputCost =
tokenCost(
usage.cachedInputTokens,
price.cachedInputPerUnit,
);
const genericCacheWriteCost =
tokenCost(
usage.cacheWriteTokens,
price.cacheWritePerUnit,
);
const cacheWrite5mCost =
tokenCost(
usage.cacheWrite5mTokens,
price.cacheWrite5mPerUnit,
);
const cacheWrite1hCost =
tokenCost(
usage.cacheWrite1hTokens,
price.cacheWrite1hPerUnit,
);
const cacheWriteCost =
genericCacheWriteCost
.plus(cacheWrite5mCost)
.plus(cacheWrite1hCost);
const outputCost =
tokenCost(
usage.outputTokens,
price.outputPerUnit,
);
const totalCost =
inputCost
.plus(cachedInputCost)
.plus(cacheWriteCost)
.plus(outputCost);
return {
inputCostUsd:
inputCost.toFixed(12),
cachedInputCostUsd:
cachedInputCost.toFixed(12),
cacheWriteCostUsd:
cacheWriteCost.toFixed(12),
outputCostUsd:
outputCost.toFixed(12),
totalCostUsd:
totalCost.toFixed(12),
};
}
料金を12桁の小数で保持しておけば、非常に安いリクエストでも途中で0へ丸められにくくなります。
画面へ表示するときだけ、用途に合わせて小数点以下を丸めます。
OpenAI Responses APIの使用量を変換する
OpenAI Responses APIでは、通常入力、キャッシュ読み込み、キャッシュ書き込み、出力、Reasoningの内訳を取得できます。
GPT-5.6以降ではcache_write_tokensも料金計算に必要です。
import OpenAI from "openai";
function normalizeOpenAIUsage(
response: OpenAI.Responses.Response,
): NormalizedUsage {
const usage = response.usage;
const inputTokens =
usage?.input_tokens ?? 0;
const cachedInputTokens =
usage?.input_tokens_details
?.cached_tokens ?? 0;
const cacheWriteTokens =
usage?.input_tokens_details
?.cache_write_tokens ?? 0;
const uncachedInputTokens =
Math.max(
0,
inputTokens -
cachedInputTokens -
cacheWriteTokens,
);
const outputTokens =
usage?.output_tokens ?? 0;
const reasoningTokens =
usage?.output_tokens_details
?.reasoning_tokens ?? 0;
return {
provider: "openai",
model: response.model,
inputTokens,
uncachedInputTokens,
cachedInputTokens,
cacheWriteTokens,
cacheWrite5mTokens: 0,
cacheWrite1hTokens: 0,
outputTokens,
reasoningTokens,
totalTokens:
usage?.total_tokens ??
inputTokens + outputTokens,
};
}
GPT-5.6より前のモデルでは、キャッシュ書き込みに追加料金がありません。GPT-5.6以降ではキャッシュ書き込みが通常入力料金の1.25倍となるため、モデル世代によってPrice Snapshotを変更します。
GPT-5.6 Lunaの料金設定例
OpenAIは2026年7月30日にGPT-5.6 Lunaの料金を約80%値下げしました。2026年8月9日時点のGPT-5.6 Lunaは、通常入力が0.20ドル、キャッシュ入力が0.02ドル、出力が1.20ドルです。
GPT-5.6系のキャッシュ書き込みは通常入力の1.25倍なので、次の設定になります。
const gpt56LunaPrice:
PriceSnapshot = {
currency: "USD",
unitTokens: 1_000_000,
inputPerUnit: "0.20",
cachedInputPerUnit: "0.02",
cacheWritePerUnit: "0.25",
cacheWrite5mPerUnit: "0",
cacheWrite1hPerUnit: "0",
outputPerUnit: "1.20",
pricingVersion:
"openai:gpt-5.6-luna:2026-07-30",
};
価格をコードへ直接埋め込む場合も、モデル名と適用日を含むバージョンを付けます。値下げなどの改定が発生した場合は新しいpricingVersionを追加し、既存レコードの単価は書き換えません。
本番ではDBの料金マスタから取得する方法が管理しやすくなります。
Claudeはinput_tokensだけを見ると料金を間違える
Claude APIのPrompt Cachingを使っている場合、usage.input_tokensだけを総入力数として扱ってはいけません。
Anthropicでは、input_tokensは最後のキャッシュブレークポイントより後ろの入力を表します。
総入力数は、通常入力、キャッシュ書き込み、キャッシュ読み込みを合計して求めます。
total_input_tokens = input_tokens + cache_creation_input_tokens + cache_read_input_tokens
さらに現在のClaude APIでは、キャッシュ作成トークンを5分と1時間へ分けて取得できます。
{
"usage": {
"input_tokens": 2048,
"cache_read_input_tokens": 1800,
"cache_creation_input_tokens": 248,
"output_tokens": 503,
"cache_creation": {
"ephemeral_5m_input_tokens": 148,
"ephemeral_1h_input_tokens": 100
}
}
}
5分と1時間ではキャッシュ書き込み単価が異なるため、cache_creation_input_tokensだけを保存するのではなく、その内訳も保存します。
Claudeの使用量を共通形式へ変換する
import Anthropic from "@anthropic-ai/sdk";
function normalizeClaudeUsage(
message: Anthropic.Message,
): NormalizedUsage {
const usage = message.usage;
const regularInputTokens =
usage.input_tokens;
const cachedInputTokens =
usage.cache_read_input_tokens ??
0;
const cacheWrite5mTokens =
usage.cache_creation
?.ephemeral_5m_input_tokens ??
0;
const cacheWrite1hTokens =
usage.cache_creation
?.ephemeral_1h_input_tokens ??
0;
const cacheWriteTokens =
cacheWrite5mTokens +
cacheWrite1hTokens;
const inputTokens =
regularInputTokens +
cachedInputTokens +
cacheWriteTokens;
const outputTokens =
usage.output_tokens;
return {
provider: "anthropic",
model: message.model,
inputTokens,
uncachedInputTokens:
regularInputTokens,
cachedInputTokens,
cacheWriteTokens: 0,
cacheWrite5mTokens,
cacheWrite1hTokens,
outputTokens,
reasoningTokens: 0,
totalTokens:
inputTokens +
outputTokens,
};
}
Claudeのcache_creation_input_tokensは、現在cache_creation内の5分と1時間の値を合計したものです。別々の単価で計算するため、詳細フィールドを優先します。
Claude Sonnet 5の料金設定例
2026年8月9日時点では、Claude Sonnet 5に導入時の特別価格が適用されています。
2026年8月31日までは通常入力2ドル、5分キャッシュ書き込み2.50ドル、1時間キャッシュ書き込み4ドル、キャッシュ読み込み0.20ドル、出力10ドルです。
const claudeSonnet5AugustPrice:
PriceSnapshot = {
currency: "USD",
unitTokens: 1_000_000,
inputPerUnit: "2",
cachedInputPerUnit: "0.20",
cacheWritePerUnit: "0",
cacheWrite5mPerUnit: "2.50",
cacheWrite1hPerUnit: "4",
outputPerUnit: "10",
pricingVersion:
"anthropic:claude-sonnet-5:through-2026-08-31",
};
2026年9月1日以降は別のPrice Snapshotを登録します。
この設計なら、8月のレコードは8月料金、9月のレコードは9月料金のまま残ります。
Geminiではthinking tokensも出力料金へ含まれる
Gemini APIのusageMetadataには、入力、キャッシュ、生成結果、Thinkingの使用量が含まれます。
promptTokenCountはキャッシュ済みコンテンツを含んだ有効な入力トークン総数で、cachedContentTokenCountはそのうちキャッシュから読み込まれた部分です。
candidatesTokenCountは生成された候補のトークン数、thoughtsTokenCountはThinkingに使われたトークン数です。
Gemini 3.5 Flashの料金表では、出力価格にThinking Tokensも含まれることが明示されています。
したがって、単純なテキスト生成では通常入力を次のように求められます。
通常入力 = promptTokenCount - cachedContentTokenCount
料金対象となる出力は次のように考えます。
出力 = candidatesTokenCount + thoughtsTokenCount
GeminiではtotalTokenCountをそのまま出力トークンとして使ってはいけません。
totalTokenCountには入力も含まれます。
Geminiの使用量を共通形式へ変換する
type GeminiUsageMetadata = {
promptTokenCount?: number;
cachedContentTokenCount?: number;
candidatesTokenCount?: number;
thoughtsTokenCount?: number;
totalTokenCount?: number;
};
function normalizeGeminiUsage(
model: string,
usage:
GeminiUsageMetadata | undefined,
): NormalizedUsage {
const inputTokens =
usage?.promptTokenCount ?? 0;
const cachedInputTokens =
usage?.cachedContentTokenCount ??
0;
const uncachedInputTokens =
Math.max(
0,
inputTokens -
cachedInputTokens,
);
const visibleOutputTokens =
usage?.candidatesTokenCount ??
0;
const reasoningTokens =
usage?.thoughtsTokenCount ??
0;
const outputTokens =
visibleOutputTokens +
reasoningTokens;
return {
provider: "gemini",
model,
inputTokens,
uncachedInputTokens,
cachedInputTokens,
cacheWriteTokens: 0,
cacheWrite5mTokens: 0,
cacheWrite1hTokens: 0,
outputTokens,
reasoningTokens,
totalTokens:
usage?.totalTokenCount ??
inputTokens +
outputTokens,
};
}
GeminiではThinking TokensをreasoningTokensとして分析用に保存しつつ、料金計算用のoutputTokensには含めています。
OpenAIとはここが異なる点です。
OpenAIのoutput_tokensにはすでにReasoning Tokensが含まれているため、追加してはいけません。GeminiではcandidatesTokenCountとthoughtsTokenCountが別々に提供されるため、出力料金を求める際に両方を考慮します。
Gemini 3.5 Flashの料金設定例
2026年8月9日時点のGemini 3.5 Flashは、通常のPaid Tierで入力が100万トークンあたり1.50ドル、キャッシュ入力が0.15ドル、出力が9ドルです。
明示的なContext Cachingには、別途100万トークン・1時間あたり1ドルの保存料金もあります。
const gemini35FlashPrice:
PriceSnapshot = {
currency: "USD",
unitTokens: 1_000_000,
inputPerUnit: "1.50",
cachedInputPerUnit: "0.15",
cacheWritePerUnit: "0",
cacheWrite5mPerUnit: "0",
cacheWrite1hPerUnit: "0",
outputPerUnit: "9",
pricingVersion:
"google:gemini-3.5-flash:standard:2026-08-09",
};
Context Cacheの保存料金は、1リクエストのToken Usageだけでは正確に計算できません。
何トークンを何時間保存したかで決まるため、リクエスト料金とは別のai_cost_adjustmentsやai_cache_storage_usageのようなテーブルへ記録するほうが扱いやすくなります。
DBには生のUsageも保存する
共通形式へ変換した値だけを保存すると、将来プロバイダーが新しい料金フィールドを追加したときに再計算できません。
そのため、正規化済みのカラムと、プロバイダーが返した生のUsageの両方を保存します。
PostgreSQLでは次のようなテーブルを作成できます。
CREATE TABLE ai_api_usage ( id BIGSERIAL PRIMARY KEY, trace_id UUID NOT NULL, provider VARCHAR(32) NOT NULL, model VARCHAR(128) NOT NULL, provider_request_id VARCHAR(255), operation VARCHAR(100), user_id VARCHAR(255), status VARCHAR(32) NOT NULL, input_tokens BIGINT NOT NULL DEFAULT 0, uncached_input_tokens BIGINT NOT NULL DEFAULT 0, cached_input_tokens BIGINT NOT NULL DEFAULT 0, cache_write_tokens BIGINT NOT NULL DEFAULT 0, cache_write_5m_tokens BIGINT NOT NULL DEFAULT 0, cache_write_1h_tokens BIGINT NOT NULL DEFAULT 0, output_tokens BIGINT NOT NULL DEFAULT 0, reasoning_tokens BIGINT NOT NULL DEFAULT 0, total_tokens BIGINT NOT NULL DEFAULT 0, input_cost_usd NUMERIC(20, 12) NOT NULL DEFAULT 0, cached_input_cost_usd NUMERIC(20, 12) NOT NULL DEFAULT 0, cache_write_cost_usd NUMERIC(20, 12) NOT NULL DEFAULT 0, output_cost_usd NUMERIC(20, 12) NOT NULL DEFAULT 0, estimated_cost_usd NUMERIC(20, 12) NOT NULL DEFAULT 0, pricing_version VARCHAR(255) NOT NULL, pricing_snapshot JSONB NOT NULL, usage_raw JSONB NOT NULL, latency_ms INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );
provider_request_idにはOpenAIやClaudeなどのリクエストIDを保存します。
trace_idには自分のアプリケーションで生成した共通IDを保存します。
一つのユーザー操作でOpenAIが失敗し、Claudeへフォールバックした場合も、同じtrace_idを使えば一連のコストを追跡できます。
user_idには個人情報そのものを保存しない
ユーザー別の原価を集計したい場合、user_idを保存します。
ただし、メールアドレスや氏名をAI利用ログへ直接複製する必要はありません。
自社DBで利用している内部ユーザーIDや匿名化したIDを使用します。
user_01JABC...
APIへ送ったプロンプト全文も、料金計算には必要ありません。
Usageテーブルには原則としてトークン数、モデル、処理種別、料金、リクエストIDだけを保存します。
プロンプトを障害調査用に保存する場合は、別テーブルへ分離し、保存期間とアクセス権限を設定します。
リクエスト完了後にUsageを保存する
OpenAIを呼び出してからDBへ保存する流れは次のようになります。
import {
randomUUID,
} from "node:crypto";
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
async function generateText(
userId: string,
prompt: string,
) {
const traceId =
randomUUID();
const startedAt =
performance.now();
const response =
await openai.responses.create({
model: "gpt-5.6-luna",
input: prompt,
});
const latencyMs =
Math.round(
performance.now() -
startedAt,
);
const usage =
normalizeOpenAIUsage(
response,
);
const cost =
calculateCost(
usage,
gpt56LunaPrice,
);
await saveAiUsage({
traceId,
userId,
providerRequestId:
response._request_id ??
null,
operation: "chat",
status:
response.status,
usage,
cost,
price:
gpt56LunaPrice,
rawUsage:
response.usage ??
{},
latencyMs,
});
return response.output_text;
}
料金保存に失敗したからといって、AI APIをもう一度呼び出してはいけません。
再送するとAPI料金がもう一度発生する可能性があります。
AI生成とUsage記録は別処理として考え、Usage保存だけを再試行できる構成にします。
DBへ保存するTypeScript実装
pgを利用してPostgreSQLへ保存する例です。
npm install pg decimal.js npm install -D @types/pg
import {
Pool,
} from "pg";
const pool = new Pool({
connectionString:
process.env.DATABASE_URL,
});
type SaveAiUsageInput = {
traceId: string;
userId: string;
providerRequestId:
string | null;
operation: string;
status: string;
usage: NormalizedUsage;
cost: CostBreakdown;
price: PriceSnapshot;
rawUsage: unknown;
latencyMs: number;
};
async function saveAiUsage(
input: SaveAiUsageInput,
): Promise<void> {
await pool.query(
`
INSERT INTO ai_api_usage (
trace_id,
provider,
model,
provider_request_id,
operation,
user_id,
status,
input_tokens,
uncached_input_tokens,
cached_input_tokens,
cache_write_tokens,
cache_write_5m_tokens,
cache_write_1h_tokens,
output_tokens,
reasoning_tokens,
total_tokens,
input_cost_usd,
cached_input_cost_usd,
cache_write_cost_usd,
output_cost_usd,
estimated_cost_usd,
pricing_version,
pricing_snapshot,
usage_raw,
latency_ms
)
VALUES (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10,
$11, $12, $13,
$14, $15, $16,
$17, $18, $19, $20, $21,
$22, $23::jsonb, $24::jsonb,
$25
)
`,
[
input.traceId,
input.usage.provider,
input.usage.model,
input.providerRequestId,
input.operation,
input.userId,
input.status,
input.usage.inputTokens,
input.usage
.uncachedInputTokens,
input.usage
.cachedInputTokens,
input.usage
.cacheWriteTokens,
input.usage
.cacheWrite5mTokens,
input.usage
.cacheWrite1hTokens,
input.usage.outputTokens,
input.usage.reasoningTokens,
input.usage.totalTokens,
input.cost.inputCostUsd,
input.cost
.cachedInputCostUsd,
input.cost
.cacheWriteCostUsd,
input.cost.outputCostUsd,
input.cost.totalCostUsd,
input.price.pricingVersion,
JSON.stringify(
input.price,
),
JSON.stringify(
input.rawUsage,
),
input.latencyMs,
],
);
}
SQLへ値を文字列連結せず、必ずプレースホルダーを使います。
同じAPIリクエストを二重登録しない
ネットワークエラーやQueueの再試行によって、同じUsage保存処理が複数回実行される可能性があります。
同じレコードを2回保存すると、月間料金が実際の2倍に見えます。
プロバイダーのリクエストIDが利用できる場合は、一意制約を付けます。
CREATE UNIQUE INDEX ai_api_usage_provider_request_unique ON ai_api_usage ( provider, provider_request_id ) WHERE provider_request_id IS NOT NULL;
登録側ではON CONFLICTを使用できます。
ON CONFLICT ( provider, provider_request_id ) WHERE provider_request_id IS NOT NULL DO NOTHING
一つのプロバイダーリクエストを一度だけ料金へ加算できるようにします。
失敗したリクエストも記録する
AI APIがHTTPエラーを返した場合、Usageが取得できないケースがあります。
一方、モデルが出力上限へ到達してincompleteになった場合などは、利用者が期待した回答を得られなくてもトークンを消費しています。
OpenAIのReasoningモデルでは、max_output_tokensへ達すると、可視回答が出る前でもReasoning Tokensを使用して料金が発生する可能性があります。
そのため、成功した回答だけを保存してはいけません。
completed incomplete failed cancelled
といった状態を一緒に記録し、Usageが返っている場合は失敗時も保存します。
これにより「ユーザーへ回答できなかった処理に月いくら使っているか」も分析できます。
リトライしたAPI料金もすべて保存する
OpenAIでタイムアウトし、Claudeへフォールバックした場合、最終的な回答はClaudeから返っていても、OpenAI側でトークン消費が発生している可能性があります。
一つのユーザー操作に複数のAPI呼び出しがある場合は、各API呼び出しを別レコードとして保存します。
同じtrace_idを使用します。
trace_id: abc OpenAI status: incomplete cost: $0.008 Claude status: completed cost: $0.015
ユーザー操作全体の原価は、同じtrace_idを合計して求めます。
SELECT
trace_id,
SUM(estimated_cost_usd)
AS total_cost_usd
FROM ai_api_usage
GROUP BY trace_id;
「最終的に回答したモデルの料金」だけを保存すると、リトライやフォールバックで発生した隠れた原価を見落とします。
ユーザーごとの料金を集計する
ユーザー単位の月間原価は簡単に集計できます。
SELECT
user_id,
SUM(estimated_cost_usd)
AS cost_usd,
SUM(input_tokens)
AS input_tokens,
SUM(output_tokens)
AS output_tokens,
COUNT(*)
AS api_requests
FROM ai_api_usage
WHERE
created_at >= DATE '2026-08-01'
AND created_at < DATE '2026-09-01'
GROUP BY user_id
ORDER BY cost_usd DESC;
月額980円のサービスで、特定ユーザーだけAI API原価が20ドル発生しているような状況を検出できます。
アクセス回数だけではなく、原価を見ることが重要です。
機能ごとの料金を集計する
operationには、どの機能でAPIを利用したかを保存します。
chat article_generation summarization translation rag_answer code_review classification
機能ごとの原価は次のSQLで確認できます。
SELECT
operation,
provider,
model,
COUNT(*) AS requests,
SUM(estimated_cost_usd)
AS cost_usd,
AVG(estimated_cost_usd)
AS average_cost_usd
FROM ai_api_usage
WHERE
created_at >= NOW()
- INTERVAL '30 days'
GROUP BY
operation,
provider,
model
ORDER BY
cost_usd DESC;
PVが多い機能より、利用回数の少ない長文生成機能のほうがAPI料金を多く消費していることもあります。
1リクエストの平均料金だけでは不十分
平均料金だけを見ると、一部の非常に高額なリクエストを発見できません。
たとえば、通常のチャットは0.005ドルでも、巨大なPDFを入力した処理だけ1ドルを超える可能性があります。
PostgreSQLではパーセンタイルも確認できます。
SELECT
operation,
AVG(estimated_cost_usd)
AS average_cost,
PERCENTILE_CONT(0.5)
WITHIN GROUP (
ORDER BY estimated_cost_usd
)
AS median_cost,
PERCENTILE_CONT(0.95)
WITHIN GROUP (
ORDER BY estimated_cost_usd
)
AS p95_cost,
MAX(estimated_cost_usd)
AS max_cost
FROM ai_api_usage
WHERE
created_at >= NOW()
- INTERVAL '30 days'
GROUP BY operation;
平均、中央値、95パーセンタイル、最大値を一緒に確認すると、異常に高額な処理を見つけやすくなります。
キャッシュ率と料金を一緒に見る
Prompt Cachingを利用している場合は、キャッシュトークン数だけでなく、実際の料金削減につながっているかを確認します。
SELECT
model,
SUM(input_tokens)
AS input_tokens,
SUM(cached_input_tokens)
AS cached_tokens,
SUM(cache_write_tokens)
AS cache_write_tokens,
SUM(estimated_cost_usd)
AS cost_usd
FROM ai_api_usage
WHERE
provider = 'openai'
AND created_at >= NOW()
- INTERVAL '7 days'
GROUP BY model;
GPT-5.6以降ではキャッシュ書き込みにも追加料金が発生するため、cached_tokensだけが多ければよいわけではありません。
OpenAI自身もGPT-5.6への移行時には、cached_tokensとcache_write_tokensの両方を監視し、キャッシュの純粋なコスト効果を確認するよう案内しています。
Price MasterとPrice Snapshotを分ける
モデル価格をすべてソースコードへ埋め込むと、価格変更のたびにデプロイが必要です。
本番では料金マスタをDBへ持たせる方法があります。
CREATE TABLE ai_model_prices (
id BIGSERIAL PRIMARY KEY,
provider VARCHAR(32) NOT NULL,
model VARCHAR(128) NOT NULL,
service_tier VARCHAR(64) NOT NULL,
input_per_million
NUMERIC(20, 8) NOT NULL,
cached_input_per_million
NUMERIC(20, 8) NOT NULL,
cache_write_per_million
NUMERIC(20, 8) NOT NULL,
cache_write_5m_per_million
NUMERIC(20, 8) NOT NULL,
cache_write_1h_per_million
NUMERIC(20, 8) NOT NULL,
output_per_million
NUMERIC(20, 8) NOT NULL,
effective_from
TIMESTAMPTZ NOT NULL,
effective_until
TIMESTAMPTZ,
pricing_version
VARCHAR(255) NOT NULL UNIQUE
);
APIを呼び出した日時に有効な料金を取得します。
SELECT *
FROM ai_model_prices
WHERE
provider = $1
AND model = $2
AND service_tier = $3
AND effective_from <= NOW()
AND (
effective_until IS NULL
OR effective_until > NOW()
)
ORDER BY effective_from DESC
LIMIT 1;
取得した料金は、Usageレコードのpricing_snapshotにもコピーします。
料金マスタを書き換えても、過去レコードの単価は変わりません。
BatchやService Tierも保存する
同じモデルでも、通常APIとBatch APIでは価格が異なる場合があります。
GeminiではStandard、Batch、Flex、Priorityで料金が異なります。たとえばGemini 3.1 Flash-LiteではBatchとFlexがStandardより安く、Priorityは高い価格が設定されています。
ClaudeにもBatch APIによる割引があります。
そのため、モデル名だけでPrice Masterを検索してはいけません。
provider model service_tier batch region pricing_version
など、実際の料金へ影響する条件を保存します。
Web Searchなどのツール料金は別に加算する
トークン料金だけでは請求額と一致しないことがあります。
Web Searchや画像生成など、一回あたりの利用料金が設定されているツールがあるためです。
AnthropicのWeb Searchは、トークン料金とは別に検索1,000回あたり10ドルの料金が発生します。Claude APIではUsageのserver_tool_use.web_search_requestsから検索回数を確認できます。
GeminiのGoogle Search Groundingにも、無料枠を超えた後は検索クエリ単位の料金が設定されています。
そのため、最終的な原価は次のように考えます。
AI API料金 = 入力トークン料金 + キャッシュ料金 + 出力トークン料金 + ツール料金 + キャッシュ保存料金 + その他の従量料金
DBではトークン料金と追加料金を分離すると管理しやすくなります。
CREATE TABLE ai_api_extra_costs (
id BIGSERIAL PRIMARY KEY,
ai_usage_id BIGINT NOT NULL
REFERENCES ai_api_usage(id),
cost_type VARCHAR(100)
NOT NULL,
quantity
NUMERIC(20, 8)
NOT NULL,
unit_price_usd
NUMERIC(20, 12)
NOT NULL,
cost_usd
NUMERIC(20, 12)
NOT NULL,
metadata JSONB NOT NULL
DEFAULT '{}'::jsonb
);
calculated_costと実際の請求額を区別する
アプリケーションが計算した料金は、最終的な請求書と完全に一致するとは限りません。
価格改定の反映漏れ、無料枠、契約割引、クレジット、税金、クラウド経由の料金、ツール利用、サービスティアなどが影響するためです。
DBのカラム名を単純なcostではなく、estimated_cost_usdやcalculated_cost_usdにする理由はここにあります。
プロバイダーがUsage APIやCost APIを提供している場合は、日次バッチで実際の利用額と照合します。
Anthropicには組織単位のUsage ReportとCost Report APIが用意されています。
アプリケーション側ではリアルタイムな原価推定に使い、経理上の確定金額はプロバイダーの請求情報を正とする設計が安全です。
API料金と売上を同じ画面で比較する
SaaSではAI APIの原価だけを集計するのではなく、ユーザーの課金額と比較します。
たとえばユーザーごとに次の値を集計します。
SELECT
user_id,
SUM(estimated_cost_usd)
AS ai_cost_usd
FROM ai_api_usage
WHERE
created_at >=
DATE_TRUNC(
'month',
CURRENT_DATE
)
GROUP BY user_id;
自社のSubscriptionテーブルと結合すれば、ユーザー単位の粗利を計算できます。
月額料金が一定でも、AI API利用量はユーザーごとに大きく異なる可能性があります。
Usage制限やクレジット制を設計するときにも、実際のリクエスト原価が重要になります。
料金アラートを設定する
DBへリクエスト単位の料金を保存すると、異常利用をリアルタイムに検出できます。
通常0.01ドル程度の処理で、1リクエストだけ1ドルを超えた場合は、コンテキストが意図せず巨大になっている可能性があります。
const MAX_REQUEST_COST_USD =
new Decimal("0.50");
const requestCost =
new Decimal(
cost.totalCostUsd,
);
if (
requestCost.greaterThan(
MAX_REQUEST_COST_USD,
)
) {
console.warn({
event:
"high_ai_request_cost",
traceId,
provider:
usage.provider,
model:
usage.model,
costUsd:
cost.totalCostUsd,
});
}
リクエストを実行した後に検知するだけでなく、送信前のトークン数から概算して、高額になりそうな入力を拒否する方法もあります。
日次・月次の予算をアプリ側でも管理する
プロバイダーのSpend Limitだけに依存せず、自社DBのUsageから予算を確認できます。
SELECT
SUM(estimated_cost_usd)
AS today_cost_usd
FROM ai_api_usage
WHERE
created_at >=
CURRENT_DATE;
ユーザー単位にも制限できます。
SELECT
SUM(estimated_cost_usd)
AS user_month_cost_usd
FROM ai_api_usage
WHERE
user_id = $1
AND created_at >=
DATE_TRUNC(
'month',
CURRENT_DATE
);
一定額を超えたら、高性能モデルから軽量モデルへ切り替える、長文機能を停止する、追加クレジットを要求するといった制御へ利用できます。
ストリーミングでも最終Usageを保存する
ストリーミングでは、画面へテキストを表示するたびに料金を計算する必要はありません。
プロバイダーから最終的なUsage情報を受信した時点でDBへ保存します。
途中でブラウザが切断された場合でも、サーバー側でAPI処理が継続してトークンを生成していれば料金が発生する可能性があります。
ユーザーへの送信成功と、AI APIのUsage記録は分けて考えます。
ストリームが途中で終了した場合も、Usageを取得できたならstatus = incompleteなどとして保存します。
プロンプトの文字数から料金を推測しない
「日本語1文字はだいたい1トークン」のような概算だけで請求額を保存するのは避けます。
モデルやTokenizerによってトークン数は変化します。
また、Tool Callingではツール定義やツール結果が入力へ追加され、Reasoningモデルでは見えない推論トークンも料金へ影響します。
料金記録には、可能な限りAPIレスポンスのUsageを使用します。
送信前のToken Countは予算チェックの概算に使い、実際の原価はレスポンスUsageから確定します。
料金表を自動取得するだけに依存しない
モデル価格を公式ページから自動スクレイピングしてDBへ反映する実装はおすすめできません。
HTML構造が変わっただけで誤った価格を登録する可能性があります。
料金変更を検知したら管理者が確認し、新しいpricing_versionとして追加する運用のほうが安全です。
既存レコードの料金スナップショットは書き換えません。
特に期間限定価格では、開始日時と終了日時を明示します。
Claude Sonnet 5のように2026年8月31日までと9月1日以降で価格が変わるモデルでも、適用期間から正しい料金を選択できます。
AI API料金を確認するときの基本設計
AI APIのコスト管理では、APIレスポンスのUsageを正規化し、リクエスト単位でDBへ保存します。
通常入力、キャッシュ読み込み、キャッシュ書き込み、出力を別々に記録します。
Reasoning Tokensも分析用に保存しますが、プロバイダーごとに料金計算上の扱いが異なる点に注意します。
料金単価は現在の料金表から後日計算するのではなく、リクエスト時点のPrice Snapshotを一緒に保存します。
また、トークン料金とは別にWeb Search、Grounding、Context Cache Storageなどの追加料金を記録できる設計にします。
これにより、モデル別、ユーザー別、機能別、日別、月別の原価をSQLだけで分析できるようになります。
AI APIの料金計算に関するよくある質問
Qトークン数だけ保存しておけば、あとから料金を再計算できますか?
Aできません。モデル価格は改定されたり、期間限定価格が終了したりします。トークン数だけを保存し、後日その時点の最新価格を掛けると、当時の実際の原価とは異なる金額になります。リクエスト時点の料金単価をPrice Snapshotとして一緒に保存してください。
QOpenAIのreasoning_tokensはoutput_tokensに含まれますか?
A含まれます。OpenAIのReasoning Tokensはoutput_tokensの一部として課金されるため、料金計算でoutput_tokensへさらにreasoning_tokensを加算すると二重計上になります。reasoning_tokensは内訳分析のためだけにDBへ保存してください。
Q料金計算にJavaScriptのnumber型を使ってよいですか?
A小規模な用途であれば問題ない場合もありますが、大量のリクエストを集計すると浮動小数点の丸め誤差が目立つ可能性があります。PostgreSQLのNUMERICやTypeScriptのdecimal.jsなど、10進数の精度を保てる型を使うことをおすすめします。
QClaudeのinput_tokensだけを合計すれば総入力トークン数になりますか?
Aなりません。Claude APIのinput_tokensは最後のキャッシュブレークポイントより後ろの入力だけを表します。総入力数を求めるには、input_tokensにcache_creation_input_tokensとcache_read_input_tokensを加算する必要があります。
まとめ
AI APIの料金をリクエスト単位で計算する場合は、単純に総トークン数へ一つの単価を掛けてはいけません。
OpenAIでは通常入力、キャッシュ読み込み、GPT-5.6以降のキャッシュ書き込み、出力を分けて計算します。Reasoning Tokensはoutput_tokensへ含まれているため、二重に加算しません。
Claudeではinput_tokens、cache_read_input_tokens、5分・1時間のcache_creationを分けて保存します。Claudeの総入力数は、それらを合計して求めます。
GeminiではpromptTokenCountからcachedContentTokenCountを引いて通常入力を求め、生成候補とThinking Tokensを出力料金へ反映します。
DBにはトークン数だけでなく、そのリクエストで使用した料金単価をpricing_snapshotとして保存することが重要です。
モデル価格は変更されるため、過去のUsageへ現在価格を掛け直すと、当時の原価を再現できない場合があります。
さらに、リトライやフォールバックしたAPI呼び出しもすべて同じtrace_idで保存し、一つのユーザー操作に実際いくら掛かったのかを集計します。
この設計にしておけば、「どのユーザーが高コストか」「どの機能の利益率が低いか」「Prompt Cachingで本当に安くなったか」「モデル変更で原価が何%変化したか」をSQLから確認できるようになります。
