/** * In the path of the call, and still only able to say yes or no. * * 1.44 gave a local service that *answers* and 1.45 gave an agent a guard it * may consult and ignore. Advice an implementation can skip is advice a budget * cannot rely on, and a connector that pulls usage after the fact always * reports the runaway after it ran. Standing in the path fixes both: usage is * measured at the moment of the call, and a refusal is a refusal. * * It also makes this the most dangerous module in the product, and the two * rules below are the whole design. * * ## It refuses; it never substitutes * * A call over budget is **rejected**, with a machine-readable reason and the * cheaper alternative named. Silently swapping the model, trimming the prompt * or downgrading a request in flight is the one behaviour this product must * never have. The caller asked for something specific; a proxy that quietly * answers a different question is worse than one that fails, because the * failure is visible and the substitution is not. * * That is enforced in the *type*, not in a comment. A `GatewayDecision` is * either `forward` — carrying nothing the caller did not send — or `refuse`, * carrying no body at all. There is no shape in which this module hands back a * modified request, so no future edit can add one without changing a type that * every caller and every test reads. * * Substitution exists only as an operator's configured, logged decision, and * even then it is a *different kind*: `substitute` names what changed and why, * and every call that took it is marked so no later report treats it as the * call the caller made. * * ## Failure is a decision made in advance * * When the gateway cannot tell — no budget, nothing measured, an unpriced * model — somebody has to have already decided what happens. **Fail-open** and * **fail-closed** are both defensible: one keeps the product working and lets * the bill run, the other stops the bill and takes the product down with it. * There is deliberately no default. A proxy that picks silently has made the * most consequential decision in somebody's architecture on their behalf. * * ## Nothing about the payload is recorded * * Prompt and completion pass through. The store has held aggregates since * 1.42 and standing in the path changes nothing about that: this module never * receives the body text at all — it is handed a *description* of the call, * and the shape of its inputs is what makes the promise checkable. */ import type { PlanAssumption } from './plan.js'; import type { PricingCatalogue } from './pricing.js'; import type { MeasuredPosition, PolicyJudgement } from './judgement.js'; import type { LimitsConfig, WaiveEntry } from './config-schema.js'; /** * What the operator has decided happens when the gateway cannot judge. * * No default, and `cannot-tell` is not one of the values: this is the answer * to *what do we do about* not being able to tell, which is a policy and not a * measurement. */ export type FailurePolicy = 'fail-open' | 'fail-closed'; export declare const FAILURE_POLICIES: readonly FailurePolicy[]; /** * A call, as the gateway sees it. * * **No prompt text, and no completion text.** The model, the counts and the * label are everything a budget decision needs, and they are everything this * module is given — so "nothing about the payload is recorded" is a fact about * the interface rather than a discipline somebody has to maintain. */ export interface GatewayCall { provider: string; model: string; /** Input tokens the caller declared, or that the wire format made countable. */ inputTokens: number | null; /** The ceiling the caller asked for, when the request named one. */ maxOutputTokens: number | null; /** The workload, when the caller labelled it. */ label: string | null; /** * The conversation, when the caller identified it — `metadata.trazum_session`, * the same seam as the label. Used to judge the per-session ceiling and * never recorded, printed, or forwarded anywhere by this module. */ session: string | null; } /** Where the budget stands, as the gateway was told at the last refresh. */ export interface GatewayStanding { limitUsd: number; consumedUsd: number; /** Always measured — a gateway decision never rests on an estimate of spend. */ provenance: 'measured'; /** How stale the figure is, so a refusal can say what it rested on. */ asOfMs: number; } export type RefuseReason = /** The budget is already past its limit, measured. Nothing was estimated. */ 'budget-exhausted' /** This call would take it past, on an estimate of this call. */ | 'call-would-cross' /** A ceiling in the `limits` policy is over — the judgement names which. */ | 'limit-over' /** Cannot tell, and the operator chose fail-closed. */ | 'cannot-tell-and-closed'; export type CannotTellCause = 'no-budget' | 'nothing-measured' | 'model-unpriced' /** A ceiling in the `limits` policy could not be judged — see `policy`. */ | 'limit-unjudged'; /** A cheaper way to make the same call, named on a refusal. */ export interface GatewayAlternative { kind: 'route' | 'batch'; model: { id: string; displayName: string; } | null; savingUsd: number; assumes: PlanAssumption[]; } /** * The decision, and the only three shapes it comes in. * * `forward` deliberately carries **nothing**. Not a rewritten model, not a * trimmed prompt, not a header to add — because a field for any of those is * how substitution arrives one refactor later, wearing a reasonable name. */ export type GatewayDecision = { kind: 'forward'; /** What this call was priced at, for the record the caller keeps. */ estimatedUsd: number | null; /** Present when the gateway could not judge and the operator fails open. */ unjudged: CannotTellCause | null; /** The limits policy, judged for this call — same judge as every door. */ policy: PolicyJudgement; } | { kind: 'refuse'; reason: RefuseReason; /** Which cause, when the reason is `cannot-tell-and-closed`. */ cause: CannotTellCause | null; /** What the refusal rests on. Never `estimated` alone. */ restsOn: 'measured' | 'measured+estimated' | null; standing: GatewayStanding | null; estimatedUsd: number | null; /** A refusal never arrives bare. Dearest saving first; may be empty. */ alternatives: GatewayAlternative[]; because: string; /** The limits policy, judged for this call — same judge as every door. */ policy: PolicyJudgement; } | { /** * The operator configured a substitution, in advance, for this case. * * A separate kind rather than a `forward` with a changed model, so that * nothing downstream can treat a substituted call as the call the caller * made. `markedInStore` is not a suggestion: the marker is what stops a * later report from attributing this traffic to a model the caller never * asked for. */ kind: 'substitute'; to: { id: string; displayName: string; }; /** The operator's own words for why this rule exists. */ configuredReason: string; estimatedUsd: number | null; markedInStore: true; /** The limits policy, judged for this call — same judge as every door. */ policy: PolicyJudgement; }; export interface GatewayPolicy { /** Required. There is no default — see the module note. */ onCannotTell: FailurePolicy; /** * Substitutions the operator configured in advance, by model id. * * Absent means refuse rather than swap, which is the only safe default for * a field whose whole risk is being switched on without anybody noticing. */ substitute?: Record; } export interface GatewayOptions { catalogue: PricingCatalogue; policy: GatewayPolicy; on?: Date; /** * The `limits` block and the measured position it is judged against — * chapter three of the 1.66 arc. Both optional: a gateway with no limits * config behaves exactly as before, and the judgement is `no-policy`. * * The judging is `judgeLimits`, verbatim — the same function `serve` and * `spend_guard` call, which is the whole point: three doors, one judge, * no door doing its own arithmetic. */ limits?: LimitsConfig; position?: MeasuredPosition; /** The config's `waive` list — a silenced limit forwards, on the record. */ waivers?: readonly WaiveEntry[]; } /** * Yes, no, or the operator's pre-made decision — for one call. * * Pure, so the rule that matters most (this never rewrites a request) is * checkable without a socket, and so the proxy around it has nothing to do but * move bytes. */ export declare function gatewayDecision(call: GatewayCall, standing: GatewayStanding | null, options: GatewayOptions): GatewayDecision; /** * Which provider's response shape a provider speaks. * * Several providers serve the OpenAI wire format rather than one of their own, * and reading `prompt_tokens` out of them is the same code. Kept as a map so * the fact lives once: the buffered reader and the streaming reader resolve * through it, and neither grows a second list of provider names to fall out of * step with the first. * * A provider absent from this map is one whose shape nobody has established — * `usageFromResponse` returns null for it rather than guessing at fields, which * the gateway then reports as an unmeasured call. */ export declare const WIRE_SHAPES: Readonly>; export type WireShape = 'anthropic' | 'openai' | 'google'; /** * What a provider reported for one call, however it arrived. * * Named for the gateway rather than `MeasuredUsage`, which `measured-profile.ts` * already uses for a different thing — a label's coverage across a log. Two * types with one name is a rename waiting to be got wrong. */ export interface GatewayUsage { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; } /** * Reads a streaming response's usage as it goes past, keeping none of it. * * A streamed answer carries its token counts in events rather than in a JSON * body, so the buffering reader below cannot see them. This one is fed the * bytes on their way to the caller and holds three numbers and a partial line * — never the text. That is the same promise the proxy makes for the buffered * path, kept structurally rather than by intention. * * **Anthropic** puts the input and cache counts on `message_start` and the * running output count on each `message_delta`; the last one wins, because it * is cumulative rather than incremental. * * **OpenAI** sends usage only when the caller asked for it with * `stream_options: {include_usage: true}`. Without that the stream carries no * counts at all, and `done()` returns null — which the gateway records as * nothing rather than as zero. A call whose usage never arrived is not a free * call, and the flattering direction is the one this project must not round to. */ export interface StreamingUsageReader { /** Feed the bytes going past. Safe to call with partial lines. */ push(chunk: string): void; /** What the provider reported, or null when the stream carried no counts. */ done(): GatewayUsage | null; } export declare function streamingUsageReader(provider: string): StreamingUsageReader; /** * The tokens a provider's own response reports, from the response body. * * This is the reason the gateway measures better than a connector: the counts * are the provider's, arriving with the answer, before any export exists. * * Returns null rather than zero when the body carries no usage. A response * whose usage could not be read is a call whose cost is unknown, and a zero * would make the period's total quietly too low — the flattering direction. */ export declare function usageFromResponse(provider: string, body: unknown): { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; } | null; //# sourceMappingURL=gateway.d.ts.map