import type { PricingCatalogue } from './pricing.js'; import type { UsageRecord } from './usage.js'; /** * How big a call's input actually is, and how uneven that is across a slice. * * ## The half of the bill nothing described * * `outputShapes` says where the *output* spend concentrates. Input had a total * and nothing else — and on a RAG or agent workload input is most of the bill, * made of retrieved context, conversation history and tool results that no * prompt file contains. "Input is 63% of this bill" is true and unactionable; * the question somebody can act on is whether that 63% is *every* call * carrying a large prompt, or a few calls carrying an enormous one. * * Two slices with identical input spend want opposite responses: * * - **Even.** The p95 call carries roughly what the median call carries. The * prompt is simply large, and the lever is the prompt: fewer retrieved * documents, a shorter system block, caching if the prefix repeats. * - **Skewed.** The p95 call carries twelve times the median. Something is * growing — a conversation nobody truncates, a retrieval with no cap, a * tool result pasted in whole. The median call is fine and the fix is a * limit, not a rewrite. * * A total cannot tell those apart, and neither can the per-day series. * * ## What "input" means here * * Everything the model read: fresh input, cache reads and cache writes. That * is the size of the request, which is what a context window and a retrieval * cap are about. `cachedShare` then says how much of it was billed at the * cache-read rate — a tenth of input on Anthropic — because a slice whose * large calls are almost entirely cache reads is a very different bill from * one paying full rate for the same tokens, and the token counts alone cannot * tell them apart. * * ## Ceilings, never interpolations * * The counts live in fixed buckets, so a usage log measured in megabytes costs * bounded memory. Every figure reported is a **bucket edge**: "half the calls * fit within N tokens" is exact for the N named, where interpolating a median * between two buckets would invent a call nobody made. `p95OverMedian` is * therefore a ratio of two ceilings and coarse by construction — it is a shape, * not a measurement, and the copy that renders it says which. */ /** How one label-and-model slice's input is distributed across its calls. */ export interface InputShape { label: string; model: string; modelName: string; calls: number; /** Fresh input, cache reads and cache writes — everything the model read. */ inputTokens: number; /** What those tokens cost, at each class's own rate. */ inputUsd: number; /** This slice's input spend as a fraction of the whole bill. */ shareOfBill: number; /** * The bucket ceiling at least half the calls fit within, and the same for * 95% of them. `null` only when the covering bucket is the open-ended last * one, which has no ceiling to name. */ medianWithinTokens: number | null; p95WithinTokens: number | null; /** * `p95WithinTokens / medianWithinTokens` — how much bigger the large calls * are than the ordinary one. A ratio of two ceilings, so it is coarse on * purpose; `null` when either ceiling is unknown or the median ceiling is * zero. */ p95OverMedian: number | null; /** * The share of these tokens that were cache reads. * * Says what the size actually costs: on Anthropic a cache read is a tenth of * input, so a slice at 0.9 here is large and cheap, and one at 0 is large at * full rate. Without it, "the p95 call carries 400,000 tokens" reads as an * emergency in a workload that is caching correctly. */ cachedShare: number; } export interface InputShapeOptions { catalogue: PricingCatalogue; on?: Date; /** Slices whose input is below this share of the bill are dropped. Default 5%. */ minShare?: number; /** * Slices with fewer calls than this are dropped. Default 20. * * A p95 over four calls is the largest of the four wearing a percentile's * name, and the sentence this feeds — "the large calls are twelve times the * ordinary one" — would be a description of one call. */ minCalls?: number; } export interface InputShapeTracker { add(record: UsageRecord): void; finish(totalUsd: number): InputShape[]; } /** An accumulator, fed in the pass a profile already makes. */ export declare function createInputShapeTracker(options: InputShapeOptions): InputShapeTracker; /** The same measurement over a list of records, for a caller holding one. */ export declare function inputShapes(records: readonly UsageRecord[], totalUsd: number, options: InputShapeOptions): InputShape[]; //# sourceMappingURL=input-shape.d.ts.map