/** * The second input to the savings report: the proxy's own ledger. * * WHY A SECOND INPUT AT ALL. `windows.ts` aggregates the analytics database, * which only the MCP tool path writes to. The proxy -- the component that * rewrites every request an agent makes and is responsible for the larger part * of what this product saves -- wrote a JSONL ledger that no savings surface * read, so `token-optimizer-savings` had to print a line disclaiming its own * scope and an operator whose savings came mostly from the proxy read the * report as the product barely working. * * NOT MERGED INTO THE ANALYTICS DATABASE. The obvious alternative was to have * the proxy write analytics rows directly, and it was rejected twice over. The * database is shared with the dashboard and three other readers, so a proxy * writing to it on every request introduces lock contention on the agent's * critical path; and the two inputs do not prove the same thing -- see * `proxy-savings.ts` -- so folding them into one column would hide the fact * that one of them carries the provider's own count as a check and the other * has no external check at all. Two inputs, reported separately. * * STREAMED AND FOLDED, NEVER COLLECTED. A ledger accumulates a line per * request for as long as it is configured; the inspect reader can keep the last * ten records in memory but a report over all time cannot keep all of them. * Every figure here is therefore accumulated as the file is read, so the peak * cost of the report is one record rather than the ledger. */ import { type AccountingRecord } from '../proxy/accounting.js'; import { type RollupRow } from './retention.js'; import type { OutputSavingsEstimate, OutputWaste } from '../proxy/output-savings.js'; import { type SavingsGroup } from './windows.js'; export interface ProxySavingsWindow { readonly label: string; readonly since: string | null; /** Every ledger row in the window, whatever it proves. */ readonly requests: number; /** Whole units the withholding arm moved out of these requests. */ readonly withheldUnits: number; /** Rows the provider accepted with a 2xx and therefore charged for. */ readonly billedRequests: number; /** Billed rows that also carry our token count of both bodies. */ readonly countedRequests: number; readonly pricedRequests: number; readonly calibratedRequests: number; readonly tokensSaved: number; readonly tokensBefore: number; readonly savingsPercent: number; readonly costUsd: number | null; /** Our count of the bodies we sent, summed over calibrated rows. */ readonly oursTokens: number; /** The provider's count of those same bodies. */ readonly billedTokens: number; /** * Rows that carried a timing block, which every latency figure below is over. * * THE DENOMINATOR IS PUBLISHED BESIDE THE FIGURES because it is not * `requests`: a ledger written by a proxy built before the timings existed * carries none, and the figures are then null rather than zero. */ readonly timedRequests: number; /** Mean milliseconds spent rewriting a request. Exact across the fold. */ readonly transformMsMean: number | null; /** The median transform, as the bound of the bucket it falls in. */ readonly transformMsP50: LatencyBound | null; /** The slow transform: what a request at the 95th percentile paid. */ readonly transformMsP95: LatencyBound | null; /** The slowest single transform in the window. */ readonly transformMsMax: number | null; /** * Mean milliseconds the provider took, which is what our share is read * against: 3 ms of transform in front of a 2-second call is a different * trade from 3 ms in front of a 30 ms one. */ readonly upstreamMsMean: number | null; /** * What compression appears to have done to the output side, ESTIMATED. * * OBSERVATIONAL, AND THE WORD IS PART OF THE FIGURE. The comparison is * against rows compression declined to act on, and it declined because they * were small or unusual -- so the two groups differ in ways besides the * treatment, in an unknown direction, and no interval covers that. The band * that travels with it covers sampling noise only. * * SIGNED. A negative figure says we made the model write more, and that is * reported as readily as the other sign. */ readonly outputEstimated: OutputSavingsEstimate; /** * What the output shaper did to the output side, MEASURED. * * NULL UNLESS A HOLDOUT ACTUALLY RAN, and the renderer then prints nothing * rather than a null result: an operator who never started the experiment * must not read "the holdout found no effect". */ readonly outputMeasured: OutputSavingsEstimate | null; /** * Tier 3: the share of a reply the model had already been shown, averaged * over the responses the opt-in scanner read. * * NULL UNLESS THE SCANNER RAN, AND NEVER A TOKEN COUNT. It is output waste * with no counterfactual -- an opportunity, not a saving -- and it is carried * in its own units so nothing downstream can add it to one. */ readonly outputWaste: OutputWaste | null; /** * What tool deferral did to the PROMPT side, MEASURED by the provider. * * INPUT TOKENS, NOT OUTPUT TOKENS, and the only field here that is. It sits * beside the output tiers because it is the same kind of evidence -- a * randomized difference read off the provider's own meter -- but its unit is * the other side of the bill, so it is never summed with them. * * IT IS THE CHECK ON A NUMBER WE ALREADY COMPUTE. The deferred schemas are * counted exactly, so `tokensSaved` above already carries deferral's effect * at prompt level. What no arithmetic of ours can show is that the provider * honoured the beta and left those schemas out of the context it charged for. * This figure is the gap between the billed prompt tokens of conversations * whose tools were deferred and those randomly withheld from the feature. * * NULL UNLESS A HOLDOUT RAN, which is the default, because a holdout makes * its control arm pay full price on purpose. */ readonly deferralMeasured: OutputSavingsEstimate | null; } /** * A latency figure the histogram can defend. * * `exceeded` SAYS THE BUCKET WAS OPEN, so the slowest requests read as "over * 2000 ms" rather than as a measurement of 2000 ms -- see `transformQuantile`. */ export interface LatencyBound { readonly ms: number; readonly exceeded: boolean; } export interface ProxySavingsReport { readonly windows: readonly ProxySavingsWindow[]; readonly byModel: readonly SavingsGroup[]; readonly totalRecords: number; /** Rows that contributed a signed token delta to the figures above. */ readonly measuredRecords: number; /** Rows whose request was never charged for: a non-2xx or a dead socket. */ readonly unbilledRecords: number; /** Billed rows with no token count -- an older proxy, or a named refusal. */ readonly uncountedRecords: number; /** Lines that were not a record. See `inspect/ledger.ts` for why skipped. */ readonly skippedLines: number; /** * Model ids a counted row named that no catalog entry prices. * * THE SAME FACT THE MCP REPORT CARRIES, under the same name, because the two * tables now sit one above the other: a note on one and silence on the other * reads as "the proxy knows every price", when both halves consult the one * catalog and both halves miss the same models. */ readonly unpricedModels: readonly string[]; /** * Rows that reached these figures as a stored day's totals, not as rows. * * DISCLOSED, NOT HIDDEN. Past the retention window a row is replaced by the * day it belonged to (`retention.ts`), which preserves every figure above * exactly but not the per-request detail behind them -- so an operator who * goes looking for a request in the ledger and finds the day gone has been * told why, by a report that says how much of itself came from where. */ readonly rolledUpRecords: number; readonly rolledUpDays: number; /** * Stored days the live rows outranked, which is how a crash mid-prune reads. * * Not an error: the rollup is written before the ledger is rewritten so that * dying between the two costs a duplicate rather than a day. This counts the * duplicates that were ignored. */ readonly supersededRollupDays: number; } export interface ProxyAggregator { add(record: AccountingRecord): void; /** One pre-folded day, from the ledger's rollup file. */ addRollup(row: RollupRow): void; /** A line that was not a record at all. Reported, never silently dropped. */ skip(): void; report(): ProxySavingsReport; } /** * THE WINDOWS NEST; THEY DO NOT PARTITION, exactly as in `windows.ts`, so each * record is folded into every window it falls inside rather than into one * bucket. Summing a column of them double-counts, which is why each one is * labelled "last N days" rather than looking like a bucket. */ export declare function createProxyAggregator(now?: Date): ProxyAggregator; /** * Reads a proxy ledger and returns its report. * * A TORN OR FOREIGN LINE IS SKIPPED AND COUNTED, never fatal -- the same trade * `inspect/ledger.ts` makes, and for the same reason: refusing to report a * week of requests because a disk filled mid-line would be the wrong answer. * The shape check is imported from there rather than copied, so a record field * added in one place cannot start being rejected in the other. */ export declare function readProxySavings(path: string, now?: Date): Promise; /** * Which of the two inputs the report actually had. * * FOUR STATES, NOT A NULLABLE REPORT. "No proxy figures" has four different * causes and three of them are actionable by the operator: the ledger was never * configured (set the variable), it is configured but empty (the proxy has not * run yet), or it exists and could not be read (a permission or a disk). Only * the fourth is a measurement. Collapsing them into an absent report would * print the same silence for all four, which is the exact defect the scope note * existed to work around. */ export declare const PROXY_INPUT: Readonly<{ readonly NotConfigured: "not-configured"; readonly Missing: "missing"; readonly Unreadable: "unreadable"; readonly Read: "read"; }>; export type ProxyInput = { readonly kind: typeof PROXY_INPUT.NotConfigured; } | { readonly kind: typeof PROXY_INPUT.Missing; readonly path: string; } | { readonly kind: typeof PROXY_INPUT.Unreadable; readonly path: string; readonly reason: string; } | { readonly kind: typeof PROXY_INPUT.Read; readonly path: string; readonly report: ProxySavingsReport; }; /** * Resolves the proxy input, reading the ledger when there is one. * * NEVER THROWS. A savings report whose first input read fine must still print; * the proxy half degrades to a named state instead of taking the command's * exit code with it. The reason text is the error's own message, which is a * local path at worst -- the same class of detail `token-optimizer-inspect` * already prints -- and it goes to the caller's terminal only. */ export declare function loadProxyInput(options?: { readonly env?: NodeJS.ProcessEnv; readonly now?: Date; readonly path?: string | null; }): Promise; //# sourceMappingURL=proxy.d.ts.map