/** * Cost estimation and the spend guard. * * Hard rule #5: any operation that will spend LLM money goes through the cost * guard - estimate first, never spend silently. Hard rule #6: estimates are * estimates and say so (see {@link CostEstimate.assumptions}). */ import type { RunSpend } from './types.js'; /** Per-million-token pricing for a model. */ export interface ModelPricing { inputPerMTokens: number; outputPerMTokens: number; /** * Flat USD charged per request, on top of tokens. Some grounded engines bill a * search fee per call (Perplexity), which token-only pricing misses entirely. */ perRequestUsd?: number; /** * USD charged per individual SEARCH QUERY, on top of tokens. Distinct from * {@link perRequestUsd}: Google bills grounding per query and one request can * issue several, so the multiplier is the search count, not the call count. * Charged only when a call actually searched. */ perSearchUsd?: number; /** * Typical output tokens for one ask against THIS model, overriding * {@link EstimateAssumptions.avgOutputTokens}. * * Exists because a single global figure cannot describe both a plain chat * model and a thinking model: the global 500 was ~5x under for Gemini, whose * reasoning tokens bill as output. That gap under-quoted the confirm gate * and under-reserved the cap. Omit unless a model is measurably different. */ avgOutputTokens?: number; } /** * Checked-in pricing table. `lastUpdated` flags staleness; prices are USD per * million tokens and are approximate - good enough for a pre-run estimate, not * billing. Update the date when you touch the numbers. * * OpenAI rows retrieved 2026-07-17 from the official sheet * (https://developers.openai.com/api/docs/pricing); `gpt-5.6-sol` (the ask * default) and `chat-latest` both re-verified there 2026-08-13, each at * $5.00/$30.00. * * The ask default is a PINNED snapshot, so the id priced here and the id asked * are the same string AND that string cannot change under us - which is the * whole reason for pinning. It took two breakages to get here: the default was * `gpt-5.3-chat-latest`, which the sheet never listed (it borrowed the generic * `chat-latest` number, an assumption rather than a quote) and which OpenAI then * retired outright with HTTP 404; and then briefly `chat-latest` itself, which * is quoted but floats. A floating row can go stale with no change here and * nothing in the response to reveal it, so a snapshot is the only form of this * row that stays true on its own. * * Gemini + Perplexity rows verified live 2026-07-20 (M17 engine smoke): * - `gemini-flash-latest` $1.50/$9.00 from the official sheet * (https://ai.google.dev/gemini-api/docs/pricing, quoting `gemini-3.5-flash`, * which the alias resolved to live). That output rate EXPLICITLY includes * thinking tokens, which is why the adapter counts `thoughtsTokenCount` as * output. Same floating caveat as `-chat-latest`. The `gemini-2.5-flash` row * is kept for historical snapshots only - that model now 404s. * - `sonar` token rates confirmed against Perplexity's own `usage.cost` on a * real call ($1/$1), plus a flat `perRequestUsd` search fee of $0.005 that * token-only pricing missed entirely (7x under-report on that call). * * Anthropic rows verified 2026-07-20 against the official sheet: `claude-haiku-4-5` * $1/$5 and `claude-sonnet-5` $3/$15 were both already correct. Note Sonnet 5 is * on introductory pricing ($2/$10) through 2026-08-31, so the row over-estimates * until then - deliberate, an estimate should not under-report spend. * * Google Search grounding is modelled as of 2026-07-20 (`perSearchUsd`): $14 * per 1,000 search queries on top of tokens. The billing unit is the SEARCH, * not the request - "A customer-submitted request to Gemini may result in one * or more queries to Google Search. You will be charged for each individual * search query performed" (official sheet, retrieved 2026-07-20). RECORDED * spend is exact because the response reports the queries it ran * (`webSearchQueries` -> `fanoutQueries`); only the pre-run ESTIMATE assumes a * count, via {@link EstimateAssumptions.searchesPerGroundedCall}. This fee is * not small: on the real captured grounded call it was $0.042 against $0.034 * of tokens. * * Google also grants a free grounding allowance (5,000 prompts/month at the * time of writing), so a user inside it pays nothing. Pricing it anyway is * deliberate: an estimate must not under-report spend. */ export declare const MODEL_PRICING: { lastUpdated: string; models: Record; }; /** * Output tokens reserved for a reasoning judge's private thinking, on top of * whatever the answer itself needs. * * Reasoning models bill thinking as output and draw it from the SAME cap as the * answer, so a cap sized for the answer alone starves it. The call still returns * HTTP 200 - with an empty text block - and every parser downstream reads that * as "the judge found nothing", which is how a failed call became an empty * result with no error (rule #6). * * Measured live 2026-08-13 against claude-sonnet-5: the competitor prompt at * max_tokens 300 spent all 300 on thinking and returned zero text; the query * generation prompt did the same at 1440. Both answered cleanly with the reserve * added (2172 thinking + 2397 text on the generation prompt). * * Thinking is ADAPTIVE - the same model answered a short scoring prompt at 60 * tokens with no thinking at all - so this is a ceiling a call is allowed to * use, never a cost it always pays. Real spend is settled from reported usage. */ export declare const REASONING_RESERVE_TOKENS = 4000; /** * The output cap for a judge call that needs `answerTokens` for its answer. * * Every judge call site sizes its cap through this and never with a bare number: * a raw answer-sized cap is silently empty on a reasoning judge, and each site * that hard-coded one had to be found by hand after it had already shipped. * * This return value is the cap SENT TO THE PROVIDER, and only that - it must * NOT also be the `outputTokens` passed to {@link estimateCallUsd} for * `CostGuard.authorize`. Price that call against `answerTokens` instead. The * reserve exists so a reasoning judge is never starved of output by its own * private thinking, but it is headroom the call is ALLOWED to use, not a cost * it typically pays - pricing the reservation off this inflated cap * over-reserved by ~20x and made a modest --max-cost skip an entire judge * pass on its first call (see the call sites in `scoring/judge.ts`, * `shopping/judge.ts`, `queries/generate.ts`, `shopping/queries.ts`, and * `discovery/competitors.ts` for the full rationale). */ export declare function judgeMaxTokens(answerTokens: number): number; /** Thrown when a model id is not present in {@link MODEL_PRICING}. */ export declare class UnknownModelError extends Error { readonly model: string; constructor(model: string); } /** * Output tokens to assume for one ask against `pricing`: the model's own * figure when it has one, else the global default. * * Single source of truth so every estimator - the confirm-gate quote, the * runner's per-call reservation, the whole-run estimate - agrees. They used to * diverge, and only the runner learned the real number (from its probe), so * the figure the USER saw stayed ~5x under for thinking models. */ export declare function assumedOutputTokens(pricing: ModelPricing, assumptions?: EstimateAssumptions): number; /** * USD cost of one call given token counts and, for grounded engines, the * number of search queries the call actually issued. * * `searchQueries` defaults to 0 so every parametric caller is unchanged: a call * that ran no search is never charged a search fee. */ export declare function costOfCall(pricing: ModelPricing, inputTokens: number, outputTokens: number, searchQueries?: number): number; /** Assumptions baked into an estimate, surfaced so output can be honest. */ export interface EstimateAssumptions { avgInputTokens: number; avgOutputTokens: number; judgeInputTokens: number; judgeOutputTokens: number; judgeSampleRate: number; /** * Search queries assumed per grounded call, for engines that bill per search * ({@link ModelPricing.perSearchUsd}). Only an ESTIMATE input - recorded * spend uses the count the provider actually reported. */ searchesPerGroundedCall: number; /** Whether the run being estimated will ask engines in grounded mode. */ grounded?: boolean; } /** The default token/sampling assumptions for a run estimate. */ export declare const ESTIMATE_ASSUMPTIONS: EstimateAssumptions; /** Result of {@link estimateRun}: a pre-run cost estimate in USD. */ export interface CostEstimate { totalUsd: number; askUsd: number; judgeUsd: number; assumptions: EstimateAssumptions; } /** * Estimate the USD cost of asking `nPrompts` to each engine (by model id) plus * the sampled judge pass. Throws {@link UnknownModelError} for any model not in * the pricing table. */ export declare function estimateRun(nPrompts: number, engineModels: string[], judgeModel: string, assumptions?: EstimateAssumptions): CostEstimate; /** * Estimate the USD cost of a single judge-model call (discovery/query-gen). * Priced by the configured judge model. Unknown models fall back to the * priciest entry so the setup-budget authorization never under-estimates. */ export declare function estimateJudgeCallUsd(model: string, assumptions?: EstimateAssumptions): number; /** * Estimate the USD cost of a single call with explicit token counts, priced by * the configured model (unknown -> priciest entry). Use this when the real * input/output budget is known, so the cost guard authorizes against reality. */ export declare function estimateCallUsd(model: string, inputTokens: number, outputTokens: number): number; /** Rough token count from character length (~4 chars/token) for pre-call estimates. */ export declare function approxTokens(text: string): number; /** * The most expensive pricing in the table - a conservative unknown-model * default, so an unpriced model never under-estimates the budget. * * Ranked by the cost of a REPRESENTATIVE CALL, not by summed per-token rates: * a flat `perRequestUsd` (Perplexity's per-search fee) is invisible to a rate * sum but dominates short calls. The judge asks for ~60 output tokens, well * inside the window where the fee outweighs the rates, so rate-only ranking * returned a row that was not actually the priciest and broke the * never-under-estimate guarantee this function exists to provide (rule #5). */ export declare function priciestPricing(): ModelPricing; /** Spend phase: pre-ASK setup (discovery + query-gen) vs the main run. */ export type CostPhase = 'setup' | 'main'; /** * Accumulates actual spend and enforces caps. Per the abort contract, hitting a * cap NEVER throws: {@link CostGuard.authorize} returns `false` and sets * {@link CostGuard.costCapped}, and the caller stops. * * Authorizing RESERVES the projected cost, and {@link CostGuard.settle} * replaces that hold with the real figure once the call returns. Without the * reservation, `authorize` compared only against RECORDED spend - and recording * happens after a call completes - so every concurrently in-flight call was * authorized against a total that excluded all the others. Measured live * 2026-07-20: `--max-cost 0.20` spent $0.3481, a 74% breach (hard rule #5). */ export declare class CostGuard { private readonly caps; private readonly spent; /** Authorized-but-not-yet-settled holds, so in-flight calls count against the cap. */ private readonly reserved; private capped; constructor(caps?: { maxCostUsd?: number; maxSetupCostUsd?: number; }); get spentUsd(): number; /** * Actual spend split by phase, for reporting what a run cost. * * The guard is the only place that sees EVERY spender - discovery, query * generation, the engine asks, and the scoring judge - so a report that sums * answers instead would silently omit the setup phase. */ get spendBreakdown(): RunSpend; get costCapped(): boolean; /** * Return whether a projected spend of `usd` in `phase` is allowed. If it * would exceed the setup cap (for setup spends) or the total cap, mark * `costCapped` and return `false`. Never throws. */ authorize(usd: number, phase?: CostPhase): boolean; /** * Close out an authorized call: release its reservation and book what it * actually cost. Pass `actualUsd: 0` when the call failed or was abandoned, * so the hold does not leak and strangle the remaining budget. * * Settling can itself BREACH a cap: a reservation is a projection, and a * call that returns far more tokens (or searches) than assumed costs more * than was held for it. That money is already spent and cannot be refused, * but it must be reported - `authorize` used to be the only thing that could * set `costCapped`, so an overshoot sailed past `--max-cost` while the * envelope still claimed a clean, full-confidence run (rule #6). */ settle(reservedUsd: number, actualUsd: number, phase?: CostPhase): void; /** * Flag the run when ACTUAL spend has reached or passed a cap. Compares spend * only (not reservations): an outstanding hold is a projection that may yet * settle lower, and treating it as a breach would flag runs that never * exceeded anything. */ private flagIfOverCap; /** * Record actual spend for a call that carried NO reservation. * * Do not pair this with {@link CostGuard.authorize} - that reserves, and only * {@link CostGuard.settle} releases the hold. Using `record` after * `authorize` leaks the reservation and permanently shrinks the budget. */ record(usd: number, phase?: CostPhase): void; }