モデルメタデータとコスト計算
models.ts は pi-ai のモデルカタログの入口だ。getModel がレジストリを引いて Model を取り、calculateCost が usage に基づいて料金を算出し、getSupportedThinkingLevels があるモデルのサポートする思考レベルを列挙し、clampThinkingLevel がリクエストされたレベルを最も近い合法値へ吸着させる。モデルデータそのものはこのファイルにはなく、models.generated.ts(自動生成、本ページでは展開しない)にある。models.ts は検索と計算だけを行う。
役割
- カタログ初期化: モジュール読み込み時に
MODELS(models.generated.tsから)をフラット化してMap<provider, Map<id, Model>>にする。packages/ai/src/models.ts:4-13参照。 - モデル検索:
getModel(provider, modelId)は provider + id でModelを取る。ジェネリックでapi型を保持する。packages/ai/src/models.ts:20-26参照。 - provider / モデルの列挙:
getProviders()、getModels(provider)は UI のリスト表示用。packages/ai/src/models.ts:28-37参照。 - コスト計算:
calculateCost(model, usage)は 100 万トークンあたりの単価 × 使用量で、usage.costの各フィールドを埋めて返す。packages/ai/src/models.ts:39-46参照。 - 思考レベル:
getSupportedThinkingLevelsはmodel.thinkingLevelMapのnullであるレベルをフィルタし、clampThinkingLevelはリクエストされたレベルを合法値に吸着させる。packages/ai/src/models.ts:48-80参照。 - モデル同士の同一性:
modelsAreEqualはid+providerで比較する。UI の切り替え時に使う。packages/ai/src/models.ts:86-92参照。
設計動機
なぜ models.generated.ts と models.ts を分けるのか? モデルカタログ(価格・コンテキスト長・thinking サポート)は高頻度で変化するデータだからだ。値下げ、新モデルの追加、API フィールドの調整があればすべて変更が必要になる。これを models.ts の検索/計算ロジックから剥がし、ジェネレータを一度走らせて静的データを生成する。検索ロジックは安定して動く。本ページではジェネレータは書かず、検索の側面だけを扱う。
なぜ ModelThinkingLevel は off | minimal | low | medium | high | xhigh の 6 段階なのか? provider ごとに思考制御の粒度が大きく違うからだ。Anthropic は budget tokens、OpenAI Responses は effort、Google は thinking budget を与える。これを 6 段階に統一抽象化し、thinkingLevelMap が各段階を provider 固有のパラメータにマップする。null は「この段階はこのモデルではサポートしない」を表す。clampThinkingLevel は「high をリクエストしたがモデルは medium まで」といった場合に、まず下に最も近い合法値を探し、リクエストがエラーにならないようにする。
主要ファイル
packages/ai/src/models.ts:4-13—modelRegistryの初期化。二層 Map。packages/ai/src/models.ts:15-18—ModelApi条件型。MODELSからapiリテラルを推論する。packages/ai/src/models.ts:20-26—getModel。ジェネリックでapiを保持。packages/ai/src/models.ts:39-46—calculateCost。四段階のトークン単価を加算。packages/ai/src/models.ts:48-48—EXTENDED_THINKING_LEVELS定数。6 段階の順序。packages/ai/src/models.ts:50-59—getSupportedThinkingLevels。nullをフィルタ。packages/ai/src/models.ts:61-80—clampThinkingLevel。下に近い合法値へ吸着。packages/ai/src/models.ts:86-92—modelsAreEqual。id + provider の二重比較。
calculateCost は純粋な算算で、四段階の単価を 100 万で割って使用量を掛ける:
// 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 はまず下に探し、次に上に戻り、必ず合法値を返す:
// 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 レベルの解決:
境界と失敗
- モデルが存在しない:
getModelはundefinedを返す。呼び出し側が自前で処理する必要がある。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参照。- リクエストレベルがリスト外:
clampThinkingLevelはrequestedIndex === -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 実装 を参照。