Skip to content

Model metadata and cost calculation

源码版本v0.73.1

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

  1. Catalog initialization: on module load, flatten MODELS (from models.generated.ts) into a Map<provider, Map<id, Model>>, see packages/ai/src/models.ts:4-13.
  2. Look up model: getModel(provider, modelId) returns a Model by provider + id, with a generic that preserves the api type. See packages/ai/src/models.ts:20-26.
  3. List providers / models: getProviders(), getModels(provider) feed UI lists. See packages/ai/src/models.ts:28-37.
  4. Cost calculation: calculateCost(model, usage) multiplies the per-million-token price by usage, fills in each field of usage.cost, and returns it. See packages/ai/src/models.ts:39-46.
  5. Thinking levels: getSupportedThinkingLevels filters out levels that are null in model.thinkingLevelMap, and clampThinkingLevel snaps a requested level to a legal value. See packages/ai/src/models.ts:48-80.
  6. Model equality: modelsAreEqual compares id + provider, used during UI switches. See packages/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

calculateCost is pure arithmetic — four unit prices divided by a million, multiplied by usage:

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 searches downward first, then upward as a fallback, guaranteeing a legal return:

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";

Data flow

Model lookup and thinking-level resolution:

Boundaries and failure modes

  • Model does not exist: getModel returns undefined; the caller must handle it, see packages/ai/src/models.ts:24-25.
  • No reasoning: getSupportedThinkingLevels returns ["off"] directly when !model.reasoning, see packages/ai/src/models.ts:51-51.
  • xhigh special case: supported only when thinkingLevelMap.xhigh !== undefined, distinct from the null filtering applied to other tiers, see packages/ai/src/models.ts:56-57.
  • Requested level outside the list: when requestedIndex === -1, clampThinkingLevel returns availableLevels[0] ?? "off", see packages/ai/src/models.ts:68-69.
  • calculateCost mutates in place: usage.cost is 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.