模型元資料與成本計算
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)按百萬 token 單價 × 用量,填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 六檔?因為不同 provider 的 thinking 控制粒度差異大——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,四檔 token 單價累加。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 是純算術,四檔單價除以百萬乘用量:
// 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原地修改:usage.cost直接被寫,回傳同一個物件,呼叫方別期望不可變。
小結
models.ts 是查詢/計算層,目錄資料在 models.generated.ts。getModel 查、calculateCost 算錢、getSupportedThinkingLevels / clampThinkingLevel 處理 thinking 等級對應。Model 實例拿到後,怎麼交給 provider 看 stream/complete 門面,Anthropic 怎麼用 thinkingLevelMap 看 Anthropic SSE 實作。