Skip to content

モデルメタデータとコスト計算

源码版本v0.73.1

models.ts は pi-ai のモデルカタログの入口だ。getModel がレジストリを引いて Model を取り、calculateCost が usage に基づいて料金を算出し、getSupportedThinkingLevels があるモデルのサポートする思考レベルを列挙し、clampThinkingLevel がリクエストされたレベルを最も近い合法値へ吸着させる。モデルデータそのものはこのファイルにはなく、models.generated.ts(自動生成、本ページでは展開しない)にある。models.ts は検索と計算だけを行う。

役割

  1. カタログ初期化: モジュール読み込み時に MODELS(models.generated.ts から)をフラット化して Map<provider, Map<id, Model>> にする。packages/ai/src/models.ts:4-13 参照。
  2. モデル検索: getModel(provider, modelId) は provider + id で Model を取る。ジェネリックで api 型を保持する。packages/ai/src/models.ts:20-26 参照。
  3. provider / モデルの列挙: getProviders()getModels(provider) は UI のリスト表示用。packages/ai/src/models.ts:28-37 参照。
  4. コスト計算: calculateCost(model, usage) は 100 万トークンあたりの単価 × 使用量で、usage.cost の各フィールドを埋めて返す。packages/ai/src/models.ts:39-46 参照。
  5. 思考レベル: getSupportedThinkingLevelsmodel.thinkingLevelMapnull であるレベルをフィルタし、clampThinkingLevel はリクエストされたレベルを合法値に吸着させる。packages/ai/src/models.ts:48-80 参照。
  6. モデル同士の同一性: modelsAreEqualid + provider で比較する。UI の切り替え時に使う。packages/ai/src/models.ts:86-92 参照。

設計動機

なぜ models.generated.tsmodels.ts を分けるのか? モデルカタログ(価格・コンテキスト長・thinking サポート)は高頻度で変化するデータだからだ。値下げ、新モデルの追加、API フィールドの調整があればすべて変更が必要になる。これを models.ts の検索/計算ロジックから剥がし、ジェネレータを一度走らせて静的データを生成する。検索ロジックは安定して動く。本ページではジェネレータは書かず、検索の側面だけを扱う。

なぜ ModelThinkingLeveloff | minimal | low | medium | high | xhigh の 6 段階なのか? provider ごとに思考制御の粒度が大きく違うからだ。Anthropic は budget tokens、OpenAI Responses は effort、Google は thinking budget を与える。これを 6 段階に統一抽象化し、thinkingLevelMap が各段階を provider 固有のパラメータにマップする。null は「この段階はこのモデルではサポートしない」を表す。clampThinkingLevel は「high をリクエストしたがモデルは medium まで」といった場合に、まず下に最も近い合法値を探し、リクエストがエラーにならないようにする。

主要ファイル

calculateCost は純粋な算算で、四段階の単価を 100 万で割って使用量を掛ける:

typescript
// packages/ai/src/models.ts:39-46
export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"] {
	usage.cost.input = (model.cost.input / 1000000) * usage.input;
	usage.cost.output = (model.cost.output / 1000000) * usage.output;
	usage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;
	usage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite;
	usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
	return usage.cost;
}

clampThinkingLevel はまず下に探し、次に上に戻り、必ず合法値を返す:

typescript
// packages/ai/src/models.ts:71-79
	for (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {
		const candidate = EXTENDED_THINKING_LEVELS[i];
		if (availableLevels.includes(candidate)) return candidate;
	}
	for (let i = requestedIndex - 1; i >= 0; i--) {
		const candidate = EXTENDED_THINKING_LEVELS[i];
		if (availableLevels.includes(candidate)) return candidate;
	}
	return availableLevels[0] ?? "off";

データフロー

モデル検索と thinking レベルの解決:

境界と失敗

  • モデルが存在しない: getModelundefined を返す。呼び出し側が自前で処理する必要がある。packages/ai/src/models.ts:24-25 参照。
  • reasoning なし: getSupportedThinkingLevels!model.reasoning の時に直接 ["off"] を返す。packages/ai/src/models.ts:51-51 参照。
  • xhigh 特例: thinkingLevelMap.xhigh !== undefined の時だけサポートする。他の段階の null フィルタとは扱いが異なる。packages/ai/src/models.ts:56-57 参照。
  • リクエストレベルがリスト外: clampThinkingLevelrequestedIndex === -1 の時に availableLevels[0] ?? "off" を返す。packages/ai/src/models.ts:68-69 参照。
  • calculateCost の in-place 変更: usage.cost が直接書き込まれ、同じオブジェクトが返る。呼び出し側はイミュータビリティを期待してはいけない。

まとめ

models.ts は検索/計算層で、カタログデータは models.generated.ts にある。getModel が検索し、calculateCost が料金を算出し、getSupportedThinkingLevels / clampThinkingLevel が thinking レベルのマッピングを処理する。Model インスタンスを取得した後、それをどう provider に渡すかは stream/complete ファサード、Anthropic が thinkingLevelMap をどう使うかは Anthropic SSE 実装 を参照。