import type { PricingCatalogue } from './pricing.js'; import type { UsageRecord } from './usage.js'; /** * The same request, sent again a moment later. * * ## The failure mode nothing else here can see * * A conversation's input **grows with every turn** — that is the finding * `conversations` exists for. So two consecutive calls in one conversation * carrying *exactly* the same input size, seconds apart, is the shape of * something going wrong rather than something working: a retry after a * timeout, an agent step repeating because a tool call failed, a loop that * re-sends the whole context and gets nowhere. * * That is expensive in a way a total hides completely. The retried call is * billed in full, and on an agent workload the input is the bill. * `duplicateLines` catches the same line recorded twice; this catches two * *different* calls that sent the same thing. * * ## What it compares, and why only that * * Each call is compared to **the one immediately before it in the same * session**, and only when the log carries both a session and a clock. Not to * every earlier call: a workload that legitimately sends a fixed-size prompt * would light up under a looser rule, and holding every size a session has * ever sent would grow without bound on a log measured in megabytes. One * previous call per session is bounded and is where a retry actually sits. * * The gap must be **non-negative and under the window** (a minute by default). * Non-negative because a log is not guaranteed to be in time order, and a * negative gap means the two records arrived out of order rather than that a * call repeated. * * ## What it refuses to conclude * * It cannot see content, so it cannot tell a retry from two genuinely * identical requests a second apart. It reports the count and the money and * stops — the same rule `duplicateLines` follows. Every rendering says the * pattern is *usually* a retry or a loop, never that it is one. */ /** Consecutive same-size calls in one conversation, per slice. */ export interface RepeatedTurns { label: string; model: string; modelName: string; /** How many calls repeated the previous call's input size inside the window. */ repeats: number; /** Calls in this slice that could be checked at all — the denominator. */ checkedCalls: number; /** What those repeated calls cost, in full. */ usd: number; /** The window they had to fall inside, in milliseconds. */ withinMs: number; } export interface RepeatsOptions { catalogue: PricingCatalogue; on?: Date; /** * How close two calls have to be. A minute by default: long enough to cover * a timeout and a retry, short enough that an ordinary next turn — which * needs a human or a tool to produce it — rarely lands inside it. */ withinMs?: number; /** Slices below this many repeats are dropped. Default 2. */ minRepeats?: number; } export interface RepeatsTracker { add(record: UsageRecord): void; finish(): RepeatedTurns[]; } /** An accumulator, fed in the pass a profile already makes. */ export declare function createRepeatsTracker(options: RepeatsOptions): RepeatsTracker; /** The same measurement over a list of records, for a caller holding one. */ export declare function repeatedTurns(records: readonly UsageRecord[], options: RepeatsOptions): RepeatedTurns[]; //# sourceMappingURL=repeats.d.ts.map