import type { PricingCatalogue } from './pricing.js'; import type { ModelPricing } from './types.js'; import type { ReceiptDocument } from './receipt.js'; import type { UsageBreakdown, UsageProfileReport } from './usage.js'; /** * The same tokens at another model's rates. * * ## Why this is allowed to exist * * `usage.ts` refuses to report a saving, and the reason is that a saving * requires imagining a prompt nobody wrote. This does not. Every token here * was actually billed; the only thing being changed is the rate card it is * multiplied by, which is the same move `cacheEconomics` makes and is * arithmetic rather than a guess. * * What it answers is the question a bill provokes and a total cannot settle: * `classify` spent $4,000 on a frontier model — what would those exact calls * have cost on the small one? That is a number, and it is the number a routing * decision is argued over. * * ## The four things it refuses * * **It says nothing about whether the answers would be as good.** This is * multiplication. Whether the cheap model can do the work is a question about * the work, and Trazum has never seen the prompts — by design, there is nowhere * in a usage record to put one. Every rendering states this next to the figure, * because a dollar number with no caveat attached reads as a recommendation. * * **It refuses to price traffic that would not fit.** A cheaper model with a * smaller context window does not make a 400k-token call cheaper; it makes it * impossible. Slices holding a call larger than the target's window are pulled * out into `overContext` and their money is excluded from every total here — * counting an impossible call's price difference as a saving is exactly the * flattering direction this repository refuses. `maxCallInputTokens` is the * maximum rather than an average for the same reason: one call over the ceiling * is a failed call, and a mean hides it. * * **It excludes what is already there.** Calls billed on the target model * reprice to themselves, and folding them in would pad both totals with money * that cannot move — a $10,000 bill of which $9,900 is already on the cheap * model would report a 1% difference and read as "not worth doing". They are * counted in `alreadyOnTarget` instead, so the reader sees the shape. * * **It assumes the token counts survive the move, and says so.** A different * model tokenizes differently, and one that is worse at the task may answer at * greater length or be retried. The counts are the ones that were billed, not a * prediction; that makes this an exact restatement of the past at another price * and an approximation of the future. Stated, not hidden — `sameTokensAssumed` * exists so no rendering can forget to say it. * * ## Why the write TTLs are kept apart * * A cache write costs 1.25x input at five minutes and 2x at an hour, and that * ratio is not a constant across providers. Repricing a combined write total * would mean picking one of the two rates for tokens that were billed at both, * so `UsageBreakdown` carries the split and this reads it. Writes whose TTL the * log never stated are in the 5-minute bucket, the same assumption * `cacheWriteUsdIfAssumed1h` prices — `assumedWriteTtlCalls` is carried through * so a reader knows the comparison inherits it. */ /** One label-and-model slice, as billed and as it would have been billed. */ export interface RepricedSlice { label: string; /** The model these calls were actually billed on. */ model: string; calls: number; /** What the log says this slice cost. */ currentUsd: number; /** What the same tokens cost at the target's rates. */ targetUsd: number; /** `targetUsd - currentUsd`. Negative is cheaper on the target. */ deltaUsd: number; /** The slice's largest single call, cache reads and writes included. */ maxCallInputTokens: number; /** * Present when the slice's cache traffic could not exist on the target. * * A cache entry only forms above the model's minimum prompt size, and when * even this slice's largest call sits under the target's minimum, none of * its calls could create one — so `targetUsd`, which grants the cache * traffic the target's discounted rates, is priced on entries the target * would refuse to create. That error flatters the move, which is exactly * the direction this repository refuses. `noCacheUsd` is the same tokens * with every cache token at the full input rate: the figure the target * would actually bill if the entries cannot form. The truth sits at * `noCacheUsd` (no entries) — not between the two by interpolation. * * Null when the slice has no cache traffic, or when its calls clear the * target's minimum — where the standard figure stands unqualified. */ cacheBeyondTarget: { minTokens: number; noCacheUsd: number; } | null; } /** A slice holding a call the target model could not have accepted. */ export interface OverContextSlice { label: string; model: string; calls: number; currentUsd: number; /** The call that does not fit, in tokens. */ maxCallInputTokens: number; } export interface RepriceReport { target: { id: string; displayName: string; /** The ceiling `overContext` was judged against. */ contextWindow: number; }; /** * Slices that could move, largest saving first. Ties break on the larger * bill, so the row somebody would act on comes first. */ slices: RepricedSlice[]; /** Totals over `slices` only — the money that would actually change hands. */ currentUsd: number; targetUsd: number; deltaUsd: number; /** * Slices excluded because at least one of their calls is larger than the * target's context window. Their money is in none of the totals above. */ overContext: OverContextSlice[]; /** Calls already billed on the target, and what they cost. Not repriced. */ alreadyOnTarget: { calls: number; usd: number; }; /** * Repriced calls whose write TTL the log did not state. Non-zero means both * sides of the comparison rest on the cheaper assumption. */ assumedWriteTtlCalls: number; /** * Models in the log the catalogue could not price, and how many calls they * made. Their tokens could be priced on the target, but the difference * cannot be — there is no current figure to subtract from. Named rather * than dropped, so a comparison covering half a bill cannot look complete. */ unpricedModels: string[]; unpricedCalls: number; /** * The moved bill with the target's Batch API on top, over the same movable * slices — or null when the target sells no batch discount, which is a * different statement from a $0 saving. * * The other half of "priced whole": routing and batching combine by * discounting the *target's* rates, not by adding two savings — the same * never-summed rule the levers learned when $12.60 plus $10.50 exceeded * the $21.00 slice they came from. The discount covers input and output, * cache traffic priced unchanged, matching the levers' convention exactly * so the two surfaces cannot disagree about what batching is worth. * * Whether these calls can wait for a batch window is not knowable from a * log, and nothing here claims it — the field is arithmetic, the decision * is the reader's. */ batchOnTarget: { targetUsd: number; } | null; /** Always true. A field, not a comment, so a rendering can print it. */ sameTokensAssumed: true; } /** * What a set of token counts costs on one model, at the rates in force on a * date. * * The same arithmetic `profileUsage` does per call, over an aggregate — which * is only sound because every token class is priced independently of the * others and of how many calls produced them. */ export declare function priceTokensOn(breakdown: Pick, model: ModelPricing, on?: Date): number; /** * Reprices a profile's label-and-model slices onto one target model. * * Returns `null` when the catalogue does not know the target: a comparison * against a price nobody has is worse than no comparison, and the caller is * better placed to say so in its own words than this is to invent a zero. */ /** * The least a slice has to carry to be repriced. * * Narrower than `UsageBreakdown` on purpose, and that narrowing is what lets a * **receipt** be repriced at all. A receipt is the aggregate that leaves a * machine; it does not carry a breakdown's truncation counts or its cache * economics, and it never will, because those are answers to questions a * receipt does not exist to answer. What it does carry — since 2.1.0 — is * exactly this. * * Two entry points feed it and one implementation consumes it. Two copies of * this loop, one reading profiles and one reading receipts, is the defect this * codebase has found more times than any other, and it would be worse here * than usual: the copies would drift on which traffic is refused, and a * refusal that quietly stops happening reads as a saving. */ export interface RepriceableSlice { /** `UNLABELLED` for the unlabelled bucket, never `null`. */ label: string; model: string; calls: number; inputTokens: number; cacheReadTokens: number; cacheWrite5mTokens: number; cacheWrite1hTokens: number; outputTokens: number; /** What it actually cost, as apportioned rather than as recomputed. */ totalUsd: number; maxCallInputTokens: number; assumedWriteTtlCalls: number; } /** The slices, plus what could not be priced at all. */ export interface RepriceableInput { slices: readonly RepriceableSlice[]; unpricedModels: readonly string[]; unpricedCalls: number; } /** * Reprices a profile's label-and-model slices onto one target model. * * Returns `null` when the catalogue does not know the target: a comparison * against a price nobody has is worse than no comparison, and the caller is * better placed to say so in its own words than this is to invent a zero. */ export declare function repriceProfile(report: UsageProfileReport, targetId: string, catalogue: PricingCatalogue, on?: Date): RepriceReport | null; /** * The same question, asked of a **receipt** rather than of the log behind it. * * ## Why this exists * * A receipt is what leaves a machine, and by the time anybody wants to ask * *what would these calls have cost on the small model* the log is usually * gone — deleted by a retention policy, on a runner that no longer exists, or * simply on somebody else's laptop. A comparison that can only be made where * the log still is can only be made by the person who least needs it. * * It also settles a question the receipt format had left open. The two * cache-write TTL fields say in their own comment that they exist *so a * consumer can reprice this traffic*, and until 2.1.0 no consumer could: the * refusal that protects the whole comparison — never price traffic the target * could not have accepted — needs the largest call, and the receipt did not * carry it. This is the entry point that field was added for. * * ## What it refuses, which is the same four things * * Nothing here is a second implementation. It maps a receipt's lines onto the * same slices `repriceProfile` maps a profile onto, and the one loop that * refuses over-context traffic, excludes calls already on the target, keeps * the write TTLs apart and states `sameTokensAssumed` is the loop both use. * * The unlabelled bucket travels as `UNLABELLED` rather than `null`, which is * the spelling every other consumer of these types already reads. */ export declare function repriceReceipt(document: ReceiptDocument, targetId: string, catalogue: PricingCatalogue, on?: Date): RepriceReport | null; //# sourceMappingURL=reprice.d.ts.map