01TL;DR
- LLM APIのコストは「入力トークン」「出力トークン」「キャッシュ読み出し」の3区分で構成されており、それぞれ単価が異なります。
- 出力トークンは入力の3〜10倍の単価になる傾向があり、「レスポンスを短くする」施策が費用対効果の高い最初の一手です。
- プロンプトキャッシュを活用すると、繰り返し登場する長いシステムプロンプトのコストを大幅に削減できます。
- タスクの複雑さに応じてモデルを使い分けることで、品質を維持しながらコストを数分の一に抑えられることがあります。
- サンプルコードはTypeScript(Node.js v22.x / Anthropic SDK v0.26.x系 / OpenAI SDK v4.x系)で記述しています。
02はじめに
この記事の対象読者
- LLMを使ったサービスを本番運用しており、APIコストが気になってきたエンジニア
- 「なんとなくトークンを消費している」状態から脱して、根拠のある見積もりと削減策を持ちたい人
- コスト最適化の施策を提案する立場にいて、効果の試算方法を探している人
TypeScript と Node.js の基礎的な扱いを前提にしています。 特定モデルの詳細な仕様については本記事では深く立ち入らず、計算の考え方と実装パターンに重点を置きます。 料金は各APIプロバイダの公式ページを必ず確認してください。本記事に掲載する数値はあくまで説明のための試算例であり、特定時点の価格を断定するものではありません。
実行環境
- Node.js: v22.4.0
- TypeScript: v5.5.x
@anthropic-ai/sdk: v0.26.xopenai: v4.52.xtiktoken: v1.0.x(トークン数の事前計算に使用)
この記事で得られること
- 入力・出力・キャッシュという3区分のコスト構造と、それぞれの特性の理解
- APIレスポンスからトークン消費を記録・集計する実装パターン
- プロンプトキャッシュの仕組みと、キャッシュヒット率を高める設計
- タスク複雑度に基づくモデルルーティングの実装例
- 月次コストを事前に試算するスプレッドシート的思考と計算コード
03コストの構造を理解する
3区分の内訳
LLM APIの課金は、大きく3つの区分で成り立っています。
| 区分 | 説明 | 相対的な単価感 |
|---|---|---|
| 入力トークン(Input) | モデルに渡したプロンプト全体のトークン数 | 基準 |
| 出力トークン(Output) | モデルが生成したレスポンスのトークン数 | 入力の3〜10倍程度 |
| キャッシュ読み出し(Cache Read) | キャッシュから再利用した入力部分のトークン数 | 入力の10〜20%程度 |
「出力トークンが高い」という点は、コスト最適化を考えるうえで最初に押さえておくべき事実です。 単純に「長い回答を返させない」だけで、同じリクエスト量でも費用を大きく変えられることがあります。
なぜ出力が高いのか
出力トークンは、モデルが1トークンずつ逐次生成するため、入力の解析コストとは性質が異なります。 入力はまとめてエンコードできますが、出力は前のトークンが確定してから次を生成する自己回帰的な処理です。 この計算量の非対称性が単価差に反映されています。
実装上の示唆として、「回答をMarkdownで詳しく説明してください」という指示は「1文で要点だけ答えてください」と比べて、出力トークン数が数倍〜数十倍に膨らむことがあります。 用途に応じてレスポンス形式を絞ることが、最もシンプルなコスト削減策の一つです。
キャッシュ読み出しの位置づけ
プロンプトキャッシュ(Prompt Caching)は、APIプロバイダ側でプロンプトの先頭部分をキャッシュし、次回のリクエストで再利用する仕組みです。 キャッシュヒット時は、通常の入力トークン単価より大幅に安い「キャッシュ読み出し」単価で課金されます。
ただし、キャッシュ書き込みには通常よりわずかに高い単価がかかることがある点も覚えておく必要があります。 「キャッシュが効くかどうか」は、同じプロンプト先頭部分が短い時間内に繰り返し使われるかどうかで決まります。
04トークン消費を計測・記録する
APIレスポンスからの取得
ほとんどのLLM APIは、レスポンスにトークン使用量を含めています。 これをそのまま記録することが、正確なコスト把握の基本です。
Anthropic APIの例を見てみます。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function callWithUsageTracking(
systemPrompt: string,
userMessage: string
): Promise<{ content: string; usage: TokenUsage }> {
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
});
// Anthropic の input_tokens はキャッシュ書き込み・読み出し分を含まない通常入力のみです。
// 総入力コストを正しく計算するには3フィールドを合算してください。
const usage: TokenUsage = {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
// キャッシュ関連フィールドはキャッシュ有効時に存在します
cacheCreationTokens: response.usage.cache_creation_input_tokens ?? 0,
cacheReadTokens: response.usage.cache_read_input_tokens ?? 0,
};
const content =
response.content[0].type === "text" ? response.content[0].text : "";
return { content, usage };
}
interface TokenUsage {
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
}
OpenAI APIでも同様にトークン使用量を取得できます。
import OpenAI from "openai";
const openai = new OpenAI();
async function callOpenAIWithUsage(
systemPrompt: string,
userMessage: string
): Promise<{ content: string; usage: TokenUsage }> {
const response = await openai.chat.completions.create({
model: "gpt-5.4-mini",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
],
});
const rawUsage = response.usage;
if (!rawUsage) {
throw new Error("usage フィールドがレスポンスに含まれていません");
}
const usage: TokenUsage = {
inputTokens: rawUsage.prompt_tokens,
outputTokens: rawUsage.completion_tokens,
cacheCreationTokens: 0,
// OpenAI では cached_tokens として返ります(APIバージョンによって異なります)
cacheReadTokens: rawUsage.prompt_tokens_details?.cached_tokens ?? 0,
};
const content = response.choices[0]?.message?.content ?? "";
return { content, usage };
}
コストの計算
取得したトークン数に単価を掛けることでコストを計算します。 単価は各プロバイダの料金ページに公表されていますが、頻繁に更新されるため、コードにハードコードするのではなく設定ファイルで管理するのが無難です。
/**
* モデルごとのトークン単価(USD/1Mトークン)の設定例。
* 実際の値は各プロバイダの公式料金ページで確認し、適宜更新してください。
* ここに掲載する数値はあくまで計算構造を示すための仮値です。
*/
interface ModelPricing {
inputPerMillion: number;
outputPerMillion: number;
cacheReadPerMillion: number;
cacheWritePerMillion: number;
}
const PRICING_CONFIG: Record<string, ModelPricing> = {
// 実際の値は公式ドキュメントで確認してください
"claude-sonnet-4-5": {
inputPerMillion: 3.0,
outputPerMillion: 15.0,
cacheReadPerMillion: 0.3,
cacheWritePerMillion: 3.75,
},
"claude-haiku-4-5": {
inputPerMillion: 0.8,
outputPerMillion: 4.0,
cacheReadPerMillion: 0.08,
cacheWritePerMillion: 1.0,
},
// OpenAI のプロンプトキャッシュは自動適用で書き込み課金はありません(Anthropic と異なる点です)
"gpt-5.5": {
inputPerMillion: 5.0,
outputPerMillion: 30.0,
cacheReadPerMillion: 0.5,
cacheWritePerMillion: 0,
},
"gpt-5.4-mini": {
inputPerMillion: 0.75,
outputPerMillion: 4.5,
cacheReadPerMillion: 0.075,
cacheWritePerMillion: 0,
},
};
function calculateCostUSD(
usage: TokenUsage,
modelId: string
): number {
const pricing = PRICING_CONFIG[modelId];
if (!pricing) {
throw new Error(`モデル "${modelId}" の料金設定が見つかりません`);
}
// Anthropic の input_tokens はキャッシュ分を含まない通常入力のみなので、
// そのまま inputPerMillion を掛けられます。
// キャッシュ書き込み・読み出しはそれぞれ別単価で加算します。
const cost =
(usage.inputTokens / 1_000_000) * pricing.inputPerMillion +
(usage.outputTokens / 1_000_000) * pricing.outputPerMillion +
(usage.cacheReadTokens / 1_000_000) * pricing.cacheReadPerMillion +
(usage.cacheCreationTokens / 1_000_000) * pricing.cacheWritePerMillion;
return cost;
}
この計算式のポイントは、Anthropic APIの input_tokens がキャッシュ書き込み・読み出し分を含まない通常入力のみを返す点です。
キャッシュ書き込み(cacheCreationTokens)と読み出し(cacheReadTokens)はそれぞれ別単価で加算することで、正確なコストが計算できます。
使用量のログ記録
個々のリクエストのコストを記録しておくと、後からどの機能・ユーザー・時間帯にコストが集中しているかを分析できます。
interface RequestLog {
timestamp: string;
requestId: string;
modelId: string;
taskType: string;
usage: TokenUsage;
costUSD: number;
durationMs: number;
}
class CostLogger {
private logs: RequestLog[] = [];
record(
requestId: string,
modelId: string,
taskType: string,
usage: TokenUsage,
durationMs: number
): void {
const costUSD = calculateCostUSD(usage, modelId);
const log: RequestLog = {
timestamp: new Date().toISOString(),
requestId,
modelId,
taskType,
usage,
costUSD,
durationMs,
};
this.logs.push(log);
// 実運用ではここでDBやファイルへの書き込みを行います
console.log(JSON.stringify(log));
}
summarize(): { totalCostUSD: number; byTaskType: Record<string, number> } {
const totalCostUSD = this.logs.reduce((sum, l) => sum + l.costUSD, 0);
const byTaskType: Record<string, number> = {};
for (const log of this.logs) {
byTaskType[log.taskType] = (byTaskType[log.taskType] ?? 0) + log.costUSD;
}
return { totalCostUSD, byTaskType };
}
}
実行例と出力のイメージは以下の通りです。
const logger = new CostLogger();
// リクエスト後に記録する例
const usage: TokenUsage = {
inputTokens: 1200,
outputTokens: 340,
cacheCreationTokens: 0,
cacheReadTokens: 800,
};
logger.record("req-001", "claude-sonnet-4-5", "summarization", usage, 1850);
// 出力例(実際の値は単価設定に依存します)
// {
// "timestamp": "2026-06-11T09:23:11.042Z",
// "requestId": "req-001",
// "modelId": "claude-sonnet-4-5",
// "taskType": "summarization",
// "usage": { "inputTokens": 1200, "outputTokens": 340, ... },
// "costUSD": 0.00564,
// "durationMs": 1850
// }
05プロンプトキャッシュを活用する
キャッシュが効く条件
プロンプトキャッシュは、リクエストのたびに同じ内容を先頭に含む場合に効果を発揮します。 典型的な例は以下のようなケースです。
- 長いシステムプロンプトを全リクエストで共有している
- RAGで取得した同じドキュメント群を文脈として毎回渡している
- Few-shotの例示が固定で、ユーザーのメッセージだけが変わる
逆に、プロンプトの先頭部分がリクエストごとに変わる場合は、キャッシュはほとんど効きません。 「変わらない部分を前に、変わる部分を後ろに」という配置が基本原則です。
Anthropic APIでのキャッシュ設定
Anthropic のプロンプトキャッシュは、cache_control フィールドで明示的に指定します。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// 長いシステムプロンプトの例
const LARGE_SYSTEM_PROMPT = `
あなたは事業計画の策定を支援するアシスタントです。
以下の評価フレームワークに従って回答してください。
## 評価フレームワーク
### 市場性の評価
市場規模・成長率・競合状況・参入障壁を4軸で評価します。
(...実際には数百〜数千トークン程度の長文が入ります...)
### 財務計画の評価
収益モデル・コスト構造・キャッシュフロー・損益分岐点を評価します。
(...長い評価基準の説明...)
### リスク評価
市場リスク・技術リスク・競合リスク・規制リスクを評価します。
(...長い評価基準の説明...)
`.trim();
async function analyzeWithCache(userQuestion: string): Promise<string> {
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
system: [
{
type: "text",
text: LARGE_SYSTEM_PROMPT,
// この部分をキャッシュするよう指示します
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: userQuestion }],
});
return response.content[0].type === "text" ? response.content[0].text : "";
}
1回目のリクエストでは cache_creation_input_tokens にトークン数が入り、通常より少し高い書き込み単価が適用されます。
2回目以降は cache_read_input_tokens にトークン数が入り、大幅に安い単価で処理されます。
キャッシュの有効期間はプロバイダと設定によって異なりますが、Anthropic では ephemeral タイプで数分程度のTTLが設定されます。
高頻度のリクエストが続く場面で特に効果が大きく、バースト的に来るリクエストでは恩恵を受けにくいケースもあります。
キャッシュヒット率の計測
キャッシュが実際に効いているかどうかを確認するには、cache_read_input_tokens の割合を継続的にモニタリングします。
interface CacheStats {
totalRequests: number;
cacheHits: number;
totalInputTokens: number;
cachedTokens: number;
estimatedSavingsUSD: number;
}
function computeCacheStats(
logs: RequestLog[],
modelId: string
): CacheStats {
const pricing = PRICING_CONFIG[modelId];
if (!pricing) throw new Error(`価格設定が見つかりません: ${modelId}`);
let totalRequests = 0;
let cacheHits = 0;
let totalInputTokens = 0;
let cachedTokens = 0;
for (const log of logs.filter((l) => l.modelId === modelId)) {
totalRequests++;
totalInputTokens += log.usage.inputTokens;
cachedTokens += log.usage.cacheReadTokens;
if (log.usage.cacheReadTokens > 0) {
cacheHits++;
}
}
// キャッシュ読み出し単価と通常入力単価の差額が節約額になります
const savingsPerToken =
(pricing.inputPerMillion - pricing.cacheReadPerMillion) / 1_000_000;
const estimatedSavingsUSD = cachedTokens * savingsPerToken;
return {
totalRequests,
cacheHits,
totalInputTokens,
cachedTokens,
estimatedSavingsUSD,
};
}
// 使用例
const stats = computeCacheStats(logs, "claude-sonnet-4-5");
console.log(`リクエスト総数: ${stats.totalRequests}`);
console.log(`キャッシュヒット率: ${((stats.cacheHits / stats.totalRequests) * 100).toFixed(1)}%`);
console.log(
`入力トークンのうちキャッシュ利用割合: ${((stats.cachedTokens / stats.totalInputTokens) * 100).toFixed(1)}%`
);
console.log(`推定節約額: $${stats.estimatedSavingsUSD.toFixed(4)}`);
// 出力例(値はあくまで試算例です)
// リクエスト総数: 500
// キャッシュヒット率: 92.0%
// 入力トークンのうちキャッシュ利用割合: 78.4%
// 推定節約額: $1.2300
ヒット率が低い場合は、システムプロンプトの先頭部分がリクエストごとに異なっている可能性があります。 タイムスタンプや動的な情報をシステムプロンプトの冒頭に入れていないか確認してみてください。
06モデル使い分けでコストを削減する
なぜ使い分けが有効なのか
高性能なモデルほど単価が高い傾向があります。 しかし、すべてのタスクで最高性能のモデルが必要というわけではありません。
たとえば、以下のようなタスクは比較的軽量なモデルでも十分な品質が出ることがあります。
- 短いテキストの分類(感情分析・カテゴリ分類など)
- 構造化された情報の抽出(定型フォームへの転写など)
- 定型文の生成や要約の概略
- 内部的な中間処理(ユーザーが直接見ない部分)
一方、以下のタスクには高性能モデルが向いています。
- 複数の条件を考慮した複雑な推論
- 長文の読解と批判的分析
- ユーザーが直接評価する最終的な出力
- コードの複雑な実装・デバッグ
モデルルーティングの実装
タスクの種類に応じてモデルを自動選択するルーターを実装する例を示します。
type TaskComplexity = "simple" | "moderate" | "complex";
interface RoutingConfig {
simple: string;
moderate: string;
complex: string;
}
// モデルルーティング設定(環境変数や設定ファイルで管理することを推奨します)
const DEFAULT_ROUTING: RoutingConfig = {
simple: "claude-haiku-4-5",
moderate: "claude-sonnet-4-5",
complex: "claude-sonnet-4-5",
};
/**
* タスクの複雑度を推定する関数の例。
* 実際の複雑度判定は用途に合わせて実装してください。
*/
function estimateComplexity(
prompt: string,
taskType: string
): TaskComplexity {
// 単純なルールベースの例
const simpleTaskTypes = ["classify", "extract_field", "format_convert"];
const complexTaskTypes = ["reasoning", "code_generation", "analysis"];
if (simpleTaskTypes.includes(taskType)) return "simple";
if (complexTaskTypes.includes(taskType)) return "complex";
// プロンプト長も参考にする(あくまで一指標として)
if (prompt.length > 3000) return "complex";
if (prompt.length < 500) return "simple";
return "moderate";
}
async function routedCompletion(
client: Anthropic,
prompt: string,
taskType: string,
routing: RoutingConfig = DEFAULT_ROUTING
): Promise<{ content: string; modelUsed: string; usage: TokenUsage }> {
const complexity = estimateComplexity(prompt, taskType);
const modelId = routing[complexity];
const response = await client.messages.create({
model: modelId,
max_tokens: complexity === "simple" ? 256 : 1024,
messages: [{ role: "user", content: prompt }],
});
const usage: TokenUsage = {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
cacheCreationTokens: response.usage.cache_creation_input_tokens ?? 0,
cacheReadTokens: response.usage.cache_read_input_tokens ?? 0,
};
const content =
response.content[0].type === "text" ? response.content[0].text : "";
return { content, modelUsed: modelId, usage };
}
実際の実装では、ルーティングのロジックを単純なルールベースに留めず、過去の品質評価データを使って動的に調整するアプローチも考えられます。 「Haikuで回答したものを週次でSonnetと比較し、品質差が許容範囲内ならHaikuを継続」という運用の仕組みを持つと、精度とコストのバランスを保ちやすくなります。
コスト比較シミュレーション
同じ用途で複数のモデル候補を比較する際に使える試算ヘルパーです。
interface ModelComparison {
modelId: string;
avgInputTokens: number;
avgOutputTokens: number;
avgCacheReadTokens: number;
requestsPerMonth: number;
}
function simulateMonthlyCost(config: ModelComparison): number {
const usage: TokenUsage = {
inputTokens: config.avgInputTokens,
outputTokens: config.avgOutputTokens,
cacheCreationTokens: 0,
cacheReadTokens: config.avgCacheReadTokens,
};
const costPerRequest = calculateCostUSD(usage, config.modelId);
return costPerRequest * config.requestsPerMonth;
}
// 比較例(数値はあくまで仮値です)
const scenarios: ModelComparison[] = [
{
modelId: "claude-sonnet-4-5",
avgInputTokens: 2000,
avgOutputTokens: 500,
avgCacheReadTokens: 0,
requestsPerMonth: 10_000,
},
{
modelId: "claude-haiku-4-5",
avgInputTokens: 2000,
avgOutputTokens: 500,
avgCacheReadTokens: 0,
requestsPerMonth: 10_000,
},
];
for (const scenario of scenarios) {
const monthlyCostUSD = simulateMonthlyCost(scenario);
console.log(
`${scenario.modelId}: 月額 $${monthlyCostUSD.toFixed(2)}(試算例)`
);
}
// 出力例(実際の値は単価設定に依存します)
// claude-sonnet-4-5: 月額 $135.00(試算例)
// claude-haiku-4-5: 月額 $24.00(試算例)
この試算でわかる通り、同じトークン量でもモデルによってコストが大きく異なります。 「品質が許容範囲内であれば軽量モデルを使う」という判断基準を持つだけで、コスト構造が変わってきます。
07出力トークンを最小化するプロンプト設計
レスポンス形式の制御
出力トークンを減らす最もシンプルな方法は、不要な説明を省いた形式で返させることです。 プロンプトに出力形式を明示するだけで、トークン数を大きく変えられます。
// 出力が膨らみやすい指示の例
const verboseInstruction = `
以下のテキストを分析して、感情(ポジティブ/ネガティブ/中立)を判定してください。
詳しく説明してください。
`;
// 出力を絞った指示の例
const conciseInstruction = `
以下のテキストの感情を判定し、JSON形式で返してください。
形式: {"sentiment": "positive" | "negative" | "neutral", "confidence": 0.0-1.0}
説明は不要です。JSONのみ返してください。
`;
verboseInstruction では200〜400トークン程度の説明文が返ってくることがありますが、
conciseInstruction では50トークン未満に収まることが多いです(タスクの性質や入力内容によって変わります)。
構造化出力(Structured Output)の活用
JSON Schemaによる構造化出力を使うと、余分な説明文を排除しやすくなります。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const SENTIMENT_TOOL = {
name: "output_sentiment",
description: "感情分析の結果を構造化して出力します",
input_schema: {
type: "object" as const,
properties: {
sentiment: {
type: "string",
enum: ["positive", "negative", "neutral"],
description: "テキストの感情分類",
},
confidence: {
type: "number",
description: "分類の確信度(0.0〜1.0)",
},
keyPhrases: {
type: "array",
items: { type: "string" },
description: "判断の根拠となったフレーズ(最大3個)",
maxItems: 3,
},
},
required: ["sentiment", "confidence"],
},
};
async function analyzeSentiment(
text: string
): Promise<{ sentiment: string; confidence: number; keyPhrases: string[] }> {
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 256, // 構造化出力なので短めで十分です
tools: [SENTIMENT_TOOL],
tool_choice: { type: "tool", name: "output_sentiment" },
messages: [
{
role: "user",
content: `以下のテキストの感情を分析してください:\n\n${text}`,
},
],
});
const toolUse = response.content.find((b) => b.type === "tool_use");
if (!toolUse || toolUse.type !== "tool_use") {
throw new Error("ツール呼び出しが返りませんでした");
}
const result = toolUse.input as {
sentiment: string;
confidence: number;
keyPhrases?: string[];
};
return {
sentiment: result.sentiment,
confidence: result.confidence,
keyPhrases: result.keyPhrases ?? [],
};
}
// 使用例
const result = await analyzeSentiment(
"このサービスのサポートチームは素晴らしく、問題が即座に解決されました。"
);
console.log(result);
// 出力例: { sentiment: 'positive', confidence: 0.97, keyPhrases: ['素晴らしく', '即座に解決'] }
max_tokens: 256 を設定している点も重要です。
これは Anthropic API のパラメータで、OpenAI の GPT-5系では同じ役割を max_completion_tokens が担います。
出力トークン上限を設けることで、意図せず長い回答が返ってきたときにコストが跳ね上がるリスクを抑えられます。
(関連記事: 構造化出力とJSONスキーマ:信頼できる形式でデータを取り出す)
出力トークン上限の適切な設定
出力トークン上限は安全上限として機能します。 タスクごとに想定される最大出力長を設定しておくことで、以下の効果が得られます。
- コスト上限の予測精度が上がる
- ハルシネーションで延々と文章を生成し続けるケースを防げる
- タイムアウトを調整する際の指標になる
パラメータ名はプロバイダによって異なります。Anthropic(Claude)では max_tokens、OpenAI の GPT-5系(gpt-5.5 / gpt-5.4-mini)では max_completion_tokens を使います。
目安として、分類・抽出タスクは128〜512、要約・説明タスクは512〜2048、コード生成・長文作成は2048〜4096程度を出発点にすると扱いやすいです。 実際の運用ログを見ながら出力トークン分布に合わせて調整していくのが確実です。
08月次コストを事前に見積もる
見積もりの考え方
月次コストを事前に試算するには、以下の4つの数値を決めます。
- 1リクエストあたりの平均入力トークン数
- 1リクエストあたりの平均出力トークン数
- 月間リクエスト数(またはユーザー数 × 1ユーザーあたりの平均リクエスト数)
- キャッシュヒット率の想定
これらが決まれば、コストは計算で出せます。
見積もり計算の実装
interface MonthlyEstimateInput {
modelId: string;
avgInputTokens: number;
avgOutputTokens: number;
cacheHitRate: number; // 0.0〜1.0
cachedTokensWhenHit: number; // キャッシュヒット時にキャッシュされるトークン数
monthlyRequests: number;
}
interface MonthlyEstimateResult {
totalCostUSD: number;
inputCostUSD: number;
outputCostUSD: number;
cacheCostUSD: number;
costPerRequest: number;
breakdown: string;
}
function estimateMonthlyCost(
input: MonthlyEstimateInput
): MonthlyEstimateResult {
const pricing = PRICING_CONFIG[input.modelId];
if (!pricing) {
throw new Error(`価格設定が見つかりません: ${input.modelId}`);
}
const { monthlyRequests, cacheHitRate, cachedTokensWhenHit } = input;
// 1リクエストあたりの平均を計算します
const avgCacheReadTokens = cacheHitRate * cachedTokensWhenHit;
const avgNormalInputTokens = input.avgInputTokens - avgCacheReadTokens;
const inputCostPerReq = (avgNormalInputTokens / 1_000_000) * pricing.inputPerMillion;
const outputCostPerReq = (input.avgOutputTokens / 1_000_000) * pricing.outputPerMillion;
const cacheCostPerReq = (avgCacheReadTokens / 1_000_000) * pricing.cacheReadPerMillion;
const costPerRequest = inputCostPerReq + outputCostPerReq + cacheCostPerReq;
const inputCostUSD = inputCostPerReq * monthlyRequests;
const outputCostUSD = outputCostPerReq * monthlyRequests;
const cacheCostUSD = cacheCostPerReq * monthlyRequests;
const totalCostUSD = costPerRequest * monthlyRequests;
const breakdown = [
`モデル: ${input.modelId}`,
`月間リクエスト数: ${monthlyRequests.toLocaleString()} 件`,
`平均入力トークン: ${input.avgInputTokens.toLocaleString()} tokens/req`,
`平均出力トークン: ${input.avgOutputTokens.toLocaleString()} tokens/req`,
`キャッシュヒット率: ${(cacheHitRate * 100).toFixed(0)}%`,
`---`,
`入力コスト: $${inputCostUSD.toFixed(2)}`,
`出力コスト: $${outputCostUSD.toFixed(2)}`,
`キャッシュ読み出しコスト: $${cacheCostUSD.toFixed(2)}`,
`合計(目安): $${totalCostUSD.toFixed(2)}`,
].join("\n");
return {
totalCostUSD,
inputCostUSD,
outputCostUSD,
cacheCostUSD,
costPerRequest,
breakdown,
};
}
// 試算例
const estimate = estimateMonthlyCost({
modelId: "claude-sonnet-4-5",
avgInputTokens: 2500, // システムプロンプト1500 + ユーザー入力1000(目安)
avgOutputTokens: 600,
cacheHitRate: 0.8,
cachedTokensWhenHit: 1500, // システムプロンプト部分がキャッシュされる想定
monthlyRequests: 50_000,
});
console.log(estimate.breakdown);
// (出力例 — 実際の値は単価設定に依存します)
// モデル: claude-sonnet-4-5
// 月間リクエスト数: 50,000 件
// 平均入力トークン: 2,500 tokens/req
// 平均出力トークン: 600 tokens/req
// キャッシュヒット率: 80%
// ---
// 入力コスト: $150.00
// 出力コスト: $450.00
// キャッシュ読み出しコスト: $18.00
// 合計(目安): $618.00
この試算から、出力コストが全体の7割以上を占めていることがわかります。 コスト削減を図るなら、まず「平均出力トークンを減らす」施策を検討するのが効果的です。
シナリオ比較で意思決定を支援する
「今のまま」「軽量モデルに切り替え」「キャッシュを最適化」というシナリオを並べると、意思決定の根拠になります。
const baseConfig = {
avgInputTokens: 2500,
avgOutputTokens: 600,
cacheHitRate: 0.3, // 現状のキャッシュヒット率
cachedTokensWhenHit: 1500,
monthlyRequests: 50_000,
};
const scenarios = [
{
label: "現状(Sonnet、低キャッシュ)",
config: { ...baseConfig, modelId: "claude-sonnet-4-5" },
},
{
label: "軽量モデルへの切り替え(Haiku)",
config: { ...baseConfig, modelId: "claude-haiku-4-5" },
},
{
label: "Sonnet + キャッシュ最適化(ヒット率80%)",
config: { ...baseConfig, modelId: "claude-sonnet-4-5", cacheHitRate: 0.8 },
},
{
label: "Haiku + キャッシュ最適化(ヒット率80%)",
config: { ...baseConfig, modelId: "claude-haiku-4-5", cacheHitRate: 0.8 },
},
];
console.log("シナリオ比較(月次試算)\n");
for (const { label, config } of scenarios) {
const result = estimateMonthlyCost(config);
console.log(`${label}`);
console.log(` 月額: $${result.totalCostUSD.toFixed(2)}(目安)`);
console.log(` 1リクエストあたり: $${result.costPerRequest.toFixed(5)}\n`);
}
// 出力例(実際の値は単価設定に依存します)
// シナリオ比較(月次試算)
//
// 現状(Sonnet、低キャッシュ)
// 月額: $780.00(目安)
// 1リクエストあたり: $0.01560
//
// 軽量モデルへの切り替え(Haiku)
// 月額: $136.00(目安)
// 1リクエストあたり: $0.00272
//
// Sonnet + キャッシュ最適化(ヒット率80%)
// 月額: $618.00(目安)
// 1リクエストあたり: $0.01236
//
// Haiku + キャッシュ最適化(ヒット率80%)
// 月額: $106.00(目安)
// 1リクエストあたり: $0.00212
この比較から、「Haikuへの切り替え + キャッシュ最適化」の組み合わせが最もコストを下げる選択肢だとわかります。 ただし、「品質が許容範囲内かどうか」は数字だけでは判断できません。 見積もりはあくまで選択肢を評価するための道具であり、最終的な判断には品質検証が必要です。
09BizPlanエージェントでの適用例
私たちが開発する BizPlan(事業計画エージェント)でも、上記の考え方を設計に取り込んでいます。
設計思想として取り込んでいるのは以下の点です。
コスト区分ごとのモニタリング: 各APIリクエストのレスポンスからトークン使用量を記録し、タスク種別・フェーズ別に集計しています。 「どのフェーズで出力トークンが多いか」を把握することで、最適化の優先順位をつけやすくなっています。
システムプロンプトのキャッシュ設計: 事業評価フレームワークや各種テンプレートなど、リクエストをまたいで共通する部分は先頭に固定し、ユーザー固有の情報は後方に配置する構成にしています。 これによりキャッシュヒット率を高める設計を意識しています。
タスク種別によるモデル選択: 「市場規模の定量推計」のような複雑な推論タスクと、「入力値のバリデーション」のような定型タスクでは、要求される能力が異なります。 タスクの性質に合わせてモデルを使い分けることで、品質とコストのバランスを維持しやすくなっています。
具体的な実装の詳細については内部仕様のため一般化した形での紹介に留めますが、「計測 → 分析 → 最適化 → 再計測」というサイクルを持つことが大切だと感じています。
10最適化施策の優先順位
コスト最適化を始める際に、どこから手をつけるか迷うことがあります。 筆者が経験から感じる優先順位の考え方を紹介します。
出力トークンを絞る — 最もシンプルで即効性が高いです。プロンプトに「簡潔に答えてください」「JSONのみ返してください」と加えるだけで変わることがあります。出力トークン上限(Anthropicは
max_tokens、OpenAI GPT-5系はmax_completion_tokens)を適切に設定することも含みます。キャッシュヒット率を上げる — システムプロンプトが長く、同じ内容を多くのリクエストで使い回している場合に効果が大きいです。プロンプトの構造を「固定部分が先頭」に整理するだけで対応できます。
モデルをタスクに合わせて選ぶ — 品質評価の仕組みが整ってから取り組む施策です。評価基準なしにモデルを下げると、品質劣化に気づきにくくなります。
不要なリクエストを減らす — 同じ入力に対してキャッシュされた応答を返す、処理をバッチ化して呼び出し回数を減らす、という方向性です。アプリケーション層での工夫が必要です。
どの施策も「計測できていないと効果がわからない」という点では共通しています。 まず記録する仕組みを作ることが、最適化の出発点です。
(関連記事: ログとトレーシング:エージェントの挙動を観測可能にする)
11まとめ
この記事では、LLM APIのコスト構造と最適化手法を段階的に解説しました。
- コストの3区分(入力・出力・キャッシュ読み出し)を理解し、出力トークンの単価が相対的に高いことを把握する
- APIレスポンスからトークン使用量を記録し、タスク別・モデル別のコストを可視化する
- プロンプトキャッシュは「固定部分を先頭に配置」するだけで大きな節約になることがある
- モデル使い分けは品質評価の仕組みを持った上で取り組む
- 月次見積もりはシナリオ比較で意思決定を支援するツールとして活用する
コスト最適化は一度やって終わりではなく、サービスの利用量やユーザーの使い方の変化に合わせて継続的に見直すものです。 「計測 → 分析 → 施策 → 再計測」のサイクルを回すことが、長期的にコストをコントロールするための基本だと筆者は考えています。

