import type { AggregateRow, RepriceGroup, UsageFilter, UsageRepository } from '../db/repositories/usage-repository.js'; import { type CostService } from './cost-service.js'; export interface CounterfactualScenario { model: string; /** * What these exact tokens would have cost at this model's list rates. Always * an estimate, never comparable to a `reported` figure as a single number. */ estimatedCost: number; /** Records whose tokens are included in `estimatedCost`. */ records: number; /** True for the model the tokens actually ran on, when it is one of the targets. */ isActual: boolean; /** Groups this model could not price. Normally zero: targets are priced by definition. */ unpricedGroups: number; } /** * What these tokens would have cost with no prompt caching at all. * * Unlike a model counterfactual, the token counts here really are invariant, and * that distinction is the whole reason this one is worth reporting. Cache-read * tokens ARE the context re-sent on every turn; without a cache they would have * been sent as ordinary input, one for one. The cache-write premium simply would * not have been paid, so those tokens price at the plain input rate too. * * The remaining assumption, and it is stated in the caveats rather than buried: * a cacheless run of the same work would have made the same requests with the * same context. That holds far better than "the same task on a weaker model * takes the same number of turns", which is why the model scenarios refuse to * subtract and this one is allowed to. */ export interface NoCacheScenario { /** The model these tokens actually ran on. */ model: string; /** Estimated cost as recorded, with cache rates applied. */ withCache: number; /** Estimated cost with every cache token billed at the full input rate. */ withoutCache: number; /** withoutCache - withCache. Positive means caching paid off. */ saved: number; /** `saved` as a fraction of `withoutCache`; absent when that is 0. */ savedFraction?: number; records: number; } export interface CounterfactualReport { period: { since?: string; until?: string; label: string; }; includeSubagents: boolean; /** Actual totals, with `reported` and `estimated` kept apart as everywhere else. */ overall: AggregateRow; /** The (client, model, speed) groups the scenarios were computed from. */ groups: RepriceGroup[]; /** Cheapest first. */ scenarios: CounterfactualScenario[]; /** * Per actual model: these same tokens with no caching at all. Empty when * nothing in the period used the cache. */ noCache: NoCacheScenario[]; pricingVersion: string; /** Things that would make a reader over-trust the comparison. Never empty. */ caveats: string[]; } /** * "These tokens on a different model." * * The arithmetic is easy and the honesty is not. Three rules this exists to * enforce, beyond the ones {@link CostService} already applies: * * 1. **A counterfactual is not a saving.** Token counts are not invariant across * models: the same task on a weaker model routinely takes more turns, and a * bigger context re-read on every one. This reports what these tokens would * have cost, which is not what the work would have cost. That caveat ships * with the numbers rather than living in a doc. * 2. **Re-pricing needs the client and the speed**, not just the token counts -- * reasoning tokens sit inside output for one client and beside it for * another, and fast mode bills at a premium. Hence the (client, model, speed) * grouping. * 3. **Reported cost stays reported.** The actual figure keeps its own basis and * is never merged with, or subtracted from, an estimated scenario. */ export declare class CounterfactualService { private readonly repo; private readonly costService; constructor(repo: UsageRepository, costService: CostService); /** * @param models Target models. Defaults to every model the pricing table knows. */ counterfactual(filter: UsageFilter, label: string, models?: string[]): CounterfactualReport; /** * The same tokens with every cache token billed as plain input. * * Grouped by the model that actually ran, because a blended "you saved $X" * across models would hide that the answer differs by an order of magnitude * between them. */ private noCacheScenarios; private caveats; }