import { ACCOUNTING_SPEND_COVERAGES, type AccountingSpendCoverage } from "./accounting-store-schema.js"; import { type AttemptRole, type Attribution, type FailureKind, type Outcome, type SpendPriceSource, type TokenBasis, type TokenTotalsV1 } from "./dashboard-contract.js"; /** Explicit provenance for estimates whose method was omitted or unusable. */ export declare const ACCOUNTING_UNSPECIFIED_ESTIMATION_METHOD = "unspecified"; export { ACCOUNTING_SPEND_COVERAGES, type AccountingSpendCoverage }; export type AccountingRequestId = string; export type AccountingAttemptId = string; export interface ReportedTokenFactsInput { inputTokens?: number | null; outputTokens?: number | null; cachedInputTokens?: number | null; cacheCreationInputTokens?: number | null; cacheReadInputTokens?: number | null; /** The contract spelling is accepted as well as the observer spelling. */ reportedInput?: number | null; reportedOutput?: number | null; reportedCachedInput?: number | null; observedAt?: string | null; } export interface EstimatedTokenFactsInput { inputTokens?: number | null; outputTokens?: number | null; estimatedInput?: number | null; estimatedOutput?: number | null; inputMethod?: string | null; outputMethod?: string | null; observedAt?: string | null; } export interface TokenFactsInput { reported?: ReportedTokenFactsInput | null; estimated?: EstimatedTokenFactsInput | null; /** Direct fields make it safe to pass a UsageAccumulator snapshot. */ inputTokens?: number | null; outputTokens?: number | null; cachedInputTokens?: number | null; cacheCreationInputTokens?: number | null; cacheReadInputTokens?: number | null; estimatedInputTokens?: number | null; estimatedOutputTokens?: number | null; estimatedInputMethod?: string | null; estimatedOutputMethod?: string | null; observedAt?: string | null; } /** Token totals with the two Anthropic cache facts kept as first-class facts. */ export type AccountingTokenTotals = TokenTotalsV1 & { readonly reported: TokenTotalsV1["reported"] & { readonly cacheCreationInputTokens: TokenTotalsV1["reported"]["reportedCachedInput"]; readonly cacheReadInputTokens: TokenTotalsV1["reported"]["reportedCachedInput"]; }; }; /** * The prices an attempt was priced with, PER TOKEN exactly as published. Carried on * every priced spend so a reader can re-derive the amount and see whether it was this * deployment's own publication or a reference. */ export interface AccountingSpendPrices { /** Per-token input price, or null when that kind is unpublished. */ readonly perMillionIn: number | null; /** Per-token output price, or null when that kind is unpublished. */ readonly perMillionOut: number | null; } /** * Spend for ONE attempt, in integer micro-USD, with the provenance the provenance * invariant demands. `null` (no price resolved, or no token count to price) is the * honest "unpriced"; it is never rendered as $0 by any surface in this repo. * * Rounding: each token kind is computed in exact integer micro-USD * (`tokens * pricePerMillion` is not generally integral, so it is scaled by 1e6 and * rounded HALF-UP once), then kinds are summed as integers — floating error can * never accumulate across requests because only integers are ever stored or summed. * * Coverage: * - "full" every priced token kind was priced; nothing unpriced rode alongside; * - "input_only" estimated-basis pricing with no reported usage — current spend policy * intentionally prices estimated input alone; separate estimated-output * metering never silently widens the amount; * - "partial" at least one token kind present in the usage went unpriced (cache kinds, * or one of in/out having no published price). * * Structurally identical to the store's persisted `AccountingSpendV1`; declared here * against the CONTRACT vocabulary so this module keeps its platform-free imports, * and re-declared there against the on-disk vocabulary. The lifecycle only ever emits * the two concrete `source` values, so the two shapes are assignment-compatible. */ export interface AccountingSpend { /** Exact integer micro-USD. A LOWER BOUND unless coverage is "full". */ readonly amountMicrousd: number; readonly priceSource: SpendPriceSource; /** Whose token counts were priced: provider-reported or relay-estimated. */ readonly tokenBasis: TokenBasis; /** Whose token counts: provider-reported or relay-estimated (never unknown here). */ readonly source: "provider_reported" | "relay_estimated"; readonly coverage: AccountingSpendCoverage; /** * Token kinds observed but NOT priced, per kind. null means the kind itself was not * reported; a number means it WAS reported and left out of the amount (cache kinds * are discounted by an unpublished factor, so pricing them at the base rate would * overstate spend). */ readonly unpricedTokens: { readonly cacheRead: number | null; readonly cacheCreation: number | null; readonly cachedInput: number | null; }; /** Per-million prices actually used, so the amount stays re-derivable. */ readonly pricesUsed: AccountingSpendPrices; readonly observedAt: string; } /** * Injection point keeping THIS module free of catalog/metadata imports: the server * builds one from `catalog.cachedLimits()` (which never fetches — request-path safe) * plus `resolveMetadata()`, and hands it to `createAccountingRequest`. * * Prices are PER MILLION tokens, the shape `resolveMetadata()` resolves. That unit * makes the arithmetic self-documenting: dollars-per-million-tokens equals * micro-dollars-per-token, so `tokens x pricePerMillion` IS the micro-USD amount. */ export type AccountingPricePort = (provider: string, model: string) => { readonly pricePerMillionIn: number | null; readonly pricePerMillionOut: number | null; readonly priceSource: "provider" | "reference" | null; } | null; export interface AccountingRecorderEventBase { readonly requestId: AccountingRequestId; } export interface RequestStartedEvent extends AccountingRecorderEventBase { readonly type: "request-started"; readonly startedAt: string; readonly client: string | null; readonly attribution: Attribution; readonly provider: string | null; readonly model: string | null; readonly credentialId: string | null; } export interface AttemptStartedEvent extends AccountingRecorderEventBase { readonly type: "attempt-started"; readonly attemptId: AccountingAttemptId; readonly role: AttemptRole; readonly startedAt: string; readonly attribution: Attribution; readonly provider: string | null; readonly model: string | null; readonly credentialId: string | null; } export interface AttemptCompletedEvent extends AccountingRecorderEventBase { readonly type: "attempt-completed"; readonly attemptId: AccountingAttemptId; readonly role: AttemptRole; readonly startedAt: string; readonly endedAt: string; readonly outcome: Outcome; readonly failureKind: FailureKind | null; readonly attribution: Attribution; readonly latencyMs: number | null; readonly commitMs: number | null; readonly provider: string | null; readonly model: string | null; readonly credentialId: string | null; readonly tokens: AccountingTokenTotals; /** Priced from published per-(provider, model) prices only; null = unpriced. */ readonly spend: AccountingSpend | null; } export interface RequestCompletedEvent extends AccountingRecorderEventBase { readonly type: "request-completed"; readonly endedAt: string; readonly outcome: Outcome; readonly failureKind: FailureKind | null; readonly attribution: Attribution; readonly attemptCount: number; readonly repairIncluded: boolean; readonly winningAttemptId: AccountingAttemptId | null; readonly commitAttemptId: AccountingAttemptId | null; readonly latencyMs: number | null; readonly commitMs: number | null; readonly provider: string | null; readonly model: string | null; readonly credentialId: string | null; /** Only the winning serve attempt is projected here; repair is separate. */ readonly tokens: AccountingTokenTotals; readonly spend: AccountingSpend | null; /** * Spend on serve attempts the RELAY abandoned — a hedge loser (owner decision D3, 2026-08-30). * * ⚠ **A LIST, and never summed into `spend`.** `AccountingSpend` carries ONE `pricesUsed`, one * `priceSource` and one `tokenBasis`, so merging a loser's amount into the winner's record would * attach one deployment's prices to another's tokens — the provenance defect this project's own * invariant forbids. Each entry stays honest about its own deployment, and the four-cell * aggregate in `accounting-store.ts` is where they may legitimately be summed, because those * cells are keyed BY provenance. * * ⚠ Empty for every request that ran no hedge, which is almost all of them. */ readonly abandonedSpend: readonly AccountingSpend[]; } export type AccountingEvent = RequestStartedEvent | AttemptStartedEvent | AttemptCompletedEvent | RequestCompletedEvent; export interface AccountingRecorder { record(event: AccountingEvent): void; } /** A recorder suitable as the default when no read model is mounted. */ export declare const NOOP_ACCOUNTING_RECORDER: AccountingRecorder; export type AccountingClockValue = number | Date | string; export type AccountingClock = (() => AccountingClockValue) | { now(): AccountingClockValue; }; export type AccountingIdFactory = () => string; export interface AccountingRequestOptions { recorder?: AccountingRecorder; clock?: AccountingClock; idFactory?: AccountingIdFactory; /** * Published-price lookup for spend. Absent ⇒ every attempt is unpriced, never * priced at a default. Must be synchronous and fetch-free (request path). */ pricePort?: AccountingPricePort | undefined; requestId?: string; startedAt?: AccountingClockValue; client?: string | null; attribution?: Attribution; provider?: string | null; model?: string | null; credentialId?: string | null; } export interface AttemptStartOptions { role?: AttemptRole; startedAt?: AccountingClockValue; attribution?: Attribution; provider?: string | null; model?: string | null; credentialId?: string | null; } export interface AttemptCompletionOptions { outcome: Outcome; failureKind?: FailureKind | null; endedAt?: AccountingClockValue; latencyMs?: number | null; commitMs?: number | null; tokens?: TokenFactsInput | null; /** * The RELAY abandoned this attempt while another was in flight — today only a hedge loser. * * ⚠ It must be STATED, never inferred. At this layer a hedge loser and a client disconnect are * both `outcome: "cancelled"` with `failureKind: "aborted"` from one call site, so the attempts * map cannot tell them apart. Inferring one from "cancelled inside a successful request" would * be the counting-based guess this project's own fact rules forbid. */ abandonedByRelay?: boolean; } export interface RequestCompletionOptions { outcome?: Outcome; failureKind?: FailureKind | null; attribution?: Attribution; endedAt?: AccountingClockValue; latencyMs?: number | null; commitMs?: number | null; winningAttemptId?: AccountingAttemptId | null; } export interface CommitOptions { at?: AccountingClockValue; commitMs?: number | null; } export interface AccountingAttempt { readonly requestId: AccountingRequestId; readonly attemptId: AccountingAttemptId; readonly role: AttemptRole; readonly startedAt: string; readonly attribution: Attribution; complete(options: AttemptCompletionOptions): AttemptCompletedEvent | undefined; markCommitted(options?: CommitOptions): boolean; } export interface AccountingRequest { readonly requestId: AccountingRequestId; readonly startedAt: string; startAttempt(options?: AttemptStartOptions): AccountingAttempt; complete(options?: RequestCompletionOptions): RequestCompletedEvent | undefined; markCommitted(attemptId: AccountingAttemptId, options?: CommitOptions): boolean; readonly completed: boolean; } interface SpendComputationInput { readonly provider: string | null; readonly model: string | null; readonly tokens: AccountingTokenTotals; readonly endedAt: string; } /** * Compute one attempt's spend from the token facts ALREADY normalized onto it. * * Pricing rules (the provenance invariant, applied to money): * - Prices come only from the injected port's PUBLISHED figures — never a fallback * price, never a tunable default, never a cache multiplier. Cache read/write * discounts are unpublished, so cache tokens are counted as unpriced beside the * amount rather than priced at the base rate. * - Reported and estimated counts are priced into separate cells upstream (this * function picks whichever basis has evidence, reported first) and are never summed. * - anthropic-messages: input_tokens × in + output_tokens × out; cache_read / * cache_creation are NOT part of input_tokens and ride unpriced. * - openai-chat: (prompt_tokens − cached_tokens) × in when a cache figure is reported, * because OpenAI INCLUDES cached tokens in prompt_tokens and bills them at an * unpublished discount; if cached > prompt that figure is malformed, so prompt_tokens * is priced in full and no unpriced cached count is recorded. * - Estimated basis prices input ONLY by current spend policy; estimated output * remains a separate metering fact. Coverage says "input_only". * - No price for either kind ⇒ null (unpriced). Unknown stays null, never 0. */ export declare function computeAccountingSpend(port: AccountingPricePort | undefined, input: SpendComputationInput): AccountingSpend | null; export declare function createAccountingRequest(options?: AccountingRequestOptions): AccountingRequest;