import type { PricingCatalogue } from './pricing.js'; import type { UsageRecord } from './usage.js'; /** * What re-sending the conversation costs. * * ## The line nothing was watching * * A chat or agent workload sends the whole conversation back on every turn. Turn * one is a system prompt and a question; turn twenty is a system prompt and * nineteen previous exchanges and a question. The input grows linearly with the * turn count, and on an agent bill that growth is routinely the largest single * line — larger than the prompt, larger than the answers. * * Nothing in this package could see it. A prompt file shows the system prompt and * not the history. A total shows the sum and not the shape. Even `profile` reported * "input is 71% of this bill" without being able to say that most of that input was * the same sentences, sent again. * * ## What it will and will not claim * * The honest figure is a **ceiling**, and the token half of it is exact: the * input tokens beyond every turn being the size of the session's smallest turn. * That quantity is order-independent — the first-seen anchor made the identical * workload vanish when exported newest-first — and it is immune to billing * rates: a cost-based anchor charged an ordinary 5-minute-TTL agent 77.5% * "growth" on a conversation that never grew, because a cache-miss turn costs * 12.5x a cache-hit turn of the same size. The dollars are that token share of * what the session actually spent, at its own blended rate. * * It is a ceiling and not a saving because part of that growth is the user's own * new messages, which nobody can truncate away, and this module cannot tell those * apart from re-sent history — it sees counts, not content. Reporting the ceiling * as an opportunity would be the flattering direction; reporting nothing because * the exact split is unknowable would be worse. So it reports the bound and says * what it is. * * ## The session key never leaves this module * * A session identifier is somebody's conversation, and in a real log it is often an * account id, a ticket number or an email. It is used to group calls and count * turns; **no figure reported anywhere carries it**, and every result is aggregated * per label. The promise that a usage log handed to Trazum contains no content is * only worth something if nothing identifying comes back out either. */ /** How one label-and-model slice grows across a conversation. */ export interface ConversationGrowth { label: string; model: string; modelName: string; /** How many distinct conversations were seen. Never which ones. */ sessions: number; calls: number; /** Mean input tokens on the smallest turn of a conversation. */ minTurnTokens: number; /** Mean input tokens on the largest turn. */ maxTurnTokens: number; /** Turns in the longest conversation seen. */ longestSession: number; /** Input-side spend: plain input, cache reads and cache writes. */ inputUsd: number; /** What that would have been if every turn had cost what its cheapest turn did. */ flatUsd: number; /** * `inputUsd - flatUsd`. **A ceiling on what removing conversation growth could * be worth, not a saving** — part of it is the user's own new messages. */ growthUsd: number; /** `growthUsd` as a fraction of the whole bill in the log. */ shareOfBill: number; } export interface ConversationOptions { catalogue: PricingCatalogue; on?: Date; /** * Slices whose growth is below this share of the bill are dropped, and slices * shorter than `minTurns` never count as conversations at all. */ minShare?: number; /** * Conversations shorter than this are ignored. * * Two turns is not a conversation, it is a retry — and a workload that never * exceeds two turns has no growth to measure, so including it would put a row on * screen whose figure is arithmetic noise. Default 3. */ minTurns?: number; } /** * Measures what conversation growth costs, from records that carry a session. * * Records without one are skipped rather than lumped together: calls from * different conversations pushed into a single bucket would report a turn count * that is really a call count, and a growth figure derived from it would be * arithmetic performed on a fiction. * * Takes records rather than a report because turn order is the whole measurement, * and a breakdown has already thrown it away. */ export interface ConversationTracker { /** Feed one parsed record. Records without a session are ignored. */ add(record: UsageRecord): void; /** The finished measurement, once the whole bill is known. */ finish(totalUsd: number): ConversationGrowth[]; } /** * An accumulator, so a profile can measure this in the pass it already makes. * * The alternative was holding every record to hand to a pure function afterwards, * and a usage log is measured in megabytes — a profile that needs the whole file in * memory to answer one question is a profile that stops working on the logs most * worth reading. What this holds is bounded by the number of **conversations**, and * only ever four numbers each. */ export declare function createConversationTracker(options: ConversationOptions): ConversationTracker; /** * The same measurement over a list of records, for a caller holding one already. * * `profileUsage` uses the tracker instead, so it never has to keep the log. */ export declare function conversationGrowth(records: readonly UsageRecord[], totalUsd: number, options: ConversationOptions): ConversationGrowth[]; //# sourceMappingURL=conversation.d.ts.map