模型元数据与成本计算
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 实现。