Model metadata and cost calculation
models.ts is the entry point to pi-ai's model catalog — getModel looks up the registry to return a Model, calculateCost computes cost from usage, getSupportedThinkingLevels lists the thinking levels a model supports, and clampThinkingLevel snaps a requested level to the nearest legal value. The model data itself does not live in this file but in models.generated.ts (auto-generated, not covered here); models.ts only handles lookup and computation.
Responsibilities
- Catalog initialization: on module load, flatten
MODELS(frommodels.generated.ts) into aMap<provider, Map<id, Model>>, seepackages/ai/src/models.ts:4-13. - Look up model:
getModel(provider, modelId)returns aModelby provider + id, with a generic that preserves theapitype. Seepackages/ai/src/models.ts:20-26. - List providers / models:
getProviders(),getModels(provider)feed UI lists. Seepackages/ai/src/models.ts:28-37. - Cost calculation:
calculateCost(model, usage)multiplies the per-million-token price by usage, fills in each field ofusage.cost, and returns it. Seepackages/ai/src/models.ts:39-46. - Thinking levels:
getSupportedThinkingLevelsfilters out levels that arenullinmodel.thinkingLevelMap, andclampThinkingLevelsnaps a requested level to a legal value. Seepackages/ai/src/models.ts:48-80. - Model equality:
modelsAreEqualcomparesid+provider, used during UI switches. Seepackages/ai/src/models.ts:86-92.
Design motivation
Why split models.generated.ts and models.ts? Because the model catalog (pricing, context length, thinking support) is high-churn data — vendors change prices, release new models, and adjust API fields. Splitting it out of the lookup/computation logic in models.ts lets the generator produce static data in one pass while the lookup logic stays stable. This page does not cover the generator; it only covers the query side.
Why does ModelThinkingLevel use six tiers — off | minimal | low | medium | high | xhigh? Because thinking control granularity differs widely across providers — Anthropic gives budget tokens, OpenAI Responses gives effort, Google gives a thinking budget. Abstracting to 6 tiers lets thinkingLevelMap map each tier to a provider-specific parameter, with null meaning "this tier is not supported by this model". clampThinkingLevel handles cases like "request high but the model only goes up to medium" by first looking downward for the nearest legal value, ensuring the request does not error out.
Key files
packages/ai/src/models.ts:4-13—modelRegistryinitialization, a two-level Map.packages/ai/src/models.ts:15-18— theModelApiconditional type, inferring theapiliteral fromMODELS.packages/ai/src/models.ts:20-26—getModel, generic preservesapi.packages/ai/src/models.ts:39-46—calculateCost, accumulating four token-price tiers.packages/ai/src/models.ts:48-48— theEXTENDED_THINKING_LEVELSconstant, the 6-tier order.packages/ai/src/models.ts:50-59—getSupportedThinkingLevels, filtering outnull.packages/ai/src/models.ts:61-80—clampThinkingLevel, snapping downward to the nearest legal value.packages/ai/src/models.ts:86-92—modelsAreEqual, comparing both id + provider.
calculateCost is pure arithmetic — four unit prices divided by a million, multiplied by usage:
// 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 searches downward first, then upward as a fallback, guaranteeing a legal return:
// 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";Data flow
Model lookup and thinking-level resolution:
Boundaries and failure modes
- Model does not exist:
getModelreturnsundefined; the caller must handle it, seepackages/ai/src/models.ts:24-25. - No reasoning:
getSupportedThinkingLevelsreturns["off"]directly when!model.reasoning, seepackages/ai/src/models.ts:51-51. xhighspecial case: supported only whenthinkingLevelMap.xhigh !== undefined, distinct from thenullfiltering applied to other tiers, seepackages/ai/src/models.ts:56-57.- Requested level outside the list: when
requestedIndex === -1,clampThinkingLevelreturnsavailableLevels[0] ?? "off", seepackages/ai/src/models.ts:68-69. calculateCostmutates in place:usage.costis written directly and the same object is returned; callers should not assume immutability.
Summary
models.ts is the query/computation layer; the catalog data lives in models.generated.ts. getModel looks up, calculateCost computes cost, and getSupportedThinkingLevels / clampThinkingLevel handle thinking-level mapping. Once a Model instance is in hand, see stream/complete facade for how it is handed to a provider, and Anthropic SSE implementation for how Anthropic uses thinkingLevelMap.