/** * @verevoir/llm — a price book for turning {@link UsageForPricing} into USD. * * Design: fetch LiteLLM's `model_prices_and_context_window.json` at * service start-up, refresh it roughly every 24h, and fall back to the * last good table (or a caller-injected static snapshot) when a refresh * fails or looks corrupt. This module makes **zero** network calls on * import, and makes none at all unless {@link PriceBook.start} is called * — the caller (v5) owns start-up. Nothing here does cost arithmetic * beyond per-call USD: there is no aggregation, no currency conversion, * no budget logic — that is out of scope for this PR. * * Source fields read from each LiteLLM entry (USD per token): * `input_cost_per_token`, `output_cost_per_token`, * `cache_creation_input_token_cost` (5-minute-TTL cache write), * `cache_creation_input_token_cost_above_1hr` (1-hour-TTL cache write), * `cache_read_input_token_cost`, `litellm_provider`. The top-level * `sample_spec` key is not a model and is always skipped. */ /** Minimal logger this module needs — a caller-injected sink for every * "kept the last good table" / "skipped an invalid entry" warning. No * default writes to stdout/stderr: supply one, or warnings are silent. */ export interface PriceBookLogger { warn(message: string, meta?: Record): void; } /** The sliver of the `fetch` Response shape this module actually reads — * deliberately narrow so a test can inject a fake without implementing * the full Fetch API. */ export interface FetchResponseLike { ok: boolean; status: number; text(): Promise; } export type FetchLike = (url: string, init?: { signal?: AbortSignal; }) => Promise; /** One model's parsed, validated price entry. Every numeric field here is * USD per token, finite, and non-negative — {@link parseEntry} is the only * place that constructs one, and it refuses anything that isn't. */ export interface ModelPriceEntry { inputCostPerToken: number; outputCostPerToken: number; /** 5-minute-TTL cache-write rate (`cache_creation_input_token_cost`). */ cacheCreationInputTokenCost?: number; /** 1-hour-TTL cache-write rate (`cache_creation_input_token_cost_above_1hr`). */ cacheCreationInputTokenCostAbove1Hr?: number; cacheReadInputTokenCost?: number; litellmProvider?: string; } /** Keyed exactly as the source payload keys its entries — sometimes a bare * model id (`"gpt-5"`), sometimes already `provider/model` (`"mistral/ * mistral-large-latest"`). {@link PriceBook.costOf}'s lookup tries both * forms against a usage record; this type makes no claim about which form * any given key is in. */ export type PriceTable = Readonly>; /** Where the table currently in effect came from. `fetchedAt` is epoch ms * from the caller-injected `now()`. `sha256` is the hash of the raw JSON * (fetched), of an explicitly-injected snapshot's `JSON.stringify`, or of * the bundled default snapshot's own `sha256` field — not a guarantee of * upstream integrity, just a stable fingerprint for logs. */ export interface PriceSource { kind: 'fetched' | 'snapshot'; url?: string; fetchedAt?: number; sha256?: string; /** Set only when this source is the BUNDLED DEFAULT snapshot * ({@link PRICING_SNAPSHOT}) — never for a fetched source, and never * for an explicitly-injected {@link PriceBookOptions.snapshot}, which * carries no `asOf` of its own. Verbatim from that snapshot's `asOf` * field, so a caller can tell a default-snapshot cost apart from one * priced off a caller-supplied table, and see how stale it is. */ asOf?: string; } /** * What {@link PriceBook.costOf} needs about one model call. Deliberately * its own shape, not `@verevoir/llm`'s `TokenUsage` — this module prices * whatever cache-token detail a caller has, including less than * `TokenUsage` may eventually carry, and depending on that type here would * wire this PR to whatever shape that one happens to be mid-migration. * * Anthropic's `inputTokens` excludes cache tokens entirely, so a correct * total is the sum of every field below that's present, each at its own * rate — never `inputTokens` alone. */ export interface UsageForPricing { provider: string; model: string; inputTokens: number; outputTokens: number; cacheReadInputTokens?: number; /** Total cache-write tokens, when the provider reports only one number * rather than splitting by TTL. Priced at the 5-minute rate as the * closest available approximation; the result is marked `complete: false` * because that is a stated approximation, not a direct observation. */ cacheCreationInputTokens?: number; /** 5-minute-TTL cache-write tokens, when the provider splits by TTL. */ cacheCreationEphemeral5mInputTokens?: number; /** 1-hour-TTL cache-write tokens, when the provider splits by TTL. */ cacheCreationEphemeral1hInputTokens?: number; } /** * The result of pricing one call. `usdMicros` is USD × 1,000,000, rounded * to the nearest integer, so downstream aggregation never accumulates * floating-point error across many calls. `complete: false` means some * portion of the usage was priced at an approximation (or not priced at * all) because the source table didn't carry the exact rate needed — the * total is a best effort, not a guess dressed up as exact. `unpriced` * means the model itself has no entry at all: never reported as `0`. */ export type CostResult = { usdMicros: number; complete: boolean; source: PriceSource; } | { unpriced: string; }; export interface PriceBookOptions { /** Injected so this module never reaches the network itself — a test * injects a fake, the real caller injects the platform's `fetch`. */ fetch: FetchLike; url?: string; /** Default 24h. */ refreshMs?: number; /** Default {@link DEFAULT_FETCH_TIMEOUT_MS} (30_000ms). An `AbortController` * aborts the in-flight fetch after this many milliseconds; a timeout * counts as a failed refresh exactly like a thrown fetch, a non-OK * status, or invalid JSON — it keeps the last good table and warns * rather than hanging indefinitely. */ fetchTimeoutMs?: number; /** A pre-built, already-trusted table to use before any fetch succeeds * (and to fall back to if every fetch ever fails). No default is * bundled in this PR — ship one as a later change. */ snapshot?: PriceTable; /** Injectable clock, for deterministic `fetchedAt` in tests. */ now?: () => number; logger?: PriceBookLogger; } export interface PriceBook { /** * Fetch once immediately, then again every `refreshMs` on an unref'd * timer so it never keeps the process alive on its own. Calling `start` * again while already running re-triggers an immediate fetch but does * not schedule a second timer. `stop` is what ends the periodic timer. */ start(): void; /** Clear the periodic timer. Safe to call when not started, or twice. */ stop(): void; costOf(usage: UsageForPricing): CostResult; /** Number of priced models currently held — diagnostics only. */ readonly size: number; /** Where the current table came from, or `undefined` before any * snapshot or successful fetch has populated it. */ readonly source: PriceSource | undefined; } export declare const DEFAULT_PRICE_BOOK_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"; export declare const DEFAULT_REFRESH_MS: number; /** How long a single refresh's fetch may run before it's aborted and * treated as a failed refresh, same as a thrown fetch, a non-OK status, * or invalid JSON — the last good table is kept and a warning is logged. */ export declare const DEFAULT_FETCH_TIMEOUT_MS = 30000; export declare function createPriceBook(options: PriceBookOptions): PriceBook; //# sourceMappingURL=pricing.d.ts.map