@bymax-one/nest-ai-tokens 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3202 @@
1
+ import { ExecutionContext, ModuleMetadata, Type, DynamicModule, OnModuleInit, CanActivate, NestInterceptor, CallHandler, CustomDecorator, HttpException, HttpStatus } from '@nestjs/common';
2
+ import { Readable } from 'node:stream';
3
+ import { Reflector } from '@nestjs/core';
4
+ import { Observable } from 'rxjs';
5
+
6
+ /**
7
+ * @fileoverview Catalog of the AI providers this library recognizes out of the
8
+ * box. Custom OpenAI-compatible providers register their own id at runtime via
9
+ * `providerPresets.openaiCompatible(id)`, so the public {@link ProviderId} type
10
+ * (see `types/catalogs.ts`) widens this closed list with `(string & {})`.
11
+ * @layer shared
12
+ */
13
+ /**
14
+ * The provider identifiers shipped with the library. Frozen tuple so the derived
15
+ * {@link KnownProviderId} union stays in sync with the runtime values.
16
+ */
17
+ declare const PROVIDER_IDS: readonly ["openai", "azure-openai", "anthropic", "gemini", "vertex", "mistral", "bedrock", "openrouter", "deepseek", "xai", "groq"];
18
+ /** A provider id known to the library at build time (excludes custom ids). */
19
+ type KnownProviderId = (typeof PROVIDER_IDS)[number];
20
+
21
+ /**
22
+ * @fileoverview Catalog of AI operation kinds a usage record can describe. The
23
+ * operation is part of the price-resolution key; `'responses'` bills identically
24
+ * to `'chat'` and shares its price rows (see spec §5.1).
25
+ * @layer shared
26
+ */
27
+ /** Every operation kind the library can meter and price. */
28
+ declare const AI_OPERATIONS: readonly ["chat", "responses", "embeddings", "image", "video", "audio", "rerank", "moderation"];
29
+ /** A logical AI operation kind. */
30
+ type AiOperation = (typeof AI_OPERATIONS)[number];
31
+
32
+ /**
33
+ * @fileoverview Catalog of response service tiers. Providers may silently
34
+ * downgrade a request (OpenAI returns `service_tier: "default"` when priority
35
+ * capacity is exhausted), so pricing keys off the tier reported in the RESPONSE,
36
+ * which is what the normalizers read (see spec §5.1).
37
+ * @layer shared
38
+ */
39
+ /**
40
+ * Service tier of the actual response. `'batch'` is the Batch API (~50% discount);
41
+ * `'flex'` is OpenAI flex processing (batch rates, synchronous); `'priority'` is
42
+ * paid premium; `'standard'` is the default.
43
+ */
44
+ declare const SERVICE_TIERS: readonly ["standard", "batch", "flex", "priority"];
45
+ /** A response service tier. */
46
+ type ServiceTier = (typeof SERVICE_TIERS)[number];
47
+
48
+ /**
49
+ * @fileoverview Catalog of the token categories that are rated independently.
50
+ * Cache reads/writes are first-class because a naive input/output-only model
51
+ * over-bills cached traffic by up to 10× (see spec §5.4).
52
+ * @layer shared
53
+ */
54
+ /** Every token category that carries its own per-million rate. */
55
+ declare const TOKEN_CATEGORIES: readonly ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h", "reasoning", "audioIn", "audioOut", "imageIn", "imageOut"];
56
+ /** A single rated token category. */
57
+ type TokenCategory = (typeof TOKEN_CATEGORIES)[number];
58
+
59
+ /**
60
+ * @fileoverview Catalog of wallet entry kinds. Balances are the signed sum of
61
+ * these append-only entries (see spec §9.1).
62
+ * @layer shared
63
+ */
64
+ /** Every kind of entry that can appear in a wallet's append-only ledger. */
65
+ declare const WALLET_ENTRY_TYPES: readonly ["grant", "debit", "refund", "adjustment", "expiry"];
66
+ /** A wallet entry kind. */
67
+ type WalletEntryType = (typeof WALLET_ENTRY_TYPES)[number];
68
+
69
+ /**
70
+ * @fileoverview The exhaustive catalog of error codes the library raises. Keys
71
+ * and values are identical strings so `keyof typeof AI_TOKENS_ERROR_CODES` is the
72
+ * canonical code union that the message/status maps (server layer) are typed
73
+ * against — the compiler then enforces exhaustive coverage (see spec §16).
74
+ * @layer shared
75
+ */
76
+ /** Every error code the library can raise, mapped to itself for value access. */
77
+ declare const AI_TOKENS_ERROR_CODES: {
78
+ readonly AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED";
79
+ readonly AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG";
80
+ readonly AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER";
81
+ readonly AI_TOKENS_USAGE_MALFORMED: "AI_TOKENS_USAGE_MALFORMED";
82
+ readonly AI_TOKENS_PRICE_NOT_FOUND: "AI_TOKENS_PRICE_NOT_FOUND";
83
+ readonly AI_TOKENS_FX_REQUIRED: "AI_TOKENS_FX_REQUIRED";
84
+ readonly AI_TOKENS_BUDGET_EXCEEDED: "AI_TOKENS_BUDGET_EXCEEDED";
85
+ readonly AI_TOKENS_QUOTA_EXCEEDED: "AI_TOKENS_QUOTA_EXCEEDED";
86
+ readonly AI_TOKENS_INSUFFICIENT_CREDITS: "AI_TOKENS_INSUFFICIENT_CREDITS";
87
+ readonly AI_TOKENS_HOLD_NOT_FOUND: "AI_TOKENS_HOLD_NOT_FOUND";
88
+ readonly AI_TOKENS_HOLD_EXPIRED: "AI_TOKENS_HOLD_EXPIRED";
89
+ readonly AI_TOKENS_HOLD_ALREADY_SETTLED: "AI_TOKENS_HOLD_ALREADY_SETTLED";
90
+ readonly AI_TOKENS_IDEMPOTENCY_CONFLICT: "AI_TOKENS_IDEMPOTENCY_CONFLICT";
91
+ readonly AI_TOKENS_STREAM_USAGE_MISSING: "AI_TOKENS_STREAM_USAGE_MISSING";
92
+ readonly AI_TOKENS_STORE_ERROR: "AI_TOKENS_STORE_ERROR";
93
+ };
94
+ /** A library error code. */
95
+ type AiTokensErrorCode = keyof typeof AI_TOKENS_ERROR_CODES;
96
+
97
+ /**
98
+ * @fileoverview The canonical usage shape every provider normalizer produces.
99
+ * Downstream pricing, ledger, and budget logic is generic over this type, so it
100
+ * never sees a provider-specific payload.
101
+ * @layer shared
102
+ */
103
+
104
+ /**
105
+ * Provider-agnostic, per-call token and cost usage. All token counts are plain
106
+ * `number` (well under 2^53 per record); money is `bigint` nano-USD.
107
+ */
108
+ interface NormalizedUsage {
109
+ provider: ProviderId;
110
+ /** Model id as reported by the RESPONSE (may be a dated snapshot — see spec §6.6). */
111
+ model: string;
112
+ operation: AiOperation;
113
+ /** Tier reported by the response; absent → `'standard'`. */
114
+ serviceTier?: ServiceTier;
115
+ /** Uncached, non-reasoning input tokens (after the last cache breakpoint). */
116
+ inputTokens: number;
117
+ /**
118
+ * Output/completion tokens EXCLUDING reasoning tokens (§5.5 invariant).
119
+ * Anthropic folds thinking into output with no sub-field, so its adapter leaves
120
+ * `reasoningTokens` at 0 and keeps everything here.
121
+ */
122
+ outputTokens: number;
123
+ /** Tokens served from a cache hit (billed at a fraction of input). */
124
+ cacheReadTokens: number;
125
+ /** Tokens written to a 5-minute cache (Anthropic 1.25× input; Bedrock cacheWrite). */
126
+ cacheWrite5mTokens: number;
127
+ /** Tokens written to a 1-hour cache (Anthropic 2× input). */
128
+ cacheWrite1hTokens: number;
129
+ /** Reasoning/thinking tokens reported separately (OpenAI, Gemini). */
130
+ reasoningTokens: number;
131
+ /** Audio input tokens (multimodal). */
132
+ audioInTokens: number;
133
+ /** Audio output tokens (multimodal). */
134
+ audioOutTokens: number;
135
+ /** Image input tokens (multimodal). */
136
+ imageInTokens: number;
137
+ /** Image output tokens (multimodal). */
138
+ imageOutTokens: number;
139
+ /**
140
+ * Server-side tool usage counts — NON-token line items rated via
141
+ * `PriceVersion.unitRates` (§6.2), e.g. `{ web_search_requests: 2 }`. Anthropic
142
+ * reports these in `usage.server_tool_use`; for OpenAI/Gemini the host passes
143
+ * counts via `MeteringContext.extraUnits`.
144
+ */
145
+ serverToolUse?: Record<string, number>;
146
+ /** Provider-reported cost in nano-USD when available (OpenRouter `usage.cost`). */
147
+ providerReportedCostNanoUsd?: bigint;
148
+ /** Unclassified fields preserved verbatim for audit. */
149
+ raw?: Record<string, unknown>;
150
+ }
151
+
152
+ /**
153
+ * @fileoverview Canonical catalog types that the rest of the library is generic
154
+ * over: provider/operation/tier unions, the metering scope, the rating mode, and
155
+ * the pure normalizer/preset contracts. Framework-free.
156
+ * @layer shared
157
+ */
158
+
159
+ /**
160
+ * A provider identifier. Widened with `(string & {})` so custom
161
+ * OpenAI-compatible providers can register their own id while known ids keep
162
+ * editor autocomplete.
163
+ */
164
+ type ProviderId = KnownProviderId | (string & {});
165
+ /**
166
+ * How a call is rated. `'rate-table'` computes cost from the price registry;
167
+ * `'provider-reported'` trusts `NormalizedUsage.providerReportedCostNanoUsd`.
168
+ */
169
+ type RatingMode = 'rate-table' | 'provider-reported';
170
+ /** The payer subject whose wallet/budget a call consumes. */
171
+ interface MeteringScope {
172
+ /** `'key'` scopes spend their owner's wallet — they cannot own money. */
173
+ type: 'tenant' | 'team' | 'user' | 'key';
174
+ id: string;
175
+ }
176
+ /** The discriminant of {@link MeteringScope}. */
177
+ type ScopeType = MeteringScope['type'];
178
+ /**
179
+ * A pure function mapping one provider's raw `usage` object into the canonical
180
+ * {@link NormalizedUsage} shape. Consumes plain objects — never a provider SDK.
181
+ */
182
+ type UsageNormalizer = (raw: unknown) => NormalizedUsage;
183
+ /**
184
+ * Pairs a normalizer with the right provider id and rating mode. Passed in
185
+ * `MeteringContext.preset` and consumed by `meter()`, the interceptor, and
186
+ * `record()`.
187
+ */
188
+ interface ProviderPreset {
189
+ provider: ProviderId;
190
+ normalizer: UsageNormalizer;
191
+ ratingMode: RatingMode;
192
+ }
193
+
194
+ /**
195
+ * @fileoverview The immutable ledger row for one AI call (or its hold), its
196
+ * lifecycle status, the ledger query filter, and the append insert type.
197
+ * @layer shared
198
+ */
199
+
200
+ /** Lifecycle status of a usage record (see spec §8.3). */
201
+ type UsageStatus = 'pending' | 'posted' | 'reversed' | 'released';
202
+ /**
203
+ * One append-only ledger entry for a single AI call (or its hold). All money is
204
+ * integer nano-USD; token counts are plain `number`.
205
+ */
206
+ interface UsageRecord {
207
+ id: string;
208
+ tenantId: string;
209
+ /** The PAYER — the subject whose wallet/budget is consumed. */
210
+ scope: MeteringScope;
211
+ /** Optional distinct beneficiary (reporting dimension only). */
212
+ beneficiary?: MeteringScope;
213
+ /** Optional actor who triggered the call (audit). */
214
+ requestedBy?: string;
215
+ provider: ProviderId;
216
+ /** Model reported by the response (pricing follows this). */
217
+ model: string;
218
+ /** Model the host requested, when supplied (drift audit; §6.6). */
219
+ requestedModel?: string;
220
+ operation: AiOperation;
221
+ serviceTier: ServiceTier;
222
+ /** Caller-supplied logical operation, e.g. `'workout.generate'`. */
223
+ feature: string;
224
+ /** Free-form cost-attribution labels (≤ 10), e.g. `['team:research']`. */
225
+ tags: string[];
226
+ inputTokens: number;
227
+ outputTokens: number;
228
+ cacheReadTokens: number;
229
+ cacheWrite5mTokens: number;
230
+ cacheWrite1hTokens: number;
231
+ reasoningTokens: number;
232
+ audioInTokens: number;
233
+ audioOutTokens: number;
234
+ imageInTokens: number;
235
+ imageOutTokens: number;
236
+ totalTokens: number;
237
+ /** Non-token line items actually rated, e.g. `{ web_search_requests: 2 }`. */
238
+ extraUnits?: Record<string, number>;
239
+ /** `null` for provider-reported mode or a priceMissing record. */
240
+ priceVersionId: string | null;
241
+ /** Provider cost (tokens + surcharges). */
242
+ rawCostNanoUsd: bigint;
243
+ /** The non-token share of `rawCostNanoUsd`. */
244
+ surchargeNanoUsd: bigint;
245
+ /** After markup — what the customer pays. */
246
+ billedCostNanoUsd: bigint;
247
+ /** The resolved 4-dp markup value actually applied. */
248
+ markupMultiplier: number;
249
+ /** `'USD'` in v0.1 (§7.4). */
250
+ currency: string;
251
+ /** Rated at 0 in non-strict mode; a backfill target. */
252
+ priceMissing: boolean;
253
+ status: UsageStatus;
254
+ /** Set on a posted record when a compensating record reversed it (§8.5). */
255
+ reversedByRecordId?: string;
256
+ /** Set on a compensating record, pointing back at the record it negates. */
257
+ reversesRecordId?: string;
258
+ /** Unique per `(tenantId, idempotencyKey)`. */
259
+ idempotencyKey: string;
260
+ /** Ties an invoice line back to app logs. */
261
+ correlationId?: string;
262
+ /** Provider request id. */
263
+ requestId?: string;
264
+ /** Platform-absorbed — never consumes wallet/budget. */
265
+ isSystemCost: boolean;
266
+ /** e.g. `'workout_generation_retry'`. */
267
+ systemCostCategory?: string;
268
+ /** True when this record consumed wallet/budget (reconciliation predicate §10.7). */
269
+ enforced: boolean;
270
+ /** Previous chain hash (optional per-tenant hash chain, §8.6). */
271
+ prevHash?: string;
272
+ /** This record's chain hash (optional per-tenant hash chain, §8.6). */
273
+ hash?: string;
274
+ /** When the LLM call happened. */
275
+ occurredAt: Date;
276
+ createdAt: Date;
277
+ /** Pending-row settlements only. */
278
+ updatedAt: Date;
279
+ }
280
+ /**
281
+ * Caller-supplied fields for `ILedgerStore.append`. The store computes
282
+ * `id`/`hash`/`prevHash`/`createdAt`/`updatedAt`.
283
+ */
284
+ type NewUsageRecord = Omit<UsageRecord, 'id' | 'prevHash' | 'hash' | 'createdAt' | 'updatedAt'>;
285
+ /** Query filter for the ledger store. */
286
+ interface LedgerFilter {
287
+ tenantId: string;
288
+ scope?: MeteringScope;
289
+ beneficiary?: MeteringScope;
290
+ feature?: string;
291
+ features?: string[];
292
+ provider?: ProviderId;
293
+ model?: string;
294
+ operation?: AiOperation;
295
+ serviceTier?: ServiceTier;
296
+ tags?: string[];
297
+ isSystemCost?: boolean;
298
+ systemCostCategory?: string;
299
+ status?: UsageStatus[];
300
+ enforcedOnly?: boolean;
301
+ from?: Date;
302
+ to?: Date;
303
+ limit?: number;
304
+ offset?: number;
305
+ }
306
+
307
+ /**
308
+ * @fileoverview Append-only ledger field whitelists and the transition guard — the
309
+ * single source of truth shared by the ledger service and the store adapters so the
310
+ * two layers can never drift. `AMOUNT_FIELDS` are the money/token amounts that stay
311
+ * immutable after a record posts; `SETTLEMENT_FIELDS` are everything a settlement
312
+ * (`pending → posted`) may replace with actuals; `RELEASE_FIELDS` are the audit
313
+ * annotations a void (`pending → released`) may set; `REVERSAL_LINKAGE_FIELD` is the
314
+ * sole annotation a `posted → reversed` flip may set. `isLegalLedgerTransition` and
315
+ * `isLegalTransitionPatchKey` encode the state machine and its per-transition patch
316
+ * contract so every layer accepts exactly the same patches (see spec §8.3/§8.5).
317
+ * @layer shared
318
+ */
319
+
320
+ /**
321
+ * The money/token amount columns that are append-only once a record posts — a
322
+ * void (`pending → released`) or a reversal annotation may never patch them.
323
+ */
324
+ declare const AMOUNT_FIELDS: readonly ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWrite5mTokens", "cacheWrite1hTokens", "reasoningTokens", "audioInTokens", "audioOutTokens", "imageInTokens", "imageOutTokens", "totalTokens", "rawCostNanoUsd", "surchargeNanoUsd", "billedCostNanoUsd"];
325
+ /**
326
+ * Every field a settlement (`pending → posted`) may replace with actuals: the
327
+ * append-only amounts plus the pricing metadata resolved at settlement time.
328
+ */
329
+ declare const SETTLEMENT_FIELDS: readonly ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWrite5mTokens", "cacheWrite1hTokens", "reasoningTokens", "audioInTokens", "audioOutTokens", "imageInTokens", "imageOutTokens", "totalTokens", "rawCostNanoUsd", "surchargeNanoUsd", "billedCostNanoUsd", "priceVersionId", "priceMissing", "markupMultiplier"];
330
+ /**
331
+ * The audit-annotation columns a void (`pending → released`) may patch. A release
332
+ * never bills, so it may only annotate the voided hold with request/correlation
333
+ * linkage — never an append-only amount, the settlement pricing metadata, or an
334
+ * immutable identity column. Kept as an explicit column whitelist so the store
335
+ * boundary never interpolates an unknown identifier into raw SQL.
336
+ */
337
+ declare const RELEASE_FIELDS: readonly ["correlationId", "requestId"];
338
+ /** The sole field a `posted → reversed` annotation may set — the reversal linkage. */
339
+ declare const REVERSAL_LINKAGE_FIELD: "reversedByRecordId";
340
+ /**
341
+ * The only patch keys a settlement (`pending → posted`) may set — the settlement
342
+ * amount/cost/pricing columns plus the reversal-linkage field. An immutable
343
+ * identity column (`id`/`tenantId`/`idempotencyKey`) is never a member.
344
+ */
345
+ declare const SETTLEMENT_PATCH_KEYS: ReadonlySet<string>;
346
+ /**
347
+ * Whether `from → to` is a legal ledger state transition (§8.3): a hold settles
348
+ * (`pending → posted`) or is voided (`pending → released`), and a posted record may
349
+ * be annotated as reversed (`posted → reversed`). Every other pair is a caller bug.
350
+ *
351
+ * @param from The record's current lifecycle status.
352
+ * @param to The requested target status.
353
+ * @returns `true` when the transition is permitted by the state machine.
354
+ */
355
+ declare function isLegalLedgerTransition(from: UsageStatus, to: UsageStatus): boolean;
356
+ /**
357
+ * Whether `key` is a legal patch field for the `from → to` transition (§8.3/§8.5) —
358
+ * the single source of truth shared by the ledger service guard and every store
359
+ * adapter so the two layers can never drift:
360
+ * - `pending → posted` (settle): only the settlement fields (+ the reversal linkage).
361
+ * - `pending → released` (void): only the audit-annotation fields.
362
+ * - `posted → reversed` (annotate): only the reversal linkage.
363
+ * Any other (illegal) transition patches nothing. The result set is always drawn
364
+ * from the fixed column whitelists above, so a store may interpolate a passing key
365
+ * as a raw SQL identifier without risk of injection.
366
+ *
367
+ * @param from The record's current lifecycle status.
368
+ * @param to The requested target status.
369
+ * @param key The patch column a caller wishes to set.
370
+ * @returns `true` when the column may legally be patched on this transition.
371
+ */
372
+ declare function isLegalTransitionPatchKey(from: UsageStatus, to: UsageStatus, key: string): boolean;
373
+
374
+ /**
375
+ * @fileoverview Normalizer for the OpenAI Chat Completions `usage` shape. The
376
+ * critical rule (spec §5.3/§5.5): `completion_tokens` INCLUDES reasoning tokens,
377
+ * so `outputTokens = completion_tokens − reasoning_tokens` — double-billing that
378
+ * detail is the audit's #1 billing bug. Cached and audio prompt tokens are a
379
+ * subset of `prompt_tokens` and are split into their own categories.
380
+ * @layer shared
381
+ */
382
+
383
+ /**
384
+ * Normalize an OpenAI Chat Completions response into {@link NormalizedUsage}.
385
+ *
386
+ * @param raw The provider response object (with a nested `usage`).
387
+ * @returns The canonical usage.
388
+ * @throws {Error} When the `usage` object or its required token fields are absent.
389
+ * @example
390
+ * normalizeOpenAiChatUsage({ model: 'gpt-5.2', usage: { prompt_tokens: 100, completion_tokens: 40 } })
391
+ */
392
+ declare function normalizeOpenAiChatUsage(raw: unknown): NormalizedUsage;
393
+
394
+ /**
395
+ * @fileoverview Normalizer for the OpenAI Responses API `usage` shape. Same
396
+ * reasoning-subtraction rule as Chat Completions, but the fields are renamed
397
+ * `input_tokens` / `output_tokens` with `input_tokens_details` /
398
+ * `output_tokens_details` (spec §5.3).
399
+ * @layer shared
400
+ */
401
+
402
+ /**
403
+ * Normalize an OpenAI Responses API response into {@link NormalizedUsage}.
404
+ *
405
+ * @param raw The provider response object (with a nested `usage`).
406
+ * @returns The canonical usage.
407
+ * @throws {Error} When the `usage` object or its required token fields are absent.
408
+ * @example
409
+ * normalizeOpenAiResponsesUsage({ model: 'gpt-5.2', usage: { input_tokens: 100, output_tokens: 40 } })
410
+ */
411
+ declare function normalizeOpenAiResponsesUsage(raw: unknown): NormalizedUsage;
412
+
413
+ /**
414
+ * @fileoverview Normalizer for OpenAI-compatible gateways (DeepSeek, xAI, Groq, …)
415
+ * that reuse the Chat Completions `prompt_tokens` / `completion_tokens` shape
416
+ * without OpenAI-specific detail guarantees. DeepSeek's `prompt_cache_hit_tokens`
417
+ * maps to `cacheReadTokens` when present. The provider id is left empty for the
418
+ * preset (`providerPresets.openaiCompatible(id)`) or context to supply (spec §5.3).
419
+ * @layer shared
420
+ */
421
+
422
+ /**
423
+ * Normalize an OpenAI-compatible chat response into {@link NormalizedUsage}.
424
+ *
425
+ * @param raw The provider response object (with a nested `usage`).
426
+ * @returns The canonical usage; `provider` is `''` unless the preset overrides it.
427
+ * @throws {Error} When the `usage` object or its required token fields are absent.
428
+ * @example
429
+ * normalizeOpenAiCompatibleUsage({ model: 'deepseek-chat', usage: { prompt_tokens: 100, completion_tokens: 40 } })
430
+ */
431
+ declare function normalizeOpenAiCompatibleUsage(raw: unknown): NormalizedUsage;
432
+
433
+ /**
434
+ * @fileoverview Normalizer for the Anthropic Messages `usage` shape. Anthropic
435
+ * folds thinking into `output_tokens` with no sub-field, so `reasoningTokens`
436
+ * stays `0` (spec §5.3/§5.5). Cache writes split into 5-minute and 1-hour
437
+ * categories via `cache_creation`; an unsplit `cache_creation_input_tokens`
438
+ * total defaults to the 5-minute category. Server tool use maps to `serverToolUse`.
439
+ * @layer shared
440
+ */
441
+
442
+ /**
443
+ * Normalize an Anthropic Messages response into {@link NormalizedUsage}.
444
+ *
445
+ * @param raw The provider response object (with a nested `usage`).
446
+ * @returns The canonical usage.
447
+ * @throws {Error} When the `usage` object or its required token fields are absent.
448
+ * @example
449
+ * normalizeAnthropicUsage({ model: 'claude-opus', usage: { input_tokens: 100, output_tokens: 40 } })
450
+ */
451
+ declare function normalizeAnthropicUsage(raw: unknown): NormalizedUsage;
452
+
453
+ /**
454
+ * @fileoverview Normalizer for the Google Gemini / Vertex `usageMetadata` shape.
455
+ * `candidatesTokenCount` EXCLUDES thoughts, so it maps directly to `outputTokens`
456
+ * with no subtraction; `thoughtsTokenCount` maps to `reasoningTokens`;
457
+ * `toolUsePromptTokenCount` folds into `inputTokens` (billed at the input rate);
458
+ * `cachedContentTokenCount` is the cache-read portion of the prompt (spec §5.3).
459
+ * @layer shared
460
+ */
461
+
462
+ /**
463
+ * Normalize a Gemini / Vertex response into {@link NormalizedUsage}.
464
+ *
465
+ * @param raw The provider response object (with a nested `usageMetadata`).
466
+ * @returns The canonical usage.
467
+ * @throws {Error} When `usageMetadata` or its required token field is absent.
468
+ * @example
469
+ * normalizeGeminiUsage({ modelVersion: 'gemini-2.5-flash', usageMetadata: { promptTokenCount: 100, candidatesTokenCount: 40 } })
470
+ */
471
+ declare function normalizeGeminiUsage(raw: unknown): NormalizedUsage;
472
+
473
+ /**
474
+ * @fileoverview Normalizer for the AWS Bedrock Converse `usage` shape. Cache
475
+ * writes come either as a single `cacheWriteInputTokens` total (defaulting to the
476
+ * 5-minute category) or split across `cacheDetails[]` TTL entries (spec §5.3).
477
+ * Model ids like `us.anthropic.claude-…-v1:0` are region-prefixed and are
478
+ * resolved downstream (spec §6.6).
479
+ * @layer shared
480
+ */
481
+
482
+ /**
483
+ * Normalize a Bedrock Converse response into {@link NormalizedUsage}.
484
+ *
485
+ * @param raw The provider response object (with a nested `usage`).
486
+ * @returns The canonical usage.
487
+ * @throws {Error} When the `usage` object or its required token fields are absent.
488
+ * @example
489
+ * normalizeBedrockConverseUsage({ model: 'us.anthropic.claude', usage: { inputTokens: 100, outputTokens: 40 } })
490
+ */
491
+ declare function normalizeBedrockConverseUsage(raw: unknown): NormalizedUsage;
492
+
493
+ /**
494
+ * @fileoverview Normalizer for the Mistral `usage` shape. v0.1 maps
495
+ * `prompt_tokens` / `completion_tokens` only; any additional audio/cache detail
496
+ * fields are version-dependent and preserved verbatim in `raw` (spec §5.3/§20.3).
497
+ * @layer shared
498
+ */
499
+
500
+ /**
501
+ * Normalize a Mistral response into {@link NormalizedUsage}.
502
+ *
503
+ * @param raw The provider response object (with a nested `usage`).
504
+ * @returns The canonical usage.
505
+ * @throws {Error} When the `usage` object or its required token fields are absent.
506
+ * @example
507
+ * normalizeMistralUsage({ model: 'mistral-large', usage: { prompt_tokens: 100, completion_tokens: 40 } })
508
+ */
509
+ declare function normalizeMistralUsage(raw: unknown): NormalizedUsage;
510
+
511
+ /**
512
+ * @fileoverview Normalizer for the OpenRouter `usage` shape. `usage.cost` is the
513
+ * real charged amount in USD (1 credit = 1 USD) and converts to
514
+ * `providerReportedCostNanoUsd`, enabling `'provider-reported'` rating with no
515
+ * price row. `completion_tokens` includes reasoning, so it is subtracted like
516
+ * OpenAI (spec §5.3/§5.5/§6.5). `cost_details` is preserved in `raw`.
517
+ * @layer shared
518
+ */
519
+
520
+ /**
521
+ * Normalize an OpenRouter response into {@link NormalizedUsage}.
522
+ *
523
+ * @param raw The provider response object (with a nested `usage`).
524
+ * @returns The canonical usage, including `providerReportedCostNanoUsd` when `usage.cost` is present.
525
+ * @throws {Error} When the `usage` object or its required token fields are absent.
526
+ * @example
527
+ * normalizeOpenRouterUsage({ model: 'gpt-5.2', usage: { prompt_tokens: 100, completion_tokens: 40, cost: 0.0123 } })
528
+ */
529
+ declare function normalizeOpenRouterUsage(raw: unknown): NormalizedUsage;
530
+
531
+ /**
532
+ * @fileoverview Normalizer for the Vercel AI SDK `usage` shape. It reads BOTH the
533
+ * v5 shape (`usage.{inputTokens, outputTokens, cachedInputTokens, reasoningTokens}`)
534
+ * and the v6 shape (`inputTokenDetails.{cacheReadTokens, cacheWriteTokens}`,
535
+ * `outputTokenDetails.reasoningTokens`). `outputTokens` includes reasoning tokens,
536
+ * so they are subtracted. The provider id and model are supplied by the caller's
537
+ * preset/context and default to empty here (spec §5.3).
538
+ * @layer shared
539
+ */
540
+
541
+ /**
542
+ * Normalize a Vercel AI SDK response into {@link NormalizedUsage}.
543
+ *
544
+ * @param raw The provider response object (with a nested `usage`).
545
+ * @returns The canonical usage; `provider` is `''` unless the context overrides it.
546
+ * @throws {Error} When the `usage` object or its required token fields are absent.
547
+ * @example
548
+ * normalizeVercelAiSdkUsage({ usage: { inputTokens: 100, outputTokens: 40 } })
549
+ */
550
+ declare function normalizeVercelAiSdkUsage(raw: unknown): NormalizedUsage;
551
+
552
+ /**
553
+ * @fileoverview Exact nano-USD money helpers. Every persisted monetary value is
554
+ * an integer number of nano-USD (`1e-9` USD) held as a `bigint`, so all
555
+ * arithmetic is exact integer math. Floating point appears only at the two
556
+ * documented boundaries: the OpenRouter `usage.cost` entry conversion
557
+ * ({@link floatUsdToNanoUsd}) and presentation ({@link formatNanoUsd}).
558
+ * @layer shared
559
+ */
560
+ /**
561
+ * The building block for token pricing: cost of `tokens` at a rate expressed as
562
+ * nano-USD per 1,000,000 tokens, as exact integer math (spec §7.1).
563
+ *
564
+ * @param tokens Integer token count.
565
+ * @param ratePerMillionNano Rate in nano-USD per 1,000,000 tokens.
566
+ * @returns Cost in nano-USD, truncated toward zero on the division.
567
+ * @example
568
+ * perMillion(1000, 5_000_000_000n) // 5_000_000n ($0.005 for 1k tokens at $5/M)
569
+ */
570
+ declare function perMillion(tokens: number, ratePerMillionNano: bigint): bigint;
571
+ /**
572
+ * Convert a floating-point USD amount (e.g. OpenRouter `usage.cost`) into
573
+ * nano-USD, rounding half away from zero at nano precision. The dollar and
574
+ * fractional parts are handled separately so no product overflows the 2^53
575
+ * exact-integer range: the result is exact for `|usd| < $1,000` (and for any
576
+ * value whose fractional dollars round cleanly). This is the only float→bigint
577
+ * entry point on a money path.
578
+ *
579
+ * @param usd The amount in USD.
580
+ * @returns The amount in nano-USD.
581
+ * @throws {RangeError} When `usd` is not finite.
582
+ * @example
583
+ * floatUsdToNanoUsd(0.005) // 5_000_000n
584
+ */
585
+ declare function floatUsdToNanoUsd(usd: number): bigint;
586
+ /** Options controlling how {@link formatNanoUsd} renders a value. */
587
+ interface FormatNanoUsdOptions {
588
+ /** ISO currency code; `'USD'` renders a `$` prefix, others append the code. Default `'USD'`. */
589
+ currency?: string;
590
+ /** Presentation FX rate as target-currency nano-units per 1 USD; converts at read time. */
591
+ fxRateNano?: bigint;
592
+ /** Fractional digits to render, in the range `[0, 9]`. Default `6`. */
593
+ decimals?: number;
594
+ }
595
+ /**
596
+ * Render a nano-USD value as a human-readable currency string. Presentation
597
+ * only — rounding to the displayed precision is round-half-up computed in
598
+ * `bigint`, never floating point (spec §7.4).
599
+ *
600
+ * @param nanoUsd The value in nano-USD.
601
+ * @param opts Currency, FX, and precision options.
602
+ * @returns The formatted string, e.g. `"$0.005000"`.
603
+ * @throws {RangeError} When `opts.decimals` is not an integer in `[0, 9]`.
604
+ * @example
605
+ * formatNanoUsd(5_000_000n) // '$0.005000'
606
+ * formatNanoUsd(5_000_000n, { currency: 'BRL', fxRateNano: 5_000_000_000n }) // '0.025000 BRL'
607
+ */
608
+ declare function formatNanoUsd(nanoUsd: bigint, opts?: FormatNanoUsdOptions): string;
609
+
610
+ /**
611
+ * @fileoverview The effective-dated price row and its insert type. Rates are
612
+ * integer nano-USD per 1,000,000 tokens (or per unit for non-token line items),
613
+ * so every cost computation is exact integer math.
614
+ * @layer shared
615
+ */
616
+
617
+ /**
618
+ * One immutable, effective-dated price version for a (provider, model, operation,
619
+ * serviceTier) tuple. A call is rated at the row in effect at its timestamp.
620
+ */
621
+ interface PriceVersion {
622
+ id: string;
623
+ provider: ProviderId;
624
+ model: string;
625
+ operation: AiOperation;
626
+ /** Part of the resolution key; `'standard'` is the default. */
627
+ serviceTier: ServiceTier;
628
+ /** Rates as integer nano-USD per 1,000,000 tokens. */
629
+ inputNanoUsdPerMillion: bigint;
630
+ outputNanoUsdPerMillion: bigint;
631
+ cacheReadNanoUsdPerMillion: bigint;
632
+ cacheWrite5mNanoUsdPerMillion: bigint;
633
+ cacheWrite1hNanoUsdPerMillion: bigint;
634
+ /** Usually equals output; kept separate for models that price reasoning differently. */
635
+ reasoningNanoUsdPerMillion: bigint;
636
+ audioInNanoUsdPerMillion: bigint;
637
+ audioOutNanoUsdPerMillion: bigint;
638
+ imageInNanoUsdPerMillion: bigint;
639
+ imageOutNanoUsdPerMillion: bigint;
640
+ /**
641
+ * Long-context tier threshold: when total input (all input-side categories)
642
+ * exceeds this, the tier rates replace the base input/output rates for the
643
+ * WHOLE call (all-or-nothing, matching Gemini's billing).
644
+ */
645
+ tierThresholdTokens?: number;
646
+ tierInputNanoUsdPerMillion?: bigint;
647
+ tierOutputNanoUsdPerMillion?: bigint;
648
+ /**
649
+ * Non-token line items in nano-USD PER UNIT, matched against
650
+ * `NormalizedUsage.serverToolUse` / `MeteringContext.extraUnits` by EXACT key —
651
+ * so the key must equal the count key the normalizer emits (e.g. Anthropic's
652
+ * `usage.server_tool_use.web_search_requests`) — e.g.
653
+ * `{ web_search_requests: 10_000_000n }` ($0.01 per web-search call).
654
+ */
655
+ unitRates?: Record<string, bigint>;
656
+ currency: 'USD';
657
+ effectiveFrom: Date;
658
+ /** `null` = current; set when a newer row supersedes it. */
659
+ effectiveTo: Date | null;
660
+ /** Provenance: `'snapshot' | 'manual' | 'import'`. */
661
+ source: string;
662
+ }
663
+ /**
664
+ * Caller-supplied fields for `upsertPrice`. Rates default to `0n` and
665
+ * `serviceTier` to `'standard'`; the store assigns `id` and closes the previous
666
+ * open row's `effectiveTo`.
667
+ */
668
+ type NewPriceVersion = Partial<Omit<PriceVersion, 'id' | 'effectiveTo'>> & Pick<PriceVersion, 'provider' | 'model' | 'operation'>;
669
+
670
+ /**
671
+ * @fileoverview The pure cost engine (spec §7.1). Computes the provider cost of a
672
+ * {@link NormalizedUsage} against a {@link PriceVersion} in exact bigint nano-USD.
673
+ * The long-context tier is all-or-nothing: when total input crosses the
674
+ * threshold, the tier rates replace the base input/output rates for the WHOLE
675
+ * call. Non-token line items are rated via `unitRates`. The token and surcharge
676
+ * parts are returned separately so reports can distinguish them.
677
+ * @layer shared
678
+ */
679
+
680
+ /** The separable parts of a computed provider cost, all in nano-USD. */
681
+ interface CostBreakdown {
682
+ /** Token cost plus surcharges. */
683
+ totalNanoUsd: bigint;
684
+ /** The token-only share. */
685
+ tokenNanoUsd: bigint;
686
+ /** The non-token (server tool use) share. */
687
+ surchargeNanoUsd: bigint;
688
+ }
689
+ /**
690
+ * Compute the provider cost of a normalized usage against a price version.
691
+ *
692
+ * @param usage The normalized usage.
693
+ * @param rate The effective-dated price version.
694
+ * @returns The exact cost broken into token and surcharge parts.
695
+ * @example
696
+ * // 1,000 Opus input tokens at $5/M → 5_000_000n nano-USD ($0.005).
697
+ * computeCostNanoUsd(usage, rate).totalNanoUsd
698
+ */
699
+ declare function computeCostNanoUsd(usage: NormalizedUsage, rate: PriceVersion): CostBreakdown;
700
+
701
+ /**
702
+ * @fileoverview Markup / margin math (spec §7.2). The multiplier is validated
703
+ * (finite, > 0) and rounded to 4 decimal places to match the persisted
704
+ * `Decimal(10,4)`; the billed amount is exact bigint with truncation toward zero
705
+ * on the final division. Markup applies in BOTH rating modes.
706
+ * @layer shared
707
+ */
708
+ /**
709
+ * Validate and round a markup multiplier to 4 decimal places — the value that is
710
+ * persisted on each usage record.
711
+ *
712
+ * @param multiplier The raw multiplier.
713
+ * @returns The multiplier rounded to 4 decimal places.
714
+ * @throws {RangeError} When the multiplier is not finite or not > 0.
715
+ * @example
716
+ * resolveMultiplier4dp(1.23456) // 1.2346
717
+ */
718
+ declare function resolveMultiplier4dp(multiplier: number): number;
719
+ /**
720
+ * Apply a markup multiplier to a raw provider cost, in exact bigint nano-USD.
721
+ *
722
+ * @param rawCostNanoUsd The provider cost in nano-USD.
723
+ * @param multiplier The markup multiplier (e.g. `4.0` for 4× resale).
724
+ * @returns The billed cost in nano-USD (division truncates toward zero).
725
+ * @throws {RangeError} When the multiplier is not finite or not > 0.
726
+ * @example
727
+ * applyMarkup(5_000_000n, 4.0) // 20_000_000n
728
+ */
729
+ declare function applyMarkup(rawCostNanoUsd: bigint, multiplier: number): bigint;
730
+
731
+ /**
732
+ * @fileoverview Host-side derivation of a stable idempotency key from request
733
+ * content (spec §8.4). The payload is serialized to canonical JSON — object keys
734
+ * sorted recursively, array order preserved, `undefined` dropped, `bigint`
735
+ * encoded as a QUOTED, type-tagged string (e.g. `42n` → `"42n"`), `Date` as its
736
+ * ISO string — then hashed with a pure synchronous SHA-256. The `n`-suffixed
737
+ * quoted form makes a `bigint` unambiguous: it can never serialize to the same
738
+ * text as the number `42` (`42`) or a numeric string `"42"` (`"42"`), so
739
+ * `deriveIdempotencyKey(42)` and `deriveIdempotencyKey(42n)` never collide.
740
+ * Deriving the key from content means a retry after a 429/network failure reuses
741
+ * the same key and is deduplicated.
742
+ * @layer shared
743
+ */
744
+ /**
745
+ * Derive a stable idempotency key from a request payload. Equal payloads (up to
746
+ * object-key order) yield the same key; distinct payloads yield distinct keys.
747
+ * Pure, synchronous, and edge-safe.
748
+ *
749
+ * @param payload Any JSON-like request content.
750
+ * @returns A 64-character lowercase hex SHA-256 digest.
751
+ * @example
752
+ * deriveIdempotencyKey({ a: 1, b: 2 }) === deriveIdempotencyKey({ b: 2, a: 1 }) // true
753
+ */
754
+ declare function deriveIdempotencyKey(payload: unknown): string;
755
+
756
+ /**
757
+ * @fileoverview Wallet types: the owner reference, the materialized wallet, and
758
+ * the append-only entry ledger whose signed sum is the balance (see spec §9.1).
759
+ * @layer shared
760
+ */
761
+
762
+ /**
763
+ * Identifies the owner of a wallet. `'key'` scopes deliberately cannot own money
764
+ * — API keys spend their owner's wallet.
765
+ */
766
+ interface WalletRef {
767
+ tenantId: string;
768
+ ownerType: 'tenant' | 'team' | 'user';
769
+ ownerId: string;
770
+ }
771
+ /** A prepaid credit wallet with a materialized nano-USD balance. */
772
+ interface Wallet {
773
+ id: string;
774
+ tenantId: string;
775
+ ownerType: WalletRef['ownerType'];
776
+ ownerId: string;
777
+ /** Materialized, kept transactionally consistent with Σ entries. */
778
+ balanceNanoUsd: bigint;
779
+ createdAt: Date;
780
+ updatedAt: Date;
781
+ }
782
+ /** One append-only entry in a wallet's ledger. */
783
+ interface WalletEntry {
784
+ id: string;
785
+ walletId: string;
786
+ type: WalletEntryType;
787
+ /** `+` = credit, `−` = debit. */
788
+ amountNanoUsd: bigint;
789
+ /** Grant burn priority (lower first). */
790
+ priority: number;
791
+ effectiveAt: Date;
792
+ /** Grants only. */
793
+ expiresAt?: Date;
794
+ /** Present on usage-driven debits/refunds. */
795
+ usageRecordId?: string;
796
+ /** Unique per wallet. */
797
+ idempotencyKey: string;
798
+ reason?: string;
799
+ createdAt: Date;
800
+ }
801
+ /** Caller-supplied fields for `IWalletStore.appendEntry`. The store assigns `id`/`createdAt`. */
802
+ type NewWalletEntry = Omit<WalletEntry, 'id' | 'createdAt'>;
803
+
804
+ /**
805
+ * @fileoverview Budget types: the budget definition, its window/policy unions,
806
+ * and the read-side status DTOs a usage meter renders (see spec §10).
807
+ * @layer shared
808
+ */
809
+
810
+ /** How a budget's window repeats. `{ customSeconds }` is a fixed-length rolling window. */
811
+ type BudgetWindowKind = 'day' | 'week' | 'month' | 'total' | {
812
+ customSeconds: number;
813
+ };
814
+ /** What happens when a budget is exceeded. */
815
+ type BudgetPolicy = 'block' | 'throttle' | 'allow';
816
+ /** A spend/token/count cap for a scope over a window. */
817
+ interface Budget {
818
+ id: string;
819
+ tenantId: string;
820
+ scope: MeteringScope;
821
+ /** Restrict which usage counts; empty/absent = all features. */
822
+ features?: string[];
823
+ /** Billed-spend cap. */
824
+ limitNanoUsd?: bigint;
825
+ /** Total-token cap. */
826
+ limitTokens?: number;
827
+ /** Operation-count cap (posted, enforced, non-reversed records). */
828
+ limitCount?: number;
829
+ window: BudgetWindowKind;
830
+ /**
831
+ * Per-budget window anchor. Absent → calendar UTC anchoring. Month windows
832
+ * clamp short months (a Jan 31 anchor yields Feb 28/29, Mar 31, …).
833
+ */
834
+ anchorAt?: Date;
835
+ /** Optional budget lifetime: enforcement ignores the budget after this instant. */
836
+ expiresAt?: Date;
837
+ /** Fractions of the limit that trigger soft alert events. */
838
+ softThresholds: number[];
839
+ policy: BudgetPolicy;
840
+ createdAt: Date;
841
+ }
842
+ /** The read-side status of one budget window — powers a usage meter. */
843
+ interface BudgetStatus {
844
+ budgetId: string;
845
+ features?: string[];
846
+ window: BudgetWindowKind;
847
+ windowStart: Date;
848
+ /** `null` for `'total'`. */
849
+ resetsAt: Date | null;
850
+ policy: BudgetPolicy;
851
+ limit: {
852
+ nanoUsd?: bigint;
853
+ tokens?: number;
854
+ count?: number;
855
+ };
856
+ spent: {
857
+ nanoUsd: bigint;
858
+ tokens: number;
859
+ count: number;
860
+ };
861
+ /** Absent dimension = unlimited. */
862
+ remaining: {
863
+ nanoUsd?: bigint;
864
+ tokens?: number;
865
+ count?: number;
866
+ };
867
+ /** Max across limited dimensions. */
868
+ usedFraction: number;
869
+ }
870
+ /** Aggregate access status for a scope (wallet + all matching budgets). */
871
+ interface AccessStatus {
872
+ hasAccess: boolean;
873
+ blockedBy?: 'wallet' | 'budget';
874
+ wallet?: {
875
+ balanceNanoUsd: bigint;
876
+ credits: number;
877
+ overdraftRemainingNanoUsd: bigint;
878
+ };
879
+ budgets: BudgetStatus[];
880
+ }
881
+
882
+ /**
883
+ * @fileoverview Reporting types: the query filter and the aggregated usage
884
+ * summary (including the cache-savings figure) produced by `UsageReportService`.
885
+ * @layer shared
886
+ */
887
+
888
+ /** Filter for a usage report query. */
889
+ interface ReportFilter {
890
+ tenantId: string;
891
+ scope?: MeteringScope;
892
+ beneficiary?: MeteringScope;
893
+ feature?: string;
894
+ features?: string[];
895
+ provider?: ProviderId;
896
+ model?: string;
897
+ operation?: AiOperation;
898
+ serviceTier?: ServiceTier;
899
+ /** Records carrying ANY of these tags. */
900
+ tags?: string[];
901
+ isSystemCost?: boolean;
902
+ systemCostCategory?: string;
903
+ enforcedOnly?: boolean;
904
+ /** Default `['posted', 'reversed']`. */
905
+ status?: UsageStatus[];
906
+ from: Date;
907
+ to: Date;
908
+ }
909
+ /** One aggregated row of a usage report. */
910
+ interface UsageSummary {
911
+ /** groupBy key → value; empty groupBy = one grand-total row. */
912
+ group: Record<string, string>;
913
+ records: number;
914
+ totalTokens: number;
915
+ tokens: Record<TokenCategory, number>;
916
+ rawCostNanoUsd: bigint;
917
+ surchargeNanoUsd: bigint;
918
+ billedCostNanoUsd: bigint;
919
+ /** Σ cacheReadTokens × (inputRate − cacheReadRate) — the "you saved $X via caching" figure. */
920
+ cacheSavingsNanoUsd: bigint;
921
+ }
922
+
923
+ /**
924
+ * @fileoverview The event envelope, the event-type union, and one payload type
925
+ * per event in the catalog (see spec §12). Delivery is at-least-once, so `id` is
926
+ * the consumer's dedupe key. `bigint` payload fields cross a JSON boundary as
927
+ * decimal strings (§15.5) — in-process delivery keeps them as `bigint`.
928
+ * @layer shared
929
+ */
930
+
931
+ /** Every event `type` the library emits. */
932
+ type AiTokensEventType = 'ai_tokens.usage.recorded' | 'ai_tokens.usage.reversed' | 'ai_tokens.hold.released' | 'ai_tokens.budget.threshold_crossed' | 'ai_tokens.budget.exceeded' | 'ai_tokens.budget.projected_exceeded' | 'ai_tokens.wallet.low_balance' | 'ai_tokens.wallet.depleted' | 'ai_tokens.wallet.granted' | 'ai_tokens.price.missing' | 'ai_tokens.audit';
933
+ /** The at-least-once event envelope. */
934
+ interface AiTokensEvent<T = unknown> {
935
+ /** UUID — the consumer's dedupe key. */
936
+ id: string;
937
+ type: AiTokensEventType;
938
+ occurredAt: Date;
939
+ tenantId: string;
940
+ scope?: MeteringScope;
941
+ data: T;
942
+ }
943
+ /** A per-window spend snapshot shared by budget event payloads. */
944
+ interface BudgetDimensionSnapshot {
945
+ nanoUsd?: bigint;
946
+ tokens?: number;
947
+ count?: number;
948
+ }
949
+ /** `ai_tokens.usage.recorded` payload. */
950
+ interface UsageRecordedEventData {
951
+ usageRecordId: string;
952
+ feature: string;
953
+ provider: ProviderId;
954
+ model: string;
955
+ serviceTier: ServiceTier;
956
+ totalTokens: number;
957
+ rawCostNanoUsd: bigint;
958
+ billedCostNanoUsd: bigint;
959
+ enforced: boolean;
960
+ isSystemCost: boolean;
961
+ }
962
+ /** `ai_tokens.usage.reversed` payload. */
963
+ interface UsageReversedEventData {
964
+ usageRecordId: string;
965
+ reversalRecordId: string;
966
+ reason: string;
967
+ }
968
+ /** `ai_tokens.hold.released` payload. */
969
+ interface HoldReleasedEventData {
970
+ holdId: string;
971
+ reason: string;
972
+ expired: boolean;
973
+ }
974
+ /** `ai_tokens.budget.threshold_crossed` payload. */
975
+ interface BudgetThresholdCrossedEventData {
976
+ budgetId: string;
977
+ threshold: number;
978
+ usedFraction: number;
979
+ limit: BudgetDimensionSnapshot;
980
+ spent: BudgetDimensionSnapshot;
981
+ remaining: BudgetDimensionSnapshot;
982
+ resetsAt: Date | null;
983
+ }
984
+ /** `ai_tokens.budget.exceeded` payload. */
985
+ interface BudgetExceededEventData {
986
+ budgetId: string;
987
+ policy: BudgetPolicy;
988
+ dimension: 'cost' | 'tokens' | 'count';
989
+ limit: BudgetDimensionSnapshot;
990
+ spent: BudgetDimensionSnapshot;
991
+ resetsAt: Date | null;
992
+ }
993
+ /** `ai_tokens.budget.projected_exceeded` payload. */
994
+ interface BudgetProjectedExceededEventData {
995
+ budgetId: string;
996
+ projectedAt: Date;
997
+ usedFraction: number;
998
+ resetsAt: Date | null;
999
+ }
1000
+ /** `ai_tokens.wallet.low_balance` payload. */
1001
+ interface WalletLowBalanceEventData {
1002
+ walletId: string;
1003
+ balanceNanoUsd: bigint;
1004
+ thresholdFraction: number;
1005
+ }
1006
+ /** `ai_tokens.wallet.depleted` payload. */
1007
+ interface WalletDepletedEventData {
1008
+ walletId: string;
1009
+ balanceNanoUsd: bigint;
1010
+ }
1011
+ /** `ai_tokens.wallet.granted` payload. */
1012
+ interface WalletGrantedEventData {
1013
+ walletId: string;
1014
+ entryId: string;
1015
+ amountNanoUsd: bigint;
1016
+ expiresAt?: Date;
1017
+ }
1018
+ /** `ai_tokens.price.missing` payload. */
1019
+ interface PriceMissingEventData {
1020
+ provider: ProviderId;
1021
+ model: string;
1022
+ operation: AiOperation;
1023
+ serviceTier: ServiceTier;
1024
+ usageRecordId: string;
1025
+ }
1026
+ /** `ai_tokens.audit` payload — an admin-plane mutation (§14.4). */
1027
+ interface AuditEventData {
1028
+ action: string;
1029
+ actor?: string;
1030
+ details: Record<string, unknown>;
1031
+ }
1032
+
1033
+ /**
1034
+ * @fileoverview The canonical error response body. `AiTokensException` (server
1035
+ * layer) serializes to this shape so hosts get a stable `{ error: {...} }`
1036
+ * envelope (see spec §16).
1037
+ * @layer shared
1038
+ */
1039
+
1040
+ /** The JSON body every {@link AI_TOKENS_ERROR_CODES} failure serializes to. */
1041
+ interface AiTokensErrorResponse {
1042
+ error: {
1043
+ code: keyof typeof AI_TOKENS_ERROR_CODES;
1044
+ message: string;
1045
+ details?: Record<string, unknown>;
1046
+ };
1047
+ }
1048
+
1049
+ /**
1050
+ * @fileoverview The budget persistence port (spec §15.1). Multi-dimension
1051
+ * budgets with atomic conditional consume, signed window adjustment, and window
1052
+ * rotation. Only validated at init when the budget feature is enabled.
1053
+ * @layer server
1054
+ */
1055
+
1056
+ /** A signed or absolute per-window spend delta across the three budget dimensions. */
1057
+ interface BudgetDelta {
1058
+ nanoUsd: bigint;
1059
+ tokens: number;
1060
+ count: number;
1061
+ }
1062
+ /** Limits checked by {@link IBudgetStore.conditionalConsume} (absent = unlimited). */
1063
+ interface BudgetLimits {
1064
+ nanoUsd?: bigint;
1065
+ tokens?: number;
1066
+ count?: number;
1067
+ }
1068
+ /** A window's materialized spend counters. */
1069
+ interface BudgetWindowSpend {
1070
+ spentNanoUsd: bigint;
1071
+ spentTokens: number;
1072
+ spentCount: number;
1073
+ }
1074
+ /** The budget port. */
1075
+ interface IBudgetStore {
1076
+ /** Create or replace a budget. */
1077
+ upsert(budget: Omit<Budget, 'id' | 'createdAt'> & {
1078
+ id?: string;
1079
+ }): Promise<Budget>;
1080
+ /** Delete a budget. */
1081
+ remove(budgetId: string): Promise<void>;
1082
+ /** Load one budget by id — the rotateWindow/reconcileWindow paths read it by id (§10.5). */
1083
+ findBudgetById(budgetId: string): Promise<Budget | null>;
1084
+ /**
1085
+ * Every budget for the tenant scoped to the EXACT scope or tenant-wide (§10.3). A
1086
+ * single {@link MeteringScope} carries no team/user lineage, so the tenant is the
1087
+ * only ancestor resolvable — intermediate ancestors are not matched. Each budget
1088
+ * returned consumes independently; a subject-level budget never exempts the tenant cap.
1089
+ */
1090
+ findMatching(tenantId: string, scope: MeteringScope): Promise<Budget[]>;
1091
+ /**
1092
+ * Atomic multi-dimension conditional consume (§10.8). `false` = a limit would be
1093
+ * exceeded. Creates the window row on first touch.
1094
+ */
1095
+ conditionalConsume(budgetId: string, windowStart: Date, delta: BudgetDelta, limits: BudgetLimits): Promise<boolean>;
1096
+ /** Signed release/adjust of a window's counters (capture delta, release, reverse). */
1097
+ adjustWindow(budgetId: string, windowStart: Date, delta: BudgetDelta): Promise<void>;
1098
+ /** A window's current spend, or `null` when it has no row yet. */
1099
+ getWindow(budgetId: string, windowStart: Date): Promise<BudgetWindowSpend | null>;
1100
+ /** Move a budget's active window start (rotateWindow support). */
1101
+ setWindowStart(budgetId: string, windowStart: Date): Promise<void>;
1102
+ }
1103
+
1104
+ /**
1105
+ * @fileoverview The ledger persistence port (spec §15.1). The append-only usage
1106
+ * ledger: idempotent append, atomic state transitions, and aggregate queries.
1107
+ * Implemented by the official Prisma adapter or a custom store.
1108
+ * @layer server
1109
+ */
1110
+
1111
+ /** Aggregate cost totals returned by {@link ILedgerStore.sumCost}. */
1112
+ interface LedgerCostSummary {
1113
+ rawCostNanoUsd: bigint;
1114
+ billedCostNanoUsd: bigint;
1115
+ surchargeNanoUsd: bigint;
1116
+ totalTokens: number;
1117
+ records: number;
1118
+ }
1119
+ /** A report group-by dimension (spec §13.1). */
1120
+ type ReportGroupBy = 'day' | 'week' | 'month' | 'feature' | 'provider' | 'model' | 'operation' | 'serviceTier' | 'scope' | 'beneficiary' | 'tag' | 'systemCostCategory';
1121
+ /** The append-only usage ledger port. */
1122
+ interface ILedgerStore {
1123
+ /**
1124
+ * Upsert on `(tenantId, idempotencyKey)` — a replay returns the existing record
1125
+ * iff the payload hash matches; a different hash is a conflict (§8.4). When
1126
+ * `hashChain` is true and the record is `posted`, the store reads the tenant's
1127
+ * last chain hash, computes this record's `prevHash`/`hash`, and persists them
1128
+ * ATOMICALLY under a per-tenant lock (§8.6) — the read-compute-write is one
1129
+ * serialized operation so concurrent appends never fork the chain. When false
1130
+ * (the default), no chain lookup or hashing occurs.
1131
+ */
1132
+ append(record: NewUsageRecord, payloadHash: string, hashChain?: boolean): Promise<UsageRecord>;
1133
+ /**
1134
+ * State transitions only (§8.3): `pending→posted` (settle, amounts patched),
1135
+ * `pending→released`, `posted→reversed` (annotation only — amount fields
1136
+ * rejected). Atomic: returns `null` when the record was not in the expected
1137
+ * source state (how exactly one reaper replica wins an expired hold). When
1138
+ * `hashChain` is true and `to` is `posted` (settlement), the store computes and
1139
+ * persists the record's chain `prevHash`/`hash` under the same per-tenant lock (§8.6).
1140
+ */
1141
+ transition(id: string, from: UsageStatus, to: UsageStatus, patch?: Partial<UsageRecord>, hashChain?: boolean): Promise<UsageRecord | null>;
1142
+ /** Look up a record by its per-tenant idempotency key. */
1143
+ findByIdempotencyKey(tenantId: string, key: string): Promise<UsageRecord | null>;
1144
+ /**
1145
+ * Load one record by its global id — the reversal path loads the original to
1146
+ * negate it into a compensating record (§8.5). Returns `null` when absent.
1147
+ */
1148
+ findById(id: string): Promise<UsageRecord | null>;
1149
+ /** Find pending holds older than `olderThan` for the reaper sweep. */
1150
+ findExpiredHolds(olderThan: Date, limit: number): Promise<UsageRecord[]>;
1151
+ /** Query records by filter. */
1152
+ query(filter: LedgerFilter): Promise<UsageRecord[]>;
1153
+ /** Aggregate cost/token totals for a filter. */
1154
+ sumCost(filter: LedgerFilter): Promise<LedgerCostSummary>;
1155
+ /**
1156
+ * OPTIONAL scalable aggregation (spec §13.1): `SUM … GROUP BY` across the report
1157
+ * dimensions, computed in the store (the official Prisma adapter uses SQL). A
1158
+ * store that omits it falls back to `UsageReportService`'s documented
1159
+ * query-and-aggregate-in-memory path, capped at `reporting.maxExportRows`.
1160
+ */
1161
+ summarize?(filter: LedgerFilter, groupBy: ReportGroupBy[]): Promise<UsageSummary[]>;
1162
+ /**
1163
+ * The last posted record's hash for a tenant (hash-chain continuation, §8.6).
1164
+ * The chain is serialized per tenant: the official Prisma adapter takes a
1165
+ * Postgres advisory lock so the last-hash read and the chained append/settle run
1166
+ * as one atomic operation; an in-memory store simulates this with a per-tenant
1167
+ * mutex. Returns `null` before the tenant's first chained record.
1168
+ */
1169
+ lastHash(tenantId: string): Promise<string | null>;
1170
+ }
1171
+
1172
+ /**
1173
+ * @fileoverview The pricing persistence port (spec §15.1). Effective-dated rate
1174
+ * resolution, append-only price upserts, and the model list that powers §6.6
1175
+ * prefix matching.
1176
+ * @layer server
1177
+ */
1178
+
1179
+ /** A distinct priced model tuple, returned by {@link IPricingStore.listModels}. */
1180
+ interface PricedModel {
1181
+ model: string;
1182
+ operation: AiOperation;
1183
+ serviceTier: ServiceTier;
1184
+ }
1185
+ /** The effective-dated price registry port. */
1186
+ interface IPricingStore {
1187
+ /** The price row in effect at `at` for the tuple, or `null` when none matches. */
1188
+ resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
1189
+ /** Close the current open row (`effectiveTo = now`) and insert the new open row. */
1190
+ upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
1191
+ /** Full effective-dated history for a tuple (newest first). */
1192
+ getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
1193
+ /** All distinct (model, operation, serviceTier) for a provider — powers §6.6 prefix matching. */
1194
+ listModels(provider: ProviderId): Promise<PricedModel[]>;
1195
+ }
1196
+
1197
+ /**
1198
+ * @fileoverview The wallet persistence port (spec §15.1). Materialized-balance
1199
+ * wallets with atomic conditional debit, grant allocation, and reconciliation.
1200
+ * Only validated at init when the wallet feature is enabled.
1201
+ * @layer server
1202
+ */
1203
+
1204
+ /** An open grant with its remaining spendable value. */
1205
+ type OpenGrant = WalletEntry & {
1206
+ remainingNanoUsd: bigint;
1207
+ };
1208
+ /** A page of wallet entries with the total count. */
1209
+ interface WalletEntryPage {
1210
+ entries: WalletEntry[];
1211
+ total: number;
1212
+ }
1213
+ /** Filter for {@link IWalletStore.listEntries}. */
1214
+ interface WalletEntryFilter {
1215
+ from?: Date;
1216
+ to?: Date;
1217
+ type?: WalletEntryType;
1218
+ limit?: number;
1219
+ offset?: number;
1220
+ }
1221
+ /** The prepaid wallet port. */
1222
+ interface IWalletStore {
1223
+ /** The wallet for a ref, or `null` when it does not exist yet. */
1224
+ getWallet(ref: WalletRef): Promise<Wallet | null>;
1225
+ /**
1226
+ * Create the wallet when missing (idempotent) and append the entry plus any
1227
+ * grant allocations in one transaction.
1228
+ */
1229
+ appendEntry(ref: WalletRef, entry: NewWalletEntry, allocations?: {
1230
+ grantEntryId: string;
1231
+ amountNanoUsd: bigint;
1232
+ }[]): Promise<WalletEntry>;
1233
+ /** Atomic conditional debit against the materialized balance. `null` = insufficient (§9.4). */
1234
+ conditionalDebit(ref: WalletRef, entry: NewWalletEntry, overdraftNanoUsd: bigint): Promise<WalletEntry | null>;
1235
+ /** Open grants with remaining value, ordered per `order` — feeds allocation. */
1236
+ openGrants(ref: WalletRef, order: 'expiry' | 'priority' | 'fifo'): Promise<OpenGrant[]>;
1237
+ /** A page of entries for a ref. */
1238
+ listEntries(ref: WalletRef, filter?: WalletEntryFilter): Promise<WalletEntryPage>;
1239
+ /** Recompute the materialized balance from Σ entries. */
1240
+ reconcile(ref: WalletRef): Promise<Wallet>;
1241
+ }
1242
+
1243
+ /**
1244
+ * @fileoverview The aggregate persistence bundle (spec §4.1). Ledger + pricing
1245
+ * are always required; the wallet and budget halves are `Partial` and validated
1246
+ * at init only when the corresponding feature is configured.
1247
+ * @layer server
1248
+ */
1249
+
1250
+ /**
1251
+ * The single store object a host provides. `forRoot()` fans it out under each
1252
+ * per-port DI token; a host may override any individual port by binding its token
1253
+ * (§4.6).
1254
+ */
1255
+ interface IAiTokensStore extends ILedgerStore, IPricingStore, Partial<IWalletStore>, Partial<IBudgetStore> {
1256
+ }
1257
+
1258
+ /**
1259
+ * @fileoverview The optional live cross-replica budget counter port (spec §15.1,
1260
+ * §10.8). The fast path for budget enforcement; the DB conditional consume is the
1261
+ * fallback. Values are serialized as int64 decimal strings. Implemented by the
1262
+ * official Redis adapter (`./redis`).
1263
+ * @layer server
1264
+ */
1265
+ /** The live budget counter port. */
1266
+ interface IBudgetCounterStore {
1267
+ /**
1268
+ * Atomically increment `key` by `amount` iff the result stays `<= limit`,
1269
+ * setting the TTL. Returns `false` when the increment would exceed the limit.
1270
+ */
1271
+ incrIfBelow(key: string, amount: bigint, limit: bigint, ttlSeconds: number): Promise<boolean>;
1272
+ /** Decrement `key` by `amount` (release/reverse). */
1273
+ decr(key: string, amount: bigint): Promise<void>;
1274
+ /** Reset `key` (window rotation). */
1275
+ reset(key: string): Promise<void>;
1276
+ }
1277
+
1278
+ /**
1279
+ * @fileoverview The token-estimation port (spec §14.2.1). Used by `hold()`
1280
+ * pre-flight estimation and the aborted-stream fallback (§5.6). The library ships
1281
+ * no tokenizer — the host plugs one in (tiktoken, provider count-tokens, …).
1282
+ * @layer server
1283
+ */
1284
+
1285
+ /** A host-plugged token counter. */
1286
+ interface ITokenizer {
1287
+ /** Count the tokens `text` consumes for the given model/provider (sync or async). */
1288
+ countTokens(input: {
1289
+ text: string;
1290
+ model?: string;
1291
+ provider?: ProviderId;
1292
+ }): number | Promise<number>;
1293
+ }
1294
+
1295
+ /**
1296
+ * @fileoverview The OpenTelemetry telemetry port (spec §14.1). Emits `gen_ai.*`
1297
+ * attributes and metrics. Content is never captured. Without a sink, telemetry is
1298
+ * a no-op.
1299
+ * @layer server
1300
+ */
1301
+
1302
+ /** A pluggable OpenTelemetry sink for gen_ai usage and duration signals. */
1303
+ interface ITelemetrySink {
1304
+ /** Record a usage measurement with its `gen_ai.*` attributes. */
1305
+ recordUsage(attributes: Record<string, string | number>, record: UsageRecord): void;
1306
+ /** Optionally record an operation duration in milliseconds. */
1307
+ recordDuration?(attributes: Record<string, string | number>, milliseconds: number): void;
1308
+ }
1309
+
1310
+ /**
1311
+ * @fileoverview The programmatic event-delivery port (spec §12.1). A sink ships
1312
+ * events to webhooks, queues, or realtime fan-out. Sink failures are logged,
1313
+ * never thrown into the metering path. `bigint` payload fields are serialized as
1314
+ * decimal strings at the JSON boundary (§15.5).
1315
+ * @layer server
1316
+ */
1317
+
1318
+ /** A programmatic sink for the library's typed events. */
1319
+ interface IEventSink {
1320
+ /** Deliver one event (at-least-once). */
1321
+ deliver(event: AiTokensEvent): Promise<void>;
1322
+ }
1323
+
1324
+ /**
1325
+ * @fileoverview The optional prompt/response content sidecar port (spec §14.2).
1326
+ * The immutable ledger NEVER stores text; hosts opt into a separate, redacted,
1327
+ * short-TTL sidecar. `purge()` supports erasure requests independently of the
1328
+ * ledger.
1329
+ * @layer server
1330
+ */
1331
+ /** The opt-in, redacted, short-TTL content sidecar port. */
1332
+ interface IContentStore {
1333
+ /** Persist masked text for a record with a TTL. */
1334
+ put(input: {
1335
+ usageRecordId: string;
1336
+ tenantId: string;
1337
+ role: 'prompt' | 'completion';
1338
+ text: string;
1339
+ ttlSeconds: number;
1340
+ }): Promise<void>;
1341
+ /** Delete stored text by tenant, record, or subject; returns the number purged. */
1342
+ purge(filter: {
1343
+ tenantId: string;
1344
+ usageRecordId?: string;
1345
+ subjectId?: string;
1346
+ }): Promise<number>;
1347
+ }
1348
+
1349
+ /**
1350
+ * @fileoverview The markup-policy port (spec §7.2). Resolves the markup
1351
+ * multiplier per call from the scope/model/feature context. Resolved once per
1352
+ * record; the resolved 4-dp value is persisted. A throwing policy fails the call
1353
+ * (never a silent 1.0 fallback).
1354
+ * @layer server
1355
+ */
1356
+
1357
+ /** A host policy that varies the markup multiplier per call. */
1358
+ interface IMarkupPolicy {
1359
+ /** Return the multiplier for this call (validated finite and `> 0`; sync or async). */
1360
+ resolve(ctx: {
1361
+ scope: MeteringScope;
1362
+ provider: ProviderId;
1363
+ model: string;
1364
+ operation: AiOperation;
1365
+ serviceTier: ServiceTier;
1366
+ feature?: string;
1367
+ }): number | Promise<number>;
1368
+ }
1369
+
1370
+ /**
1371
+ * @fileoverview The per-call metering context and the metered-result wrapper
1372
+ * (spec §11.1). The context carries the payer scope, feature, normalization
1373
+ * instructions, and idempotency key. It is TRUSTED INPUT — the host builds it
1374
+ * from its VERIFIED auth layer, never from client-supplied body/query fields.
1375
+ * @layer server
1376
+ */
1377
+
1378
+ /** Everything the metering path needs about one call. */
1379
+ interface MeteringContext {
1380
+ tenantId: string;
1381
+ /** The PAYER — enforcement target. */
1382
+ scope: MeteringScope;
1383
+ /** Optional distinct beneficiary (reporting dimension). */
1384
+ beneficiary?: MeteringScope;
1385
+ /** Optional actor id (audit). */
1386
+ requestedBy?: string;
1387
+ feature: string;
1388
+ tags?: string[];
1389
+ /** Normalization/rating instructions for `meter()`/interceptor. */
1390
+ preset?: ProviderPreset;
1391
+ ratingMode?: RatingMode;
1392
+ /** Price-lookup override for deployment-named models (Azure/Bedrock) — §6.6. */
1393
+ baseModel?: string;
1394
+ /** Declared tier when it cannot come from the response (e.g. Batch API result files). */
1395
+ serviceTier?: ServiceTier;
1396
+ /** Non-token line items the provider does not report in usage. */
1397
+ extraUnits?: Record<string, number>;
1398
+ /** Strongly recommended; derive with `deriveIdempotencyKey(payload)` (§8.4). */
1399
+ idempotencyKey?: string;
1400
+ correlationId?: string;
1401
+ /** `record()` only: also debit wallet + consume budgets post-hoc. Default `false`. */
1402
+ enforce?: boolean;
1403
+ isSystemCost?: boolean;
1404
+ systemCostCategory?: string;
1405
+ }
1406
+ /** The wrapper `meter()` returns: the function result plus its settled usage record. */
1407
+ interface MeterResult<T> {
1408
+ result: T;
1409
+ usage: UsageRecord;
1410
+ }
1411
+ /** The pure pre-flight cost estimate `estimateCost()` returns. */
1412
+ interface CostEstimate {
1413
+ rawCostNanoUsd: bigint;
1414
+ billedCostNanoUsd: bigint;
1415
+ }
1416
+
1417
+ /**
1418
+ * @fileoverview The auth-hold types (spec §11.1). A {@link Hold} is a plain,
1419
+ * method-free object that survives process boundaries (store it in Redis, hand it
1420
+ * to a worker); `capture()`/`release()` revalidate it against the store AND the
1421
+ * caller's tenant/scope (§14.4) and read the authoritative amounts from the pending
1422
+ * record, so the Hold's own fields are informational. Its `bigint` money field
1423
+ * crosses a JSON boundary via `toJsonSafe()` as a decimal string, exactly like every
1424
+ * other library value (§15.5) — never `JSON.stringify` a bigint directly. A
1425
+ * {@link HoldEstimate} is the discriminated union of ways to size a hold.
1426
+ * @layer server
1427
+ */
1428
+
1429
+ /**
1430
+ * A reservation over real wallet/budget headroom, settled by `capture()`. Plain and
1431
+ * method-free; serialize with `toJsonSafe()` (its `bigint` field becomes a decimal
1432
+ * string, §15.5) to cross a process/JSON boundary.
1433
+ */
1434
+ interface Hold {
1435
+ /** Equals the pending `UsageRecord` id. */
1436
+ id: string;
1437
+ tenantId: string;
1438
+ scope: MeteringScope;
1439
+ estimatedTokens: number;
1440
+ /** Billed (post-markup) estimate. Serialize via `toJsonSafe()` at a JSON boundary (§15.5). */
1441
+ estimatedCostNanoUsd: bigint;
1442
+ expiresAt: Date;
1443
+ }
1444
+ /** The three ways to size a hold, as a discriminated union. */
1445
+ type HoldEstimate = {
1446
+ provider: ProviderId;
1447
+ model: string;
1448
+ operation: AiOperation;
1449
+ serviceTier?: ServiceTier;
1450
+ inputTokens: number;
1451
+ maxOutputTokens: number;
1452
+ } | {
1453
+ tokens: number;
1454
+ } | {
1455
+ amountNanoUsd: bigint;
1456
+ };
1457
+
1458
+ /**
1459
+ * @fileoverview The module configuration contract (spec §4.1) and the async
1460
+ * dynamic-module options pattern. `store` is required; every feature block is
1461
+ * opt-in and enables its service/guard only when present. All persisted money is
1462
+ * nano-USD regardless of `currency` — that setting is presentation-only (§7.4).
1463
+ * @layer server
1464
+ */
1465
+
1466
+ /** The synchronous configuration for `BymaxAiTokensModule.forRoot()`. */
1467
+ interface BymaxAiTokensModuleOptions {
1468
+ /** Persistence adapter implementing the storage ports. REQUIRED. */
1469
+ store: IAiTokensStore;
1470
+ /**
1471
+ * Resolves the metering scope from the current request for `BudgetGuard` and
1472
+ * `MeteringInterceptor`. TRUSTED INPUT: read the host's VERIFIED auth context
1473
+ * (JWT claims/session), never client-supplied body/query fields. REQUIRED only
1474
+ * when the guard/interceptor/decorators are used.
1475
+ */
1476
+ scopeResolver?: (ctx: ExecutionContext) => MeteringContext | Promise<MeteringContext>;
1477
+ /** Default rating mode; overridable per call/preset. Default `'rate-table'`. */
1478
+ ratingMode?: RatingMode;
1479
+ /** Presentation currency for reports/exports/status. Persisted money is always nano-USD. Default `'USD'`. */
1480
+ currency?: string;
1481
+ /**
1482
+ * FX resolver USD → `currency`, returning integer nano-units of `currency` per
1483
+ * USD. REQUIRED when `currency !== 'USD'` (else `AI_TOKENS_FX_REQUIRED` at init).
1484
+ */
1485
+ fx?: (date: Date, currency: string) => Promise<bigint> | bigint;
1486
+ /** Price registry behavior. */
1487
+ pricing?: {
1488
+ /** Seed from `MODEL_PRICES_SEED` on first boot (idempotent, advisory-locked). Default `true`. */
1489
+ seedFromSnapshot?: boolean;
1490
+ /** Throw `AI_TOKENS_PRICE_NOT_FOUND` when no rate matches; else cost 0 + `priceMissing`. Default `true`. */
1491
+ strict?: boolean;
1492
+ /** In-memory rate cache TTL. Default `300_000` ms. */
1493
+ cacheTtlMs?: number;
1494
+ /** Model-ID alias map consulted during rate resolution (§6.6). */
1495
+ modelAliases?: Record<string, string>;
1496
+ };
1497
+ /**
1498
+ * Markup / margin — a flat multiplier or a policy object. Validated finite, `> 0`,
1499
+ * at most 4 decimal places. Default `1.0` (bill at cost).
1500
+ */
1501
+ markup?: number | IMarkupPolicy;
1502
+ /** Enables `WalletService` when present. */
1503
+ wallets?: {
1504
+ /** 1 credit = this many nano-USD. Default `1_000_000_000n` (1 credit = $1). */
1505
+ creditRateNanoUsd?: bigint;
1506
+ /** Allowed negative balance (postpaid overdraft) in nano-USD. Default `0n`. */
1507
+ overdraftNanoUsd?: bigint;
1508
+ /** Grant burn order. Default `'expiry'`. */
1509
+ burnOrder?: 'expiry' | 'priority' | 'fifo';
1510
+ };
1511
+ /** Enables `BudgetService` + `BudgetGuard` when present. */
1512
+ budgets?: {
1513
+ /** Default enforcement policy when a budget is exceeded. Default `'block'`. */
1514
+ defaultPolicy?: BudgetPolicy;
1515
+ /** Soft-alert thresholds as fractions of the limit. Default `[0.8, 1.0]`. */
1516
+ alertThresholds?: number[];
1517
+ /** Optional live cross-replica spend counter (Redis). Falls back to DB atomic consume. */
1518
+ counter?: IBudgetCounterStore;
1519
+ /** Enforce budgets as a hard ceiling even if the counter store is unavailable. Default `true`. */
1520
+ failClosed?: boolean;
1521
+ /** Host callback invoked when a matched budget has policy `'throttle'`. */
1522
+ onThrottle?: (ctx: {
1523
+ context: MeteringContext;
1524
+ budget: Budget;
1525
+ status: BudgetStatus;
1526
+ }) => void | Promise<void>;
1527
+ };
1528
+ /** Hold lifecycle (applies whenever `hold()`/`meter()` is used). */
1529
+ holds?: {
1530
+ /** Pending-hold TTL; expired holds are swept. Default `3_600` s. */
1531
+ ttlSeconds?: number;
1532
+ /** Reaper sweep interval. Default `300` s. */
1533
+ reaperIntervalSeconds?: number;
1534
+ };
1535
+ /** Ledger extras. */
1536
+ ledger?: {
1537
+ /** Per-tenant tamper-evident hash chain over posted records (§8.6). Default `false`. */
1538
+ hashChain?: boolean;
1539
+ };
1540
+ /** Pre-flight token estimation used by `hold()` when the caller supplies text instead of counts. */
1541
+ tokenizer?: ITokenizer;
1542
+ /** Typed event emission (§12). */
1543
+ events?: {
1544
+ /** Emit through `@nestjs/event-emitter` when installed. Default `true`. */
1545
+ emitter?: boolean;
1546
+ /** Additional programmatic sink (webhooks, queues). */
1547
+ sink?: IEventSink;
1548
+ };
1549
+ /** OpenTelemetry emission of `gen_ai.*` attributes and metrics. */
1550
+ telemetry?: {
1551
+ sink?: ITelemetrySink;
1552
+ /** Emit token-usage + duration metrics. Default `true` when a sink is present. */
1553
+ metrics?: boolean;
1554
+ };
1555
+ /** Reporting + export. */
1556
+ reporting?: {
1557
+ /** Max rows a single export streams before requiring pagination. Default `1_000_000`. */
1558
+ maxExportRows?: number;
1559
+ };
1560
+ /**
1561
+ * PII policy for the optional prompt/response text sidecar. The immutable ledger
1562
+ * NEVER stores text. Default `undefined` (no text is ever stored).
1563
+ */
1564
+ content?: {
1565
+ store: IContentStore;
1566
+ /** Mask function applied before persistence. */
1567
+ mask?: (text: string) => string;
1568
+ /** Retention in seconds. Default `604_800` (7 days). */
1569
+ ttlSeconds?: number;
1570
+ };
1571
+ }
1572
+ /** Factory contract for `useExisting` / `useClass` async configuration. */
1573
+ interface BymaxAiTokensModuleOptionsFactory {
1574
+ createAiTokensOptions(): BymaxAiTokensModuleOptions | Promise<BymaxAiTokensModuleOptions>;
1575
+ }
1576
+ /**
1577
+ * Asynchronous configuration for `BymaxAiTokensModule.forRootAsync()` — the
1578
+ * standard NestJS async dynamic-module options shape (the async factory is not
1579
+ * yet wired; the contract is defined here).
1580
+ */
1581
+ interface BymaxAiTokensModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
1582
+ useFactory?: (...args: unknown[]) => BymaxAiTokensModuleOptions | Promise<BymaxAiTokensModuleOptions>;
1583
+ inject?: readonly (string | symbol | Type<unknown>)[];
1584
+ useExisting?: Type<BymaxAiTokensModuleOptionsFactory>;
1585
+ useClass?: Type<BymaxAiTokensModuleOptionsFactory>;
1586
+ }
1587
+
1588
+ /**
1589
+ * @fileoverview The `@Global()` dynamic module (spec §2.1/§4.4/§4.6). `forRoot()`
1590
+ * validates and resolves the options synchronously; `forRootAsync()` resolves them
1591
+ * from injected dependencies (`useFactory`/`useClass`/`useExisting`). Both feed the
1592
+ * SAME options-agnostic core providers (each derives its value from the resolved
1593
+ * `BYMAX_AI_TOKENS_OPTIONS` token, so wiring is shared by construction), fanning the
1594
+ * single `store` object out under each per-port token. `forRoot()` registers the
1595
+ * wallet/budget providers only when their feature block is present; `forRootAsync()`
1596
+ * — which cannot inspect the options at module-definition time — registers them
1597
+ * unconditionally, resolving to `null` when disabled (services treat `null` as
1598
+ * absent). The module is global so the guard/interceptor are injectable anywhere.
1599
+ * @layer server/module
1600
+ */
1601
+
1602
+ /**
1603
+ * The root NestJS dynamic module for `@bymax-one/nest-ai-tokens`. Register once
1604
+ * at application root with `forRoot()` or `forRootAsync()`. Exports the metering,
1605
+ * pricing, ledger, wallet, budget, report services and the enforcement primitives.
1606
+ */
1607
+ declare class BymaxAiTokensModule {
1608
+ /**
1609
+ * Configure the module synchronously. Validates and resolves the options, then
1610
+ * returns the wired dynamic module. `PricingService.onModuleInit` seeds the price
1611
+ * registry when `pricing.seedFromSnapshot` is enabled.
1612
+ *
1613
+ * @param options The module options.
1614
+ * @returns The configured dynamic module.
1615
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` / `AI_TOKENS_FX_REQUIRED` on invalid options.
1616
+ */
1617
+ static forRoot(options: BymaxAiTokensModuleOptions): DynamicModule;
1618
+ /**
1619
+ * Configure the module asynchronously, resolving the options from injected
1620
+ * dependencies via `useFactory`/`useClass`/`useExisting` — the family pattern.
1621
+ * The resolved options feed the SAME core providers as {@link forRoot}; the
1622
+ * wallet/budget providers are registered unconditionally and resolve to `null`
1623
+ * when their feature is disabled.
1624
+ *
1625
+ * @param asyncOptions The async options descriptor.
1626
+ * @returns The configured dynamic module.
1627
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when no async style is supplied or the factory rejects/produces invalid options.
1628
+ */
1629
+ static forRootAsync(asyncOptions: BymaxAiTokensModuleAsyncOptions): DynamicModule;
1630
+ }
1631
+
1632
+ /**
1633
+ * @fileoverview The eleven `Symbol()` dependency-injection tokens (spec §4.5).
1634
+ * `forRoot()` registers the resolved options plus each store port and optional
1635
+ * collaborator under these tokens; a host may override any individual port by
1636
+ * binding its token directly (§4.6).
1637
+ * @layer server
1638
+ */
1639
+ /** The fully-resolved {@link ResolvedAiTokensOptions} object. */
1640
+ declare const BYMAX_AI_TOKENS_OPTIONS: unique symbol;
1641
+ /** The ledger store port. */
1642
+ declare const BYMAX_AI_TOKENS_LEDGER_STORE: unique symbol;
1643
+ /** The pricing store port. */
1644
+ declare const BYMAX_AI_TOKENS_PRICING_STORE: unique symbol;
1645
+ /** The wallet store port (only bound when wallets are enabled). */
1646
+ declare const BYMAX_AI_TOKENS_WALLET_STORE: unique symbol;
1647
+ /** The budget store port (only bound when budgets are enabled). */
1648
+ declare const BYMAX_AI_TOKENS_BUDGET_STORE: unique symbol;
1649
+ /** The optional live budget counter port. */
1650
+ declare const BYMAX_AI_TOKENS_BUDGET_COUNTER: unique symbol;
1651
+ /** The optional tokenizer port. */
1652
+ declare const BYMAX_AI_TOKENS_TOKENIZER: unique symbol;
1653
+ /** The optional telemetry sink port. */
1654
+ declare const BYMAX_AI_TOKENS_TELEMETRY: unique symbol;
1655
+ /** The optional event sink port. */
1656
+ declare const BYMAX_AI_TOKENS_EVENT_SINK: unique symbol;
1657
+ /** The optional content sidecar port. */
1658
+ declare const BYMAX_AI_TOKENS_CONTENT_STORE: unique symbol;
1659
+ /** The optional logger. */
1660
+ declare const BYMAX_AI_TOKENS_LOGGER: unique symbol;
1661
+
1662
+ /**
1663
+ * @fileoverview The fully-resolved options every service consumes (spec §4.2/§4.6).
1664
+ * Each opt-in feature is a discriminated union — `{ enabled: false }` or
1665
+ * `{ enabled: true, ...resolved }` — so services never branch on `undefined`.
1666
+ * @layer server
1667
+ */
1668
+
1669
+ /** Resolved pricing behavior. */
1670
+ interface ResolvedPricingOptions {
1671
+ seedFromSnapshot: boolean;
1672
+ strict: boolean;
1673
+ cacheTtlMs: number;
1674
+ modelAliases: Record<string, string>;
1675
+ }
1676
+ /** Resolved hold lifecycle. */
1677
+ interface ResolvedHoldsOptions {
1678
+ ttlSeconds: number;
1679
+ reaperIntervalSeconds: number;
1680
+ }
1681
+ /** Resolved ledger extras. */
1682
+ interface ResolvedLedgerOptions {
1683
+ hashChain: boolean;
1684
+ }
1685
+ /** Resolved event emission. */
1686
+ interface ResolvedEventsOptions {
1687
+ emitter: boolean;
1688
+ sink?: IEventSink | undefined;
1689
+ }
1690
+ /** Resolved reporting limits. */
1691
+ interface ResolvedReportingOptions {
1692
+ maxExportRows: number;
1693
+ }
1694
+ /** Resolved wallet feature (disabled or fully-resolved). */
1695
+ type ResolvedWalletsOptions = {
1696
+ enabled: false;
1697
+ } | {
1698
+ enabled: true;
1699
+ creditRateNanoUsd: bigint;
1700
+ overdraftNanoUsd: bigint;
1701
+ burnOrder: 'expiry' | 'priority' | 'fifo';
1702
+ };
1703
+ /** Resolved budget feature (disabled or fully-resolved). */
1704
+ type ResolvedBudgetsOptions = {
1705
+ enabled: false;
1706
+ } | {
1707
+ enabled: true;
1708
+ defaultPolicy: BudgetPolicy;
1709
+ alertThresholds: readonly number[];
1710
+ failClosed: boolean;
1711
+ counter?: IBudgetCounterStore | undefined;
1712
+ onThrottle?: BudgetThrottleCallback | undefined;
1713
+ };
1714
+ /** The host callback invoked when a matched budget has policy `'throttle'`. */
1715
+ type BudgetThrottleCallback = (ctx: {
1716
+ context: MeteringContext;
1717
+ budget: Budget;
1718
+ status: BudgetStatus;
1719
+ }) => void | Promise<void>;
1720
+ /** Resolved telemetry feature (disabled or fully-resolved). */
1721
+ type ResolvedTelemetryOptions = {
1722
+ enabled: false;
1723
+ } | {
1724
+ enabled: true;
1725
+ sink: ITelemetrySink;
1726
+ metrics: boolean;
1727
+ };
1728
+ /** Resolved content sidecar feature (disabled or fully-resolved). */
1729
+ type ResolvedContentOptions = {
1730
+ enabled: false;
1731
+ } | {
1732
+ enabled: true;
1733
+ store: IContentStore;
1734
+ mask?: ((text: string) => string) | undefined;
1735
+ ttlSeconds: number;
1736
+ };
1737
+ /** The fully-resolved options object bound under `BYMAX_AI_TOKENS_OPTIONS`. */
1738
+ interface ResolvedAiTokensOptions {
1739
+ store: IAiTokensStore;
1740
+ scopeResolver?: ((ctx: ExecutionContext) => MeteringContext | Promise<MeteringContext>) | undefined;
1741
+ ratingMode: RatingMode;
1742
+ currency: string;
1743
+ fx?: ((date: Date, currency: string) => Promise<bigint> | bigint) | undefined;
1744
+ /** A resolved 4-dp multiplier or a markup policy. */
1745
+ markup: number | IMarkupPolicy;
1746
+ pricing: ResolvedPricingOptions;
1747
+ wallets: ResolvedWalletsOptions;
1748
+ budgets: ResolvedBudgetsOptions;
1749
+ holds: ResolvedHoldsOptions;
1750
+ ledger: ResolvedLedgerOptions;
1751
+ tokenizer?: ITokenizer | undefined;
1752
+ events: ResolvedEventsOptions;
1753
+ telemetry: ResolvedTelemetryOptions;
1754
+ reporting: ResolvedReportingOptions;
1755
+ content: ResolvedContentOptions;
1756
+ }
1757
+
1758
+ /**
1759
+ * @fileoverview `PricingService` — effective-dated rate resolution with the §6.6
1760
+ * six-step model-resolution chain, an in-memory TTL cache, price upserts, and the
1761
+ * idempotent snapshot seed (§6.3/§6.4). A call is priced at the rate in effect at
1762
+ * its timestamp; batch/flex/priority tiers must find their own row (no silent
1763
+ * fallback to standard rates).
1764
+ * @layer server
1765
+ */
1766
+
1767
+ /** Inputs to {@link PricingService.resolveRate}. */
1768
+ interface ResolveRateInput {
1769
+ provider: ProviderId;
1770
+ model: string;
1771
+ operation: AiOperation;
1772
+ at: Date;
1773
+ /** Defaults to `'standard'`. */
1774
+ serviceTier?: ServiceTier;
1775
+ /** Price-lookup override for deployment-named models (Azure/Bedrock) — §6.6 step 2. */
1776
+ baseModel?: string;
1777
+ }
1778
+ /**
1779
+ * Effective-dated rate resolution with a six-step model-resolution chain, an
1780
+ * in-memory TTL cache, price upserts, and idempotent snapshot seed. Past
1781
+ * records are never re-rated (point-in-time pricing). See file overview.
1782
+ */
1783
+ declare class PricingService implements OnModuleInit {
1784
+ private readonly options;
1785
+ private readonly store;
1786
+ private readonly now;
1787
+ private readonly cache;
1788
+ /**
1789
+ * @param options The resolved module options (pricing config lives here).
1790
+ * @param store The pricing store port.
1791
+ * @param now A clock returning epoch milliseconds; injected for testability.
1792
+ */
1793
+ constructor(options: ResolvedAiTokensOptions, store: IPricingStore, now?: () => number);
1794
+ /** Seed the registry on boot when enabled. */
1795
+ onModuleInit(): Promise<void>;
1796
+ /**
1797
+ * Resolve the price version in effect at `input.at`, applying the §6.6 chain.
1798
+ *
1799
+ * @param input The provider/model/operation/timestamp (+ optional tier/baseModel).
1800
+ * @returns The matching price version, or `null` in non-strict mode on a miss.
1801
+ * @throws {AiTokensException} `AI_TOKENS_PRICE_NOT_FOUND` on a miss in strict mode.
1802
+ */
1803
+ resolveRate(input: ResolveRateInput): Promise<PriceVersion | null>;
1804
+ /**
1805
+ * Close the current open row and insert a new one; clears the resolution cache.
1806
+ * ADMIN PLANE: this sets prices — the host MUST restrict it to privileged roles
1807
+ * (§14.4) and should audit every call.
1808
+ *
1809
+ * @param input The new price version (rates default to `0n`, tier to `'standard'`).
1810
+ * @returns The newly-inserted open price row.
1811
+ */
1812
+ upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
1813
+ /** Full effective-dated history for a tuple. */
1814
+ getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
1815
+ /** Seed the registry from the pinned snapshot; idempotent and advisory-locked. */
1816
+ seedFromSnapshot(): Promise<void>;
1817
+ /** The §6.6 six-step model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1818
+ private resolveUncached;
1819
+ /** §6.6 step 5: longest-`startsWith` match among priced models for provider+operation+tier. */
1820
+ private resolveByLongestPrefix;
1821
+ }
1822
+
1823
+ /**
1824
+ * @fileoverview The optional per-tenant tamper-evident hash chain (spec §8.6).
1825
+ * When `ledger.hashChain` is enabled, each SETTLED record (`posted`, including
1826
+ * compensating records) hashes the previous settled record for its tenant, so any
1827
+ * post-hoc modification of a record's content becomes detectable — the SOC 2
1828
+ * expectation for billing records.
1829
+ *
1830
+ * The hashed field set is the immutable settled content: the payload-hash fields
1831
+ * (§8.4) plus the record `id`. `status` and `reversedByRecordId` are deliberately
1832
+ * EXCLUDED because the sole permitted post-posting mutation is the annotation-only
1833
+ * `posted → reversed` flip (§8.3): the chain must survive it (a reversed record
1834
+ * keeps its settlement hash, §8.6 rule 1), so the hash cannot depend on fields
1835
+ * that the annotation changes. Any change to a content, cost, token, or id field
1836
+ * breaks the chain.
1837
+ *
1838
+ * THROUGHPUT CAVEAT (§20.2): enabling the chain serializes posted writes per tenant
1839
+ * (a Postgres advisory lock in the official adapter) — a measurable cost on hot
1840
+ * tenants. Leave `ledger.hashChain` off unless tamper evidence is required.
1841
+ * @layer server
1842
+ */
1843
+
1844
+ /** The outcome of {@link LedgerService.verifyChain}: intact, or the first broken record. */
1845
+ type ChainVerification = {
1846
+ valid: true;
1847
+ } | {
1848
+ valid: false;
1849
+ brokenAtRecordId: string;
1850
+ };
1851
+
1852
+ /**
1853
+ * @fileoverview `LedgerService` — the append-only usage ledger core over
1854
+ * `ILedgerStore` (spec §8): exactly-once append (payload-hash replay-or-conflict,
1855
+ * §8.4), filtered queries, cost aggregation, the lifecycle state machine
1856
+ * (`transition`, §8.3), the ledger-only compensation primitive (`reverse`, §8.5
1857
+ * steps 1–2), and the opt-in per-tenant tamper-evident hash chain (§8.6). Balance
1858
+ * and spend math sums `posted` + `reversed` records only (§8.3), so those are the
1859
+ * default query statuses. The service NEVER issues an `UPDATE`/`DELETE` of a posted
1860
+ * amount — corrections are compensating records (a `posted` row with negated
1861
+ * amounts) and the sole permitted post-posting mutation is the annotation-only
1862
+ * `posted → reversed` flip.
1863
+ * @layer server
1864
+ */
1865
+
1866
+ /**
1867
+ * Caller-supplied fields for {@link LedgerService.append}; the service derives
1868
+ * `idempotencyKey` (from `ctxKey` or a random UUID) and `totalTokens`.
1869
+ */
1870
+ type LedgerAppendInput = Omit<NewUsageRecord, 'idempotencyKey' | 'totalTokens'>;
1871
+ /** The resolved-options subset the ledger consumes (the hash-chain flag, §8.6). */
1872
+ type LedgerServiceOptions = Pick<ResolvedAiTokensOptions, 'ledger'>;
1873
+ /**
1874
+ * Audit hook invoked by {@link LedgerService.verifyChain}. The module wires it to
1875
+ * the event dispatcher's `ai_tokens.audit` emission; it defaults to a no-op so the
1876
+ * service has no dependency cycle on the dispatcher.
1877
+ */
1878
+ type LedgerAuditHook = (action: string, details: Record<string, unknown>) => void;
1879
+ /**
1880
+ * Append-only usage ledger over {@link ILedgerStore}. Provides exactly-once
1881
+ * append (content-derived idempotency), lifecycle transitions, filtered queries,
1882
+ * cost aggregation, and the ledger-only compensation primitive. See file overview.
1883
+ */
1884
+ declare class LedgerService {
1885
+ private readonly store;
1886
+ private readonly options;
1887
+ private readonly audit;
1888
+ /**
1889
+ * @param store The append-only ledger store port.
1890
+ * @param options The resolved options carrying the hash-chain flag (§8.6).
1891
+ * @param audit The audit hook for chain verification; wired by the module.
1892
+ */
1893
+ constructor(store: ILedgerStore, options?: LedgerServiceOptions, audit?: LedgerAuditHook);
1894
+ /** Whether the per-tenant tamper-evident hash chain is enabled (§8.6). */
1895
+ private get hashChainEnabled();
1896
+ /**
1897
+ * Append a record exactly once. The idempotency key is `ctxKey` when supplied
1898
+ * — a content-derived key deduplicates retries (§8.4) — or a random UUID
1899
+ * otherwise, in which case the record is still written once but retries are NOT
1900
+ * deduplicated. A replay with a matching payload hash returns the stored record
1901
+ * unchanged and writes nothing; the same key with a different payload throws
1902
+ * `AI_TOKENS_IDEMPOTENCY_CONFLICT`.
1903
+ *
1904
+ * @param input The record content (without `idempotencyKey`/`totalTokens`).
1905
+ * @param ctxKey The host-supplied idempotency key, when available.
1906
+ * @returns The stored record (new or the existing one on a matching replay).
1907
+ * @throws {AiTokensException} `AI_TOKENS_IDEMPOTENCY_CONFLICT` on a payload mismatch.
1908
+ */
1909
+ append(input: LedgerAppendInput, ctxKey?: string): Promise<UsageRecord>;
1910
+ /**
1911
+ * Look up a record by its per-tenant idempotency key.
1912
+ *
1913
+ * @param tenantId The owning tenant.
1914
+ * @param key The idempotency key.
1915
+ * @returns The record, or `null` when none exists.
1916
+ */
1917
+ findByIdempotencyKey(tenantId: string, key: string): Promise<UsageRecord | null>;
1918
+ /**
1919
+ * Load one record by its global id. The metering lifecycle loads a pending hold
1920
+ * by its id to settle or void it, and the orchestrated reversal loads the
1921
+ * (now-annotated) original to restore its wallet/budget effects (§8.5).
1922
+ *
1923
+ * @param id The record id.
1924
+ * @returns The record, or `null` when none exists.
1925
+ */
1926
+ findById(id: string): Promise<UsageRecord | null>;
1927
+ /**
1928
+ * Find pending holds whose TTL has elapsed — the reaper's sweep source (§8.3).
1929
+ * Returns at most `limit` records so the reaper reclaims in bounded batches.
1930
+ *
1931
+ * @param olderThan The cutoff instant (records created at or before are expired).
1932
+ * @param limit The maximum batch size.
1933
+ * @returns The expired pending records.
1934
+ */
1935
+ findExpiredHolds(olderThan: Date, limit: number): Promise<UsageRecord[]>;
1936
+ /**
1937
+ * Query records. When the filter omits `status`, it defaults to the
1938
+ * balance-contributing statuses (`posted`, `reversed`) so callers see settled
1939
+ * spend without special-casing (§8.3).
1940
+ *
1941
+ * @param filter The ledger query filter.
1942
+ * @returns The matching records.
1943
+ */
1944
+ query(filter: LedgerFilter): Promise<UsageRecord[]>;
1945
+ /**
1946
+ * Aggregate cost/token totals. Defaults to `posted` + `reversed` records, so a
1947
+ * reversed record and its compensating negation net to zero (§8.3).
1948
+ *
1949
+ * @param filter The ledger query filter.
1950
+ * @returns The aggregate cost/token summary.
1951
+ */
1952
+ sumCost(filter: LedgerFilter): Promise<LedgerCostSummary>;
1953
+ /** Default the status filter to the balance-contributing statuses when unset. */
1954
+ private withBalanceStatuses;
1955
+ /**
1956
+ * Move a record between lifecycle states (§8.3). Only three transitions are
1957
+ * legal — `pending → posted` (settlement; only the settlement amount/cost fields
1958
+ * may be patched), `pending → released` (void, no amount patch), and
1959
+ * `posted → reversed` (annotation with `reversedByRecordId` only). An illegal
1960
+ * `(from, to)` pair or an out-of-contract patch — including any attempt to mutate
1961
+ * an immutable field such as `id`/`tenantId`/`idempotencyKey` — is a caller bug
1962
+ * and throws `AI_TOKENS_IDEMPOTENCY_CONFLICT`. A legal
1963
+ * transition whose record is NOT currently in `from` returns `null` — the atomic
1964
+ * claim that lets exactly one reaper replica win an expired hold (§8.3).
1965
+ *
1966
+ * @param id The record id.
1967
+ * @param from The required current status.
1968
+ * @param to The target status.
1969
+ * @param patch The fields to set alongside the status flip.
1970
+ * @returns The updated record, or `null` on a from-state mismatch.
1971
+ * @throws {AiTokensException} `AI_TOKENS_IDEMPOTENCY_CONFLICT` on an illegal transition/patch.
1972
+ */
1973
+ transition(id: string, from: UsageStatus, to: UsageStatus, patch?: Partial<UsageRecord>): Promise<UsageRecord | null>;
1974
+ /**
1975
+ * Verify a tenant's tamper-evident hash chain (§8.6). Walks its settled records
1976
+ * (`posted` + `reversed`) in append order and recomputes each record's chain hash
1977
+ * from its stored `prevHash` link, comparing it to the stored `hash`; reports the
1978
+ * first record whose hash does not match (a post-hoc content modification).
1979
+ * Recomputing from each record's own stored link makes verification independent
1980
+ * of walk order and of any `from`/`to` window (the first in-range record keeps
1981
+ * its real predecessor link). Always emits an `ai_tokens.audit` hook.
1982
+ *
1983
+ * @param tenantId The tenant whose chain to verify.
1984
+ * @param from Optional inclusive lower bound on `occurredAt`.
1985
+ * @param to Optional inclusive upper bound on `occurredAt`.
1986
+ * @returns `{ valid: true }`, or `{ valid: false, brokenAtRecordId }`.
1987
+ */
1988
+ verifyChain(tenantId: string, from?: Date, to?: Date): Promise<ChainVerification>;
1989
+ /**
1990
+ * Reverse a posted record with a compensating record (§8.5, ledger-only steps
1991
+ * 1–2). Appends a `posted` record that exactly negates the original's token
1992
+ * counts and costs (keyed `reverse:<id>`, so a retry is a clean replay), then
1993
+ * annotates the original `posted → reversed`. Reversing a `pending`/`released`
1994
+ * or already-`reversed` record is invalid and throws
1995
+ * `AI_TOKENS_IDEMPOTENCY_CONFLICT`.
1996
+ *
1997
+ * ADMIN PLANE: reversal is a privileged mutation — the host MUST restrict it to
1998
+ * privileged roles (§14.4). This ledger-only primitive does NOT touch wallets,
1999
+ * budgets, counters, or events, and does not persist `reason` (the immutable
2000
+ * record has no reason column); the orchestrating `MeteringService.reverse`
2001
+ * surfaces `reason` on the `ai_tokens.usage.reversed` event.
2002
+ *
2003
+ * @param recordId The id of the posted record to reverse.
2004
+ * @param reason The caller's stated reason (echoed in the conflict details when the record cannot be reversed).
2005
+ * @returns The compensating record.
2006
+ * @throws {AiTokensException} `AI_TOKENS_IDEMPOTENCY_CONFLICT` when the record is missing or not `posted`.
2007
+ */
2008
+ reverse(recordId: string, reason: string): Promise<UsageRecord>;
2009
+ /**
2010
+ * Enforce the legality table and per-transition patch contract (§8.3/§8.5) via
2011
+ * the shared guard the store adapters also consume, so the service layer and the
2012
+ * store boundary can never drift. Store-agnostic: it runs before the store
2013
+ * applies the patch, so even a store that assigns patches broadly can never
2014
+ * mutate an immutable field (`id`/`tenantId`/`idempotencyKey`) or write an
2015
+ * out-of-contract column.
2016
+ */
2017
+ private assertLegalTransition;
2018
+ }
2019
+
2020
+ /**
2021
+ * @fileoverview `MarkupResolver` — the internal per-call markup engine (spec §7.2).
2022
+ * Resolves the billed/raw multiplier from either a static number (validated and
2023
+ * rounded to 4 dp at init) or an `IMarkupPolicy` (resolved and validated per call),
2024
+ * and returns the resolved 4-dp value plus a bound `applyMarkup` so the caller
2025
+ * persists exactly what was applied. Markup applies in BOTH rating modes (§2.3) —
2026
+ * the returned `apply` is used on a rate-table cost and on a provider-reported cost
2027
+ * alike. A policy that throws or returns an invalid multiplier fails the call with
2028
+ * `AI_TOKENS_INVALID_CONFIG` — never a silent `1.0` fallback. Internal: not part of
2029
+ * the public barrel; the module registers it.
2030
+ * @layer server
2031
+ */
2032
+
2033
+ /** The per-call context an `IMarkupPolicy` receives (spec §7.2). */
2034
+ type MarkupContext = Parameters<IMarkupPolicy['resolve']>[0];
2035
+ /** The resolved markup: the 4-dp multiplier actually applied plus a bound applier. */
2036
+ interface ResolvedMarkup {
2037
+ /** The resolved 4-dp multiplier — persist this on the record. */
2038
+ multiplier: number;
2039
+ /** Apply the resolved multiplier to a raw provider cost, exact bigint nano-USD. */
2040
+ apply: (rawCostNanoUsd: bigint) => bigint;
2041
+ }
2042
+ /**
2043
+ * Resolves the effective markup multiplier for a metering context, delegating
2044
+ * to a host-supplied {@link IMarkupPolicy} when configured, or using the fixed
2045
+ * module-level `options.markup` value as the default.
2046
+ */
2047
+ declare class MarkupResolver {
2048
+ private readonly options;
2049
+ /**
2050
+ * @param options The resolved options carrying the markup (a 4-dp number or a policy).
2051
+ */
2052
+ constructor(options: Pick<ResolvedAiTokensOptions, 'markup'>);
2053
+ /**
2054
+ * Resolve the markup for one call. A static multiplier is returned as-is (it was
2055
+ * validated and rounded to 4 dp at init); a policy is invoked with the full
2056
+ * context, and its result is validated and rounded to 4 dp.
2057
+ *
2058
+ * @param context The scope/provider/model/operation/serviceTier/feature context.
2059
+ * @returns The resolved 4-dp multiplier and a bound applier.
2060
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when a policy throws or returns an invalid value.
2061
+ */
2062
+ resolve(context: MarkupContext): Promise<ResolvedMarkup>;
2063
+ /** Invoke a host policy, wrapping any failure as an invalid-config error. */
2064
+ private invokePolicy;
2065
+ /** Validate + round a policy's multiplier to 4 dp, or fail the call. */
2066
+ private validate;
2067
+ }
2068
+
2069
+ /**
2070
+ * @fileoverview `TelemetryEmitter` — emits OpenTelemetry GenAI (`gen_ai.*`) usage
2071
+ * and duration signals for every posted record through the pluggable
2072
+ * {@link ITelemetrySink} (spec §14.1). It imports NOTHING from `@opentelemetry/api`
2073
+ * — the sink port isolates that optional peer, so the host's sink owns the SDK.
2074
+ * NO CONTENT: only token counts, model, provider, operation, and service tier
2075
+ * become attributes — prompt/completion text never reaches telemetry. When no sink
2076
+ * is configured every call is a cheap no-op that allocates no attribute object
2077
+ * (the null guard runs before {@link buildGenAiAttributes}).
2078
+ * @layer server
2079
+ */
2080
+
2081
+ /** Emits GenAI telemetry through a sink, or does nothing when none is configured. */
2082
+ declare class TelemetryEmitter {
2083
+ private readonly sink;
2084
+ /** @param sink The host telemetry sink, or `null` to disable telemetry. */
2085
+ constructor(sink: ITelemetrySink | null);
2086
+ /**
2087
+ * Record a usage measurement for a posted record (no-op without a sink).
2088
+ *
2089
+ * @param record The posted usage record.
2090
+ */
2091
+ recordUsage(record: UsageRecord): void;
2092
+ /**
2093
+ * Record an operation duration for a posted record (no-op without a sink, or when
2094
+ * the sink does not implement `recordDuration`).
2095
+ *
2096
+ * @param record The posted usage record.
2097
+ * @param milliseconds The measured operation duration.
2098
+ */
2099
+ recordDuration(record: UsageRecord, milliseconds: number): void;
2100
+ }
2101
+
2102
+ /**
2103
+ * @fileoverview `ContentCapture` — the ONLY path prompt/completion text may enter
2104
+ * the library (spec §14.2/§14.3). It is OFF by default: unless `options.content` is
2105
+ * configured, `capture()` is a pure no-op and no text is touched. When enabled it
2106
+ * masks each text via `content.mask` (default identity), then writes it to the
2107
+ * opt-in, short-TTL {@link IContentStore} sidecar under its role. The immutable
2108
+ * ledger, events, telemetry, and logs NEVER receive text — a sidecar failure is
2109
+ * logged (without the text) and never breaks the metering path. `IContentStore.purge`
2110
+ * supports erasure independently of the ledger. Internal — not part of the public barrel.
2111
+ * @layer server
2112
+ */
2113
+
2114
+ /** The text a host explicitly hands the library for the sidecar (never sourced by the library). */
2115
+ interface CaptureContentInput {
2116
+ usageRecordId: string;
2117
+ tenantId: string;
2118
+ prompt?: string | undefined;
2119
+ completion?: string | undefined;
2120
+ }
2121
+ /**
2122
+ * The ONLY path through which prompt/completion text may enter the library.
2123
+ * Disabled by default; when enabled, masks and forwards text to the opt-in
2124
+ * {@link IContentStore} sidecar. A sidecar failure is always non-fatal. Internal.
2125
+ */
2126
+ declare class ContentCapture {
2127
+ private readonly options;
2128
+ private readonly logger;
2129
+ /** @param options The resolved content-sidecar settings (disabled by default). */
2130
+ constructor(options: ResolvedContentOptions);
2131
+ /**
2132
+ * Persist the masked prompt/completion to the content sidecar. A no-op when the
2133
+ * feature is disabled or no text was supplied. Failures are logged (never the
2134
+ * text) and never rethrown — content capture must not break metering.
2135
+ *
2136
+ * @param input The record id, tenant, and the host-supplied text.
2137
+ */
2138
+ capture(input: CaptureContentInput): Promise<void>;
2139
+ }
2140
+
2141
+ /**
2142
+ * @fileoverview `WalletService` — the prepaid-credit facade over `IWalletStore`
2143
+ * (spec §9.2). Wallets hold nano-USD balances as append-only entries
2144
+ * (grant/debit/refund/adjustment/expiry) with a materialized balance column for
2145
+ * atomic debits. `grant`/`debit`/`refund`/`adjust` each carry a per-wallet
2146
+ * idempotency key (replay-or-conflict, §15.2); `debit` is the race-safe
2147
+ * conditional path (§9.4) — the store reserves against the materialized balance,
2148
+ * sweeps expired grants, and records the grant burn-down allocation trail (§9.3) in
2149
+ * one atomic operation, so the service never does a check-then-write. `getBalance`
2150
+ * reports the materialized balance; grant/adjust are admin-plane mutations that the
2151
+ * host MUST restrict to privileged roles and that emit `ai_tokens.audit` (§14.4).
2152
+ * `'key'` scopes cannot own money (§9.1). All amounts are bigint nano-USD.
2153
+ * @layer server
2154
+ */
2155
+
2156
+ /** The resolved wallet settings the service consumes (the enabled half of the union). */
2157
+ type WalletServiceOptions = Extract<ResolvedWalletsOptions, {
2158
+ enabled: true;
2159
+ }>;
2160
+ /** A wallet balance in nano-USD plus its presentation credits. */
2161
+ interface WalletBalance {
2162
+ nanoUsd: bigint;
2163
+ credits: number;
2164
+ }
2165
+ /** Input to {@link WalletService.grant}. */
2166
+ interface GrantInput {
2167
+ amountNanoUsd: bigint;
2168
+ priority?: number;
2169
+ effectiveAt?: Date;
2170
+ expiresAt?: Date;
2171
+ idempotencyKey: string;
2172
+ reason: string;
2173
+ }
2174
+ /** Input to {@link WalletService.debit}. */
2175
+ interface DebitInput {
2176
+ amountNanoUsd: bigint;
2177
+ usageRecordId?: string;
2178
+ idempotencyKey: string;
2179
+ reason?: string;
2180
+ }
2181
+ /** Input to {@link WalletService.refund}. */
2182
+ interface RefundInput {
2183
+ amountNanoUsd: bigint;
2184
+ usageRecordId?: string;
2185
+ idempotencyKey: string;
2186
+ reason: string;
2187
+ }
2188
+ /** Input to {@link WalletService.adjust}. */
2189
+ interface AdjustInput {
2190
+ amountNanoUsd: bigint;
2191
+ idempotencyKey: string;
2192
+ reason: string;
2193
+ }
2194
+ /** Input to {@link WalletService.settleAdjustment}: the signed capture ±delta. */
2195
+ interface SettleAdjustmentInput {
2196
+ /** The signed adjustment amount (`!= 0`): `+` credits, `−` debits. */
2197
+ amountNanoUsd: bigint;
2198
+ usageRecordId?: string;
2199
+ idempotencyKey: string;
2200
+ reason: string;
2201
+ }
2202
+ /**
2203
+ * The event hooks `WalletService` fires; the module wires them to the dispatcher
2204
+ * (default no-op so the service has no dependency cycle on the event dispatcher).
2205
+ */
2206
+ interface WalletEventHooks {
2207
+ granted(ref: WalletRef, data: WalletGrantedEventData): void | Promise<void>;
2208
+ depleted(ref: WalletRef, data: WalletDepletedEventData): void | Promise<void>;
2209
+ audit(action: string, details: Record<string, unknown>): void | Promise<void>;
2210
+ }
2211
+ /**
2212
+ * Prepaid-credit facade over {@link IWalletStore}. Wallets hold nano-USD
2213
+ * balances as append-only entries (grant/debit/refund/adjust) with a
2214
+ * materialized balance column for atomic debits. See file overview.
2215
+ */
2216
+ declare class WalletService {
2217
+ private readonly store;
2218
+ private readonly options;
2219
+ private readonly events;
2220
+ /**
2221
+ * @param store The wallet persistence port.
2222
+ * @param options The resolved wallet settings (credit rate, overdraft).
2223
+ * @param events The event hooks; the module wires them to the dispatcher.
2224
+ */
2225
+ constructor(store: IWalletStore, options: WalletServiceOptions, events?: WalletEventHooks);
2226
+ /**
2227
+ * The current balance, excluding not-yet-effective and expired grants (§9.2).
2228
+ * Reads the materialized balance; expiry is applied lazily on debit, so a grant
2229
+ * that lapses purely with the passage of time is reflected at the next debit or
2230
+ * `reconcile` (the documented §9.3 lazy-materialization trade-off).
2231
+ *
2232
+ * @param ref The wallet owner (a `'tenant' | 'team' | 'user'`, never `'key'`).
2233
+ * @returns The nano-USD balance and its presentation credits.
2234
+ */
2235
+ getBalance(ref: WalletRef): Promise<WalletBalance>;
2236
+ /**
2237
+ * Grant credits (allowance, purchase, promo). Auto-creates the wallet on the
2238
+ * first grant. ADMIN PLANE (§14.4): the host MUST restrict this to privileged
2239
+ * roles. Emits `ai_tokens.wallet.granted` and `ai_tokens.audit`.
2240
+ *
2241
+ * @param ref The wallet owner.
2242
+ * @param input The grant amount (`> 0`), optional schedule, key, and reason.
2243
+ * @returns The appended grant entry.
2244
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` on a non-positive amount / `'key'` owner; `AI_TOKENS_IDEMPOTENCY_CONFLICT` on a key reuse with a different payload.
2245
+ */
2246
+ grant(ref: WalletRef, input: GrantInput): Promise<WalletEntry>;
2247
+ /**
2248
+ * Atomic conditional debit (§9.4). `usageRecordId` links usage-driven debits;
2249
+ * a non-usage debit (e.g. a voucher reservation) omits it and MUST carry a
2250
+ * `reason`. The store reserves against the materialized balance and records the
2251
+ * grant burn-down allocation in one atomic step — never a check-then-write.
2252
+ *
2253
+ * @param ref The wallet owner.
2254
+ * @param input The debit amount (`> 0`), optional `usageRecordId`, key, and reason.
2255
+ * @returns The appended debit entry.
2256
+ * @throws {AiTokensException} `AI_TOKENS_INSUFFICIENT_CREDITS` (402) when the balance would fall below `-overdraft`; `AI_TOKENS_INVALID_CONFIG` when neither `usageRecordId` nor `reason` is present, or the amount is not positive.
2257
+ */
2258
+ debit(ref: WalletRef, input: DebitInput): Promise<WalletEntry>;
2259
+ /**
2260
+ * Refund a previous debit (a plain `refund` credit entry). Never resurrects an
2261
+ * expired grant — the credit is unallocated balance (§9.3). Refunds are always
2262
+ * allowed (they can restore a negative balance).
2263
+ *
2264
+ * @param ref The wallet owner.
2265
+ * @param input The refund amount (`> 0`), optional `usageRecordId`, key, and reason.
2266
+ * @returns The appended refund entry.
2267
+ */
2268
+ refund(ref: WalletRef, input: RefundInput): Promise<WalletEntry>;
2269
+ /**
2270
+ * Signed manual correction (ADMIN PLANE — §14.4; the host MUST restrict this to
2271
+ * privileged roles). A positive adjustment auto-creates the wallet. Emits
2272
+ * `ai_tokens.audit`.
2273
+ *
2274
+ * @param ref The wallet owner.
2275
+ * @param input The signed amount (`!= 0`), key, and reason.
2276
+ * @returns The appended adjustment entry.
2277
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` on a zero amount / `'key'` owner.
2278
+ */
2279
+ adjust(ref: WalletRef, input: AdjustInput): Promise<WalletEntry>;
2280
+ /**
2281
+ * Apply an unconditional signed settlement adjustment — the capture ±delta vs a
2282
+ * hold (§11.2). Unlike {@link debit} it NEVER blocks on the balance (capture must
2283
+ * settle actuals even into overdraft) and unlike {@link adjust} it emits NO admin
2284
+ * audit (it is a system settlement, not a manual correction). Appends an
2285
+ * `adjustment` entry; a positive amount credits (refund of an over-reservation), a
2286
+ * negative amount debits (top-up when actuals exceeded the estimate).
2287
+ *
2288
+ * @param ref The wallet owner.
2289
+ * @param input The signed amount (`!= 0`), optional `usageRecordId`, key, and reason.
2290
+ * @returns The appended adjustment entry.
2291
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` on a zero amount / `'key'` owner.
2292
+ */
2293
+ settleAdjustment(ref: WalletRef, input: SettleAdjustmentInput): Promise<WalletEntry>;
2294
+ /**
2295
+ * Paginated entry history with type/date filters — the transaction listing a
2296
+ * usage meter renders.
2297
+ *
2298
+ * @param ref The wallet owner.
2299
+ * @param filter Optional type/date filters and pagination.
2300
+ * @returns A page of entries and the total match count.
2301
+ */
2302
+ getEntries(ref: WalletRef, filter?: WalletEntryFilter): Promise<WalletEntryPage>;
2303
+ /**
2304
+ * Recompute and repair the materialized balance from the entry ledger (§9.4).
2305
+ * ADMIN PLANE (§14.4): the host MUST restrict this to privileged roles. Emits
2306
+ * `ai_tokens.audit`.
2307
+ *
2308
+ * @param ref The wallet owner.
2309
+ * @returns The reconciled wallet.
2310
+ */
2311
+ reconcile(ref: WalletRef): Promise<WalletBalance>;
2312
+ /** Append a non-debit entry, mapping a store idempotency/missing-wallet signal to the catalog. */
2313
+ private append;
2314
+ /** Run the atomic conditional debit, mapping `null` to insufficient credits (§9.4). */
2315
+ private conditionalDebit;
2316
+ /** Map a store idempotency conflict / missing-wallet signal to the typed catalog error. */
2317
+ private mapStoreError;
2318
+ /** Reject a `'key'` owner at runtime (`'key'` scopes cannot own money, §9.1). */
2319
+ private assertOwner;
2320
+ /** Present a nano-USD balance as credits (presentation-only division). */
2321
+ private toCredits;
2322
+ /** Build the invalid-config exception with an actionable reason. */
2323
+ private invalid;
2324
+ }
2325
+
2326
+ /**
2327
+ * @fileoverview `BudgetService` — multi-dimension budget CRUD, enforcement, and
2328
+ * the read-side status query (spec §10). Budgets cap spend/tokens/operation-count
2329
+ * per scope per window; windows anchor to calendar UTC or a per-subject `anchorAt`
2330
+ * with month-end clamping (§10.1). Unlimited semantics are NORMATIVE (§10.2): no
2331
+ * budget row / an absent limit = unlimited, a present `0` = a hard block, negatives
2332
+ * are rejected at validation. Every matching budget across the scope hierarchy is
2333
+ * checked and consumes INDEPENDENTLY (§10.3). Consumption is race-safe: `'block'`
2334
+ * budgets go through the atomic §10.8 `conditionalConsume` (never a check-then-write)
2335
+ * and roll back on any partial failure; `'allow'`/`'throttle'` record spend and
2336
+ * signal. `reconcileWindow` recomputes a window from the ledger with the SAME §10.7
2337
+ * predicate. `upsertBudget`/`removeBudget`/`rotateWindow` are admin-plane mutations
2338
+ * the host MUST restrict to privileged roles and that emit `ai_tokens.audit` (§14.4).
2339
+ * All money is bigint nano-USD.
2340
+ * @layer server
2341
+ */
2342
+
2343
+ /** The resolved budget settings the service consumes (the enabled half of the union). */
2344
+ type BudgetServiceOptions = Extract<ResolvedBudgetsOptions, {
2345
+ enabled: true;
2346
+ }>;
2347
+ /** Input to {@link BudgetService.upsertBudget}: `softThresholds`/`policy` default from options. */
2348
+ type UpsertBudgetInput = Omit<Budget, 'id' | 'createdAt' | 'softThresholds' | 'policy'> & {
2349
+ id?: string;
2350
+ softThresholds?: number[];
2351
+ policy?: BudgetPolicy;
2352
+ };
2353
+ /** The event hooks `BudgetService` fires; the module wires them to the dispatcher. */
2354
+ interface BudgetEventHooks {
2355
+ thresholdCrossed(tenantId: string, scope: MeteringScope, data: BudgetThresholdCrossedEventData): void | Promise<void>;
2356
+ exceeded(tenantId: string, scope: MeteringScope, data: BudgetExceededEventData): void | Promise<void>;
2357
+ projectedExceeded(tenantId: string, scope: MeteringScope, data: BudgetProjectedExceededEventData): void | Promise<void>;
2358
+ audit(action: string, details: Record<string, unknown>): void | Promise<void>;
2359
+ }
2360
+ /**
2361
+ * Multi-dimension budget CRUD, enforcement, and read-side status queries.
2362
+ * Budgets cap spend/tokens/operation-count per scope per window; unlimited
2363
+ * semantics are NORMATIVE (no row = no budget; 0 = block all). See file overview.
2364
+ */
2365
+ declare class BudgetService {
2366
+ private readonly store;
2367
+ private readonly ledger;
2368
+ private readonly options;
2369
+ private readonly now;
2370
+ private readonly events;
2371
+ private readonly logger;
2372
+ /** Highest soft-threshold emitted per `(budgetId, windowStartISO)` — per-instance dedupe (§20). */
2373
+ private readonly notifiedThreshold;
2374
+ /** Windows that have already emitted a projected-exceeded event, per instance. */
2375
+ private readonly projected;
2376
+ /**
2377
+ * @param store The budget persistence port.
2378
+ * @param ledger The ledger service (reconcileWindow reads the source of truth).
2379
+ * @param options The resolved budget settings (policy, thresholds, throttle callback).
2380
+ * @param now The injected clock.
2381
+ * @param events The event hooks; the module wires them to the dispatcher.
2382
+ */
2383
+ constructor(store: IBudgetStore, ledger: LedgerService, options: BudgetServiceOptions, now?: () => Date, events?: BudgetEventHooks);
2384
+ /**
2385
+ * Create or replace a budget (ADMIN PLANE — §14.4; restrict to privileged roles).
2386
+ * Applies the §10.2 normative validation and emits `ai_tokens.audit`.
2387
+ *
2388
+ * @param input The budget definition (`softThresholds`/`policy` default from options).
2389
+ * @returns The stored budget.
2390
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` on a negative limit, no limit dimension, or a threshold outside `(0, 1]`.
2391
+ */
2392
+ upsertBudget(input: UpsertBudgetInput): Promise<Budget>;
2393
+ /**
2394
+ * Delete a budget (ADMIN PLANE — §14.4). Emits `ai_tokens.audit`.
2395
+ *
2396
+ * @param budgetId The budget id.
2397
+ * @param tenantId The owning tenant (audit context).
2398
+ */
2399
+ removeBudget(budgetId: string, tenantId: string): Promise<void>;
2400
+ /**
2401
+ * List budgets applicable to a scope (the exact scope plus tenant-wide budgets).
2402
+ * Defaults to the tenant scope when no scope is supplied.
2403
+ *
2404
+ * @param tenantId The owning tenant.
2405
+ * @param scope The scope to list for; defaults to tenant-wide.
2406
+ * @returns The applicable budgets.
2407
+ */
2408
+ list(tenantId: string, scope?: MeteringScope): Promise<Budget[]>;
2409
+ /**
2410
+ * Force a fresh window now (or at `newWindowStart`) and re-anchor the budget so
2411
+ * subsequent windows follow the new cycle (ADMIN PLANE — §14.4). Emits
2412
+ * `ai_tokens.audit`.
2413
+ *
2414
+ * @param budgetId The budget id.
2415
+ * @param newWindowStart The new window start; defaults to now.
2416
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when the budget does not exist.
2417
+ */
2418
+ rotateWindow(budgetId: string, newWindowStart?: Date): Promise<void>;
2419
+ /**
2420
+ * Consume every matching budget for a metered call (§10.3/§10.8). `'block'`
2421
+ * budgets use the atomic conditional consume and roll back all prior consumption
2422
+ * on a shortfall (throwing the dimension's typed error); `'allow'`/`'throttle'`
2423
+ * budgets record the spend and signal. Called by the hold/record enforcement paths.
2424
+ *
2425
+ * @param context The metering context (payer scope, feature — trusted input).
2426
+ * @param delta The spend to consume across the three dimensions.
2427
+ * @throws {AiTokensException} `AI_TOKENS_BUDGET_EXCEEDED` (402, cost) or `AI_TOKENS_QUOTA_EXCEEDED` (429, tokens/count) when a `'block'` budget is exhausted.
2428
+ */
2429
+ consume(context: MeteringContext, delta: BudgetDelta): Promise<void>;
2430
+ /**
2431
+ * Release a previously-consumed delta across every matching budget (capture
2432
+ * delta, hold release, reversal). Best-effort signed subtraction.
2433
+ *
2434
+ * @param context The metering context.
2435
+ * @param delta The spend to release.
2436
+ */
2437
+ release(context: MeteringContext, delta: BudgetDelta): Promise<void>;
2438
+ /**
2439
+ * Apply a SIGNED spend delta to every matching budget window WITHOUT enforcement
2440
+ * — the capture settlement ±delta (§11.2). A positive delta records extra spend
2441
+ * (actual above the hold estimate), a negative delta releases (actual below);
2442
+ * capture NEVER re-blocks, so this never throws a budget/quota error. Keeps the
2443
+ * live counter in sync (increment/decrement per dimension sign).
2444
+ *
2445
+ * @param context The metering context (payer scope, feature).
2446
+ * @param delta The signed spend delta across the three dimensions.
2447
+ */
2448
+ adjust(context: MeteringContext, delta: BudgetDelta): Promise<void>;
2449
+ /**
2450
+ * Unconditionally move a live counter by a signed capture ±delta (never re-blocks).
2451
+ * A positive move uses `incrIfBelow` against the int64 ceiling; a `false` result
2452
+ * means the increment would overflow int64 (or the backend treats the ceiling as a
2453
+ * hard cap), so the counter has DIVERGED from the authoritative DB window — it is
2454
+ * then resynced to `authoritative` (the post-adjust window spend) rather than left
2455
+ * holding a stale value the fast path would trust. A negative move decrements.
2456
+ */
2457
+ private adjustCounter;
2458
+ /** Discard a diverged counter and reseed it to the authoritative DB window spend (§10.8). */
2459
+ private resyncCounter;
2460
+ /**
2461
+ * Recompute a window's counters from the ledger using the §10.7 predicate — the
2462
+ * ledger is the reconcilable source of truth; the window row is a cache.
2463
+ *
2464
+ * @param budgetId The budget id.
2465
+ * @param windowStart A window start for the budget.
2466
+ */
2467
+ reconcileWindow(budgetId: string, windowStart: Date): Promise<void>;
2468
+ /**
2469
+ * The read-side "how much is left" query across every matching budget (§10.6) —
2470
+ * the usage-meter data source. Reads live window rows (documented freshness
2471
+ * trade-off; no counter read).
2472
+ *
2473
+ * @param tenantId The owning tenant.
2474
+ * @param scope The scope to report for.
2475
+ * @returns One {@link BudgetStatus} per matching budget.
2476
+ * @example
2477
+ * const budgets = await budgetService.status(tenantId, scope)
2478
+ * res.json(toJsonSafe(budgets)) // bigint fields become decimal strings (§15.5)
2479
+ */
2480
+ status(tenantId: string, scope: MeteringScope): Promise<BudgetStatus[]>;
2481
+ /** Apply consumption for one located window (throws through {@link blockExceeded} on a hard shortfall). */
2482
+ private applyConsumption;
2483
+ /**
2484
+ * The atomic §10.8 consume with the optional live-counter fast path and
2485
+ * fail-closed fallback. With a counter bound, each limited dimension is checked
2486
+ * cheaply via `incrIfBelow` FIRST (a reject touches no DB); on pass the DB
2487
+ * conditional consume remains authoritative and rolls the counter back on a DB
2488
+ * shortfall. A counter that is UNAVAILABLE falls back to the DB alone; if the DB
2489
+ * is also down, `failClosed` blocks (else it allows with a warning). Without a
2490
+ * counter it is the plain DB conditional consume.
2491
+ */
2492
+ private guardedConsume;
2493
+ /**
2494
+ * Try the live-counter fast path. On a hard reject it reports the FAILING dimension
2495
+ * (whose `incrIfBelow` returned `false`) so the block raises the correct §16 code
2496
+ * — a count/token quota is a 429, not the spend 402 the DB-window default would
2497
+ * infer when the counter is ahead of the window row. Otherwise it reports the
2498
+ * increments made and whether the counter was available (an outage falls back to DB).
2499
+ */
2500
+ private counterFastPath;
2501
+ /** Run the authoritative DB consume, rolling the counter back on a shortfall and failing closed on an outage. */
2502
+ private databaseConsume;
2503
+ /** Best-effort counter decrement (rollback); failures are logged, never thrown. */
2504
+ private decrCounters;
2505
+ /**
2506
+ * Emit `exceeded` and throw the dimension's typed error for a blocked budget. The
2507
+ * failing dimension is threaded from the counter fast-path reject when it decided
2508
+ * the block; the DB conditional-consume path passes none, so it defaults to the
2509
+ * over-limit dimension of the DB window spend — both routes map to the same §16 code.
2510
+ */
2511
+ private blockExceeded;
2512
+ /** Signal an over-limit `'allow'`/`'throttle'` budget (records spend, never throws). */
2513
+ private softOverLimit;
2514
+ /** Invoke the host throttle callback (or warn + allow when none is configured). */
2515
+ private throttle;
2516
+ /** Emit each soft threshold crossed by this delta, once per window (per-instance dedupe). */
2517
+ private signalThresholds;
2518
+ /** Emit one `threshold_crossed` event. */
2519
+ private emitThreshold;
2520
+ /** Emit `projected_exceeded` once per window when the burn rate projects crossing before reset. */
2521
+ private signalProjection;
2522
+ /** The matching, feature-relevant budgets located in the current window. */
2523
+ private matchingWindows;
2524
+ /** Undo every recorded consumption (signed DB subtraction + live-counter decrement). */
2525
+ private rollback;
2526
+ /** Load a budget by id or throw an invalid-config error. */
2527
+ private requireBudget;
2528
+ /** Validate the §10.2 limit rules: no negatives and at least one limit dimension. */
2529
+ private assertLimits;
2530
+ }
2531
+
2532
+ /**
2533
+ * @fileoverview `MeteringService` — the public metering facade (spec §11). It
2534
+ * carries the whole lifecycle: the post-hoc `record()` (observe-only, or
2535
+ * enforcing post-hoc with `enforce: true`), the pure `estimateCost()`, and the
2536
+ * auth-hold flow `hold()` → `capture()` / `release()` plus the `meter()` wrapper,
2537
+ * the orchestrated `reverse()`, and the combined `getStatus()`. Per the §11.2
2538
+ * side-effect matrix: a hold reserves (budget consume → wallet debit, each step
2539
+ * compensated), a capture settles the ±delta and is IDEMPOTENT, a release/reaper
2540
+ * restores in full and NEVER bills, and `isSystemCost` rows touch only the ledger
2541
+ * and events. Money is bigint nano-USD; no prompt/response text ever reaches the
2542
+ * ledger, events, or logs.
2543
+ * @layer server
2544
+ */
2545
+
2546
+ /** Input to {@link MeteringService.record}. */
2547
+ interface RecordInput {
2548
+ /** Raw provider usage OR an already-normalized {@link NormalizedUsage}. */
2549
+ usage: unknown;
2550
+ /** Preset carrying the normalizer + rating mode (alternative to `normalizer`). */
2551
+ preset?: ProviderPreset;
2552
+ /** A bare normalizer (alternative to `preset`). */
2553
+ normalizer?: UsageNormalizer;
2554
+ /** The per-call metering context (trusted input from the host's auth layer). */
2555
+ context: MeteringContext;
2556
+ /** When the call happened; defaults to now (backfills pass the original time). */
2557
+ occurredAt?: Date;
2558
+ /**
2559
+ * The ONLY path prompt/completion text enters the library (spec §14.2): masked +
2560
+ * TTL'd into the opt-in content sidecar, dropped entirely when the feature is off.
2561
+ * Never persisted to the ledger, events, telemetry, or logs.
2562
+ */
2563
+ content?: {
2564
+ prompt?: string;
2565
+ completion?: string;
2566
+ };
2567
+ }
2568
+ /** Input to {@link MeteringService.estimateCost}. */
2569
+ interface EstimateCostInput {
2570
+ provider: ProviderId;
2571
+ model: string;
2572
+ operation: AiOperation;
2573
+ serviceTier?: ServiceTier;
2574
+ inputTokens: number;
2575
+ maxOutputTokens: number;
2576
+ at?: Date;
2577
+ /** Lets a markup policy resolve; defaults to a neutral estimate scope. */
2578
+ scope?: MeteringScope;
2579
+ feature?: string;
2580
+ }
2581
+ /** The event hooks the lifecycle fires; the module wires them to the dispatcher (default no-op). */
2582
+ interface MeteringEventHooks {
2583
+ usageRecorded(record: UsageRecord): Promise<void>;
2584
+ priceMissing(record: UsageRecord): Promise<void>;
2585
+ holdReleased(record: UsageRecord, reason: string, expired: boolean): Promise<void>;
2586
+ usageReversed(original: UsageRecord, reversalRecordId: string, reason: string): Promise<void>;
2587
+ audit(action: string, details: Record<string, unknown>): Promise<void>;
2588
+ }
2589
+ /** The resolved-options subset the metering facade consumes. */
2590
+ type MeteringServiceOptions = Pick<ResolvedAiTokensOptions, 'ratingMode'> & {
2591
+ /** Absent in record-only setups that never place a hold. */
2592
+ holds?: ResolvedAiTokensOptions['holds'];
2593
+ /** Absent when the wallet feature is disabled. */
2594
+ wallets?: ResolvedAiTokensOptions['wallets'];
2595
+ };
2596
+ /**
2597
+ * The public metering facade — `record()`, `estimateCost()`, `hold()`,
2598
+ * `capture()`, `release()`, and the `meter()` wrapper. Wires the pricing,
2599
+ * ledger, wallet, and budget services together with the markup and event
2600
+ * layers. See file overview for the full lifecycle description.
2601
+ */
2602
+ declare class MeteringService {
2603
+ private readonly ledger;
2604
+ private readonly pricing;
2605
+ private readonly markup;
2606
+ private readonly options;
2607
+ private readonly events;
2608
+ private readonly now;
2609
+ private readonly telemetry;
2610
+ private readonly content;
2611
+ private readonly logger;
2612
+ private readonly effects;
2613
+ /** The wallet service, or `undefined` when the wallet feature is disabled. */
2614
+ private readonly wallets;
2615
+ /** The budget service, or `undefined` when the budget feature is disabled. */
2616
+ private readonly budgets;
2617
+ /** The wallet overdraft headroom (0 when wallets are disabled). */
2618
+ private readonly overdraftNanoUsd;
2619
+ /**
2620
+ * @param ledger The append-only ledger service.
2621
+ * @param pricing The pricing service (rate resolution).
2622
+ * @param markup The per-call markup resolver.
2623
+ * @param options The resolved options (rating mode, hold TTL, wallet overdraft).
2624
+ * @param events The event hooks; the module wires them to the dispatcher.
2625
+ * @param wallets The wallet service, when the wallet feature is enabled.
2626
+ * @param budgets The budget service, when the budget feature is enabled.
2627
+ * @param now The injected clock (hold expiry).
2628
+ * @param telemetry The GenAI telemetry emitter (no-op by default).
2629
+ */
2630
+ constructor(ledger: LedgerService, pricing: PricingService, markup: MarkupResolver, options: MeteringServiceOptions, events?: MeteringEventHooks, wallets?: WalletService | null, budgets?: BudgetService | null, now?: () => Date, telemetry?: TelemetryEmitter, content?: ContentCapture);
2631
+ /**
2632
+ * Post-hoc metering: normalize, rate, apply markup, append a `posted` record, and
2633
+ * emit `ai_tokens.usage.recorded`. Observe-only by default; `enforce: true` also
2634
+ * consumes budgets and debits the wallet AFTER the ledger write (§11.2 trade-off:
2635
+ * a call that already ran can still exceed a limit) — it requires the wallet or
2636
+ * budget feature.
2637
+ *
2638
+ * @param input The usage, its preset/normalizer, and the metering context.
2639
+ * @returns The posted usage record.
2640
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when `enforce: true` without wallets/budgets; normalize/rate errors; post-hoc budget/quota/credits errors.
2641
+ */
2642
+ record(input: RecordInput): Promise<UsageRecord>;
2643
+ /**
2644
+ * Pure pre-flight cost estimate — no ledger or event side effects.
2645
+ *
2646
+ * @param input The hypothetical call.
2647
+ * @returns The raw and billed nano-USD estimate.
2648
+ */
2649
+ estimateCost(input: EstimateCostInput): Promise<CostEstimate>;
2650
+ /**
2651
+ * Place an auth-hold: rate the estimate, apply markup, reserve budget + wallet
2652
+ * headroom (compensated on any step's failure), and write a `pending` ledger
2653
+ * record with a TTL (§2.2 step 1). A repeat with the same `idempotencyKey`
2654
+ * returns the EXISTING hold without re-reserving — this prioritizes not
2655
+ * double-consuming budget/wallet over parameter-conflict detection, so a replay
2656
+ * with a CHANGED estimate silently keeps the original reservation (unlike
2657
+ * `record()`, whose payload hash rejects a changed payload with 409); settlement
2658
+ * still corrects to actuals at capture. Concurrent holds sharing one key can both
2659
+ * pass the pre-check and reserve; the losing ledger insert compensates its
2660
+ * reservation (a brief over-reserve window under high contention).
2661
+ *
2662
+ * @param context The metering context (payer scope, feature).
2663
+ * @param estimate One of the three {@link HoldEstimate} variants.
2664
+ * @returns The plain, serializable {@link Hold}.
2665
+ * @throws {AiTokensException} `AI_TOKENS_BUDGET_EXCEEDED` / `_QUOTA_EXCEEDED` / `_INSUFFICIENT_CREDITS` on a shortfall (prior steps compensated).
2666
+ */
2667
+ hold(context: MeteringContext, estimate: HoldEstimate): Promise<Hold>;
2668
+ /**
2669
+ * Settle a hold with provider-reported actuals (§2.2 step 3). IDEMPOTENT: a repeat
2670
+ * on an already-settled hold returns the posted record unchanged. Applies the
2671
+ * ±delta between the actual and the reserved estimate to wallet/budget/counter.
2672
+ *
2673
+ * @param hold The hold to settle (revalidated against the store + caller tenant).
2674
+ * @param usage The raw provider usage or an already-normalized usage.
2675
+ * @param preset The preset normalizing the usage (or provider-reported mode).
2676
+ * @param content Optional prompt/completion text for the sidecar (§14.2) — the ONLY text path.
2677
+ * @returns The posted usage record.
2678
+ * @throws {AiTokensException} `AI_TOKENS_HOLD_NOT_FOUND` (cross-tenant/missing), `_HOLD_EXPIRED` (reaped), `_HOLD_ALREADY_SETTLED` (released).
2679
+ */
2680
+ capture(hold: Hold, usage: unknown, preset?: ProviderPreset, content?: {
2681
+ prompt?: string;
2682
+ completion?: string;
2683
+ }): Promise<UsageRecord>;
2684
+ /**
2685
+ * Void a hold: `pending → released`, restoring wallet/budget in full (§2.2 step
2686
+ * 4). NEVER bills. A no-op (with a warning) on an already-captured hold; a no-op
2687
+ * on an already-released hold. Multi-caller safe via the atomic transition claim.
2688
+ *
2689
+ * @param hold The hold to void (revalidated against the store + caller tenant).
2690
+ * @param reason The caller's stated reason (surfaced on `ai_tokens.hold.released`).
2691
+ * @throws {AiTokensException} `AI_TOKENS_HOLD_NOT_FOUND` on a cross-tenant/missing hold.
2692
+ */
2693
+ release(hold: Hold, reason: string): Promise<void>;
2694
+ /**
2695
+ * The most common entry point: hold → run `fn` → capture (release on error). With
2696
+ * no estimate it skips the hold and runs `record({ enforce: true })` post-hoc.
2697
+ *
2698
+ * @param fn The metered async function (the LLM call).
2699
+ * @param context The metering context (payer scope, feature, preset).
2700
+ * @param extract Pull the raw usage out of `fn`'s result.
2701
+ * @param estimate The optional hold estimate; omit for the post-hoc enforce path.
2702
+ * @returns The function result and its settled usage record.
2703
+ */
2704
+ meter<T>(fn: () => Promise<T>, context: MeteringContext, extract: (result: T) => unknown, estimate?: HoldEstimate): Promise<MeterResult<T>>;
2705
+ /**
2706
+ * Orchestrated compensation (§8.5 step 3): reverse the ledger record, then — for
2707
+ * an enforced, non-system record — refund the wallet and release the budget
2708
+ * (cost, tokens, and count). ADMIN PLANE (§14.4): the host MUST restrict this to
2709
+ * privileged roles; it emits `ai_tokens.usage.reversed` and `ai_tokens.audit`.
2710
+ * Cross-store effects are best-effort sequential with logged partial failures;
2711
+ * deterministic keys make retries safe.
2712
+ *
2713
+ * @param usageRecordId The posted record to reverse.
2714
+ * @param reason The caller's stated reason.
2715
+ * @returns The compensating record.
2716
+ * @throws {AiTokensException} `AI_TOKENS_IDEMPOTENCY_CONFLICT` when the record is missing or not `posted`.
2717
+ */
2718
+ reverse(usageRecordId: string, reason: string): Promise<UsageRecord>;
2719
+ /**
2720
+ * Combined wallet + budget status (§10.6) — the usage-meter data source. No side
2721
+ * effects. The wallet section is absent when wallets are disabled or the scope is
2722
+ * a `'key'` (keys spend their owner's wallet). `hasAccess` is false with
2723
+ * `blockedBy` set when the wallet or a hard budget is exhausted.
2724
+ *
2725
+ * @param tenantId The owning tenant.
2726
+ * @param scope The scope to report for.
2727
+ * @returns The combined access status.
2728
+ */
2729
+ getStatus(tenantId: string, scope: MeteringScope): Promise<AccessStatus>;
2730
+ /**
2731
+ * Restore a reclaimed hold and announce it — the shared code path for `release()`
2732
+ * and the hold reaper (§8.3). The caller has already won the `pending → released`
2733
+ * transition claim.
2734
+ *
2735
+ * @param record The just-released record (carries the reserved amounts).
2736
+ * @param reason The stated reason.
2737
+ * @param expired True when reclaimed by the reaper (vs an explicit release).
2738
+ */
2739
+ restoreReleasedHold(record: UsageRecord, reason: string, expired: boolean): Promise<void>;
2740
+ /** Reserve the pending ledger row, compensating the reservation if the insert fails. */
2741
+ private appendPending;
2742
+ /** Forward host-supplied prompt/completion text to the opt-in content sidecar (§14.2). */
2743
+ private captureContent;
2744
+ /** Build the {@link Hold} view of a pending (or replayed) ledger record. */
2745
+ private holdOf;
2746
+ /** Load a hold's record and reject a cross-tenant/scope or missing hold as HOLD_NOT_FOUND (§14.4). */
2747
+ private loadHoldRecord;
2748
+ /** Dispatch a non-pending hold record: posted → idempotent return; released → expired/settled. */
2749
+ private settledOrConflict;
2750
+ /** Reload a record by id, or fail HOLD_NOT_FOUND when it vanished mid-capture. */
2751
+ private reload;
2752
+ /**
2753
+ * Normalize the usage handed to `capture()`. A {@link StreamUsageCollector} is
2754
+ * finalized here; when it fell back to a tokenizer count with no prompt tokens
2755
+ * (input `0`), the hold's estimated tokens supply the aborted-stream input
2756
+ * fallback (spec §5.6 order: collector prompt count → hold estimate → 0).
2757
+ */
2758
+ private finalizeCaptureUsage;
2759
+ /** Build the settlement patch for a `pending → posted` transition. */
2760
+ private settlementPatch;
2761
+ /** Build the wallet section of an {@link AccessStatus}, or `undefined` when wallets are off / scope is a key. */
2762
+ private walletSection;
2763
+ /** Resolve the normalizer chain and produce the normalized usage. */
2764
+ private resolveNormalizedUsage;
2765
+ /** Resolve markup for a settled/pending call from the normalized identity. */
2766
+ private resolveMarkup;
2767
+ /** Run a normalizer, wrapping a plain failure as a malformed-usage error. */
2768
+ private normalize;
2769
+ /** Rate a call in the resolved mode; a non-strict rate miss yields zero cost + `priceMissing`. */
2770
+ private rate;
2771
+ }
2772
+
2773
+ /**
2774
+ * @fileoverview `UsageReportService` — real aggregation over the typed ledger
2775
+ * (spec §13). `summarize()` groups `SUM`s across the twelve report dimensions
2776
+ * (delegating to the store's optional SQL `summarize`, else a documented
2777
+ * in-memory fallback capped at `reporting.maxExportRows`) and computes the
2778
+ * cache-savings figure per group from each record's effective price. `export()`
2779
+ * streams the full §13.2 field set as CSV or line-delimited JSON with every bigint
2780
+ * rendered as a decimal string (§15.5) and, for a non-USD presentation currency,
2781
+ * additional `fx`-converted columns (§7.4, presentation-only). Every export emits
2782
+ * an `ai_tokens.audit` event — it is an ADMIN-PLANE surface the host MUST restrict
2783
+ * to privileged roles (§14.4). No prompt/completion text is ever read or emitted.
2784
+ * @layer server
2785
+ */
2786
+
2787
+ /** The resolved-options subset the report service consumes. */
2788
+ interface UsageReportOptions {
2789
+ currency: string;
2790
+ fx?: ((date: Date, currency: string) => Promise<bigint> | bigint) | undefined;
2791
+ reporting: {
2792
+ maxExportRows: number;
2793
+ };
2794
+ }
2795
+ /** The `summarize()` input: a report filter plus the group-by dimensions. */
2796
+ type SummarizeInput = ReportFilter & {
2797
+ groupBy: ReportGroupBy[];
2798
+ };
2799
+ /** The export serialization format. */
2800
+ type ReportExportFormat = 'csv' | 'json';
2801
+ /** The audit hook the module wires to the event dispatcher (default no-op). */
2802
+ type ReportAuditHook = (action: string, details: Record<string, unknown>) => void;
2803
+ /**
2804
+ * Read-side reporting: per-scope cost summaries with currency conversion, CSV
2805
+ * and JSON export, paginated history, and per-model analytics. All money is
2806
+ * presented in nano-USD internally; `formatNanoUsd` handles display. See file overview.
2807
+ */
2808
+ declare class UsageReportService {
2809
+ private readonly store;
2810
+ private readonly pricing;
2811
+ private readonly options;
2812
+ private readonly audit;
2813
+ /**
2814
+ * @param store The ledger store port (query + optional SQL summarize).
2815
+ * @param pricing The pricing store port (effective rate for cache savings).
2816
+ * @param options The resolved currency/fx/reporting settings.
2817
+ * @param audit The audit hook; the module wires it to the dispatcher.
2818
+ */
2819
+ constructor(store: ILedgerStore, pricing: IPricingStore, options: UsageReportOptions, audit?: ReportAuditHook);
2820
+ /**
2821
+ * Aggregate `SUM … GROUP BY` across the report dimensions. Delegates to the
2822
+ * store's SQL `summarize` when present, else aggregates in memory. The in-memory
2823
+ * fallback REFUSES to emit partial totals: like `export()` it throws when the
2824
+ * filter matches more than `reporting.maxExportRows` rows (paginate instead) — a
2825
+ * silent truncation would hand callers incorrect aggregates. An empty `groupBy`
2826
+ * yields one grand-total row.
2827
+ *
2828
+ * @param input The report filter plus the group-by dimensions.
2829
+ * @returns One {@link UsageSummary} per group.
2830
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when the matched rows exceed `reporting.maxExportRows`.
2831
+ */
2832
+ summarize(input: SummarizeInput): Promise<UsageSummary[]>;
2833
+ /**
2834
+ * Stream the §13.2 export field set as CSV or line-delimited JSON, bigints as
2835
+ * decimal strings, plus fx-converted columns for a non-USD currency. Emits an
2836
+ * `ai_tokens.audit` event. ADMIN PLANE (§14.4): restrict to privileged roles.
2837
+ *
2838
+ * @param filter The report filter.
2839
+ * @param format `'csv'` or `'json'` (ndjson).
2840
+ * @returns A readable stream of the export.
2841
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when the result exceeds `reporting.maxExportRows`.
2842
+ */
2843
+ export(filter: ReportFilter, format: ReportExportFormat): Promise<Readable>;
2844
+ /**
2845
+ * Refuse a partial result: throw when a `maxExportRows + 1`-bounded query matched
2846
+ * more rows than `reporting.maxExportRows`, signaling the caller to paginate. Both
2847
+ * `summarize()` and `export()` share this guard so neither ever silently truncates.
2848
+ *
2849
+ * @param rowCount The number of rows the `(max + 1)`-bounded query returned.
2850
+ * @param operation The operation name for the error reason (`summarize`/`export`).
2851
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when `rowCount` exceeds the limit.
2852
+ */
2853
+ private assertWithinMaxRows;
2854
+ /** Yield the serialized rows (CSV header + rows, or ndjson lines). */
2855
+ private serialize;
2856
+ /** The ordered header names for the export (with fx columns when converting). */
2857
+ private headers;
2858
+ /** Build one export row as ordered `[header, cell]` pairs (all cells strings). */
2859
+ private rowOf;
2860
+ /** The nano-USD cache savings for a record: `cacheReadTokens × (inputRate − cacheReadRate)`. */
2861
+ private cacheSavings;
2862
+ }
2863
+
2864
+ /**
2865
+ * @fileoverview `toJsonSafe` — the bigint-at-the-JSON-boundary helper (spec §15.5).
2866
+ * `bigint` does not survive `JSON.stringify`, so any library value returned from a
2867
+ * controller or shipped through an out-of-process `IEventSink` is first deep-cloned
2868
+ * with every `bigint` rendered as an exact decimal string (e.g. `5000000n` →
2869
+ * `"5000000"`). Token counts stay plain `number`; `Date` values are cloned and
2870
+ * serialize to ISO strings via the standard JSON rules. In-process delivery keeps
2871
+ * `bigint` intact — this helper is only for the JSON boundary.
2872
+ * @layer server
2873
+ */
2874
+ /** The result of {@link toJsonSafe}: every `bigint` replaced by a decimal string. */
2875
+ type JsonSafe<T> = T extends bigint ? string : T extends Date ? Date : T extends readonly (infer U)[] ? JsonSafe<U>[] : T extends object ? {
2876
+ [K in keyof T]: JsonSafe<T[K]>;
2877
+ } : T;
2878
+ /**
2879
+ * Deep-clone a value into a JSON-safe form, serializing every `bigint` as a
2880
+ * decimal string so the result survives `JSON.stringify` losslessly.
2881
+ *
2882
+ * @param value The value to make JSON-safe (e.g. a `UsageRecord` or event envelope).
2883
+ * @returns A structural clone with `bigint` fields rendered as decimal strings.
2884
+ * @example
2885
+ * toJsonSafe({ billedCostNanoUsd: 5_000_000n }) // { billedCostNanoUsd: '5000000' }
2886
+ */
2887
+ declare function toJsonSafe<T>(value: T): JsonSafe<T>;
2888
+
2889
+ /**
2890
+ * @fileoverview `StreamUsageCollector` — abort-safe usage capture for streamed
2891
+ * responses (spec §5.6). Provider `usage` arrives only in the final chunk (OpenAI:
2892
+ * a trailing chunk with `usage` and empty `choices`; Anthropic: cumulative in
2893
+ * `message_delta`, finalized at `message_stop`), and an aborted stream reports
2894
+ * all-zero usage even though tokens were consumed. The collector accumulates the
2895
+ * output text as chunks flow and, at `finalize()`, prefers the provider-final usage;
2896
+ * on an abort it falls back to a tokenizer count of the accumulated output so the
2897
+ * call is still billed for what it produced. `push()` NEVER throws — a malformed
2898
+ * chunk is skipped and counted. Only token counts and (optionally) the prompt cross
2899
+ * this boundary; prompt/response text stays here for counting and is never persisted.
2900
+ * @layer server
2901
+ */
2902
+
2903
+ /** Construction options for {@link StreamUsageCollector}. */
2904
+ interface StreamUsageCollectorOptions {
2905
+ provider: ProviderId;
2906
+ model: string;
2907
+ /** The rated operation; defaults to `'chat'`. */
2908
+ operation?: AiOperation;
2909
+ /** How to parse the provider-final usage chunk; defaults to the provider's built-in normalizer. */
2910
+ preset?: ProviderPreset;
2911
+ /** Fallback token counter for aborted streams; defaults to the module tokenizer. */
2912
+ tokenizer?: ITokenizer;
2913
+ /** Custom output-text extractor for providers beyond the OpenAI/Anthropic built-ins. */
2914
+ outputText?: (chunk: Record<string, unknown>) => string | undefined;
2915
+ }
2916
+ /**
2917
+ * Accumulates usage chunks from a streamed provider response and finalises
2918
+ * into a {@link NormalizedUsage} object. Prefer the provider's own final
2919
+ * usage block; fall back to tokenizer-estimated output tokens when the
2920
+ * stream is aborted before the final chunk arrives.
2921
+ *
2922
+ * @example
2923
+ * const collector = new StreamUsageCollector({ provider: 'openai-chat', model: 'gpt-4o' })
2924
+ * // call collector.push(chunk) for each SSE chunk
2925
+ * const usage = await collector.finalize()
2926
+ */
2927
+ declare class StreamUsageCollector {
2928
+ private readonly opts;
2929
+ private readonly operation;
2930
+ private outputText;
2931
+ /** The provider-final response object to normalize, once seen. */
2932
+ private finalResponse;
2933
+ /** Anthropic streaming state accumulated across events (seeded from the constructed identity). */
2934
+ private anthropicModel;
2935
+ private anthropicBaseUsage;
2936
+ private anthropicOutputTokens;
2937
+ private promptTokens;
2938
+ private promptText;
2939
+ private finalized;
2940
+ private malformed;
2941
+ /** True after `finalize()` when the tokenizer fallback (not provider-final usage) was used. */
2942
+ usedFallback: boolean;
2943
+ /** @param opts The provider/model + optional preset/tokenizer/extractor. */
2944
+ constructor(opts: StreamUsageCollectorOptions);
2945
+ /** How many chunks were skipped as malformed (debug/observability). */
2946
+ get malformedCount(): number;
2947
+ /** Provide the prompt text so an aborted stream can count input tokens via the tokenizer. */
2948
+ setPromptText(text: string): void;
2949
+ /** Provide a pre-counted prompt token total (takes precedence over `setPromptText`). */
2950
+ setPromptTokens(count: number): void;
2951
+ /**
2952
+ * Feed one stream chunk: accumulate output text and watch for the provider-final
2953
+ * usage. Never throws — a malformed chunk is skipped and counted.
2954
+ *
2955
+ * @param chunk One raw provider stream chunk/event.
2956
+ */
2957
+ push(chunk: unknown): void;
2958
+ /**
2959
+ * Resolve the final usage: provider-final when seen (normalized), else a tokenizer
2960
+ * count of the accumulated output (input tokens from the prompt count when
2961
+ * provided, else 0 — the hold estimate is applied by `capture()`), else a
2962
+ * missing-usage error. Async to support async tokenizers (§14.2.1). Single-use.
2963
+ *
2964
+ * @returns The finalized normalized usage.
2965
+ * @throws {AiTokensException} `AI_TOKENS_STREAM_USAGE_MISSING` on an abort with no tokenizer; a re-use error on a second call.
2966
+ */
2967
+ finalize(): Promise<NormalizedUsage>;
2968
+ /** Accumulate an OpenAI-shaped chunk and capture its trailing usage. */
2969
+ private pushOpenAiLike;
2970
+ /** Accumulate an Anthropic-shaped event and reconstruct its final usage at message_stop. */
2971
+ private pushAnthropic;
2972
+ /** Normalize a captured provider-final response via the preset or the provider default. */
2973
+ private normalizeFinal;
2974
+ /** Build the tokenizer-counted fallback usage for an aborted stream. */
2975
+ private fallbackUsage;
2976
+ }
2977
+
2978
+ /**
2979
+ * @fileoverview The provider presets (spec §4.3). Each preset pairs a normalizer
2980
+ * with the right provider id and rating mode; a preset is passed in
2981
+ * `MeteringContext.preset`. `azureOpenai` reuses the OpenAI Chat normalizer but
2982
+ * requires the caller to pass `baseModel` (§6.6); `vercelAiSdk` defaults its
2983
+ * provider to `'openai'` and expects a per-call override; `openrouter` rates in
2984
+ * `'provider-reported'` mode.
2985
+ * @layer server
2986
+ */
2987
+
2988
+ /** The built-in provider presets, plus the `openaiCompatible(id)` factory. */
2989
+ declare const providerPresets: {
2990
+ openaiChat: ProviderPreset;
2991
+ openaiResponses: ProviderPreset;
2992
+ azureOpenai: ProviderPreset;
2993
+ anthropic: ProviderPreset;
2994
+ gemini: ProviderPreset;
2995
+ vertex: ProviderPreset;
2996
+ bedrock: ProviderPreset;
2997
+ mistral: ProviderPreset;
2998
+ vercelAiSdk: ProviderPreset;
2999
+ openrouter: ProviderPreset;
3000
+ /** A custom OpenAI-compatible provider (DeepSeek, xAI, Groq, …). */
3001
+ openaiCompatible: (id: string) => ProviderPreset;
3002
+ };
3003
+
3004
+ /**
3005
+ * @fileoverview `BudgetGuard` — the CanActivate enforcement gate (spec §11.3). It
3006
+ * resolves the caller's metering context via the host-configured `scopeResolver`
3007
+ * (TRUSTED INPUT — the host's VERIFIED auth context only, never client body/query,
3008
+ * §14.4), merges the decorator config (feature precedence `@RequireBudget` >
3009
+ * `@Meter` > `@AiFeature`), and checks budget status. If any matching HARD (`block`)
3010
+ * budget is exhausted it throws `AI_TOKENS_BUDGET_EXCEEDED` (402, spend) or
3011
+ * `AI_TOKENS_QUOTA_EXCEEDED` (429, tokens/count) BEFORE the handler runs; otherwise
3012
+ * it enriches the request with `request.aiTokens = { status, context }` and returns
3013
+ * true. This is CHECK-ONLY — it performs NO consumption; the §10.8 atomic consume
3014
+ * still protects the actual charge at record/capture time (the documented gate
3015
+ * race). A missing `scopeResolver` fails fast at guard construction. When
3016
+ * `@RequireBudget.estimate` is present the guard additionally places a hold and
3017
+ * attaches it to `request.aiTokens.hold` so the `MeteringInterceptor` settles it
3018
+ * with the handler's actual usage (spec §11.3).
3019
+ * @layer server
3020
+ */
3021
+
3022
+ /** The request enrichment the guard attaches (the `AIGenerationGuard` parity contract). */
3023
+ interface RequestAiTokens {
3024
+ status: BudgetStatus[];
3025
+ context: MeteringContext;
3026
+ /** Present when `@RequireBudget.estimate` placed a hold; the interceptor settles it. */
3027
+ hold?: Hold;
3028
+ }
3029
+ /**
3030
+ * NestJS guard that checks budget status before each request and enriches
3031
+ * `request.aiTokens` with the scope's current usage. Optionally places a
3032
+ * spend hold when `@RequireBudget.estimate` is configured (see file overview).
3033
+ */
3034
+ declare class BudgetGuard implements CanActivate {
3035
+ private readonly budgets;
3036
+ private readonly metering;
3037
+ private readonly reflector;
3038
+ private readonly scopeResolver;
3039
+ /**
3040
+ * @param budgets The budget service (status check).
3041
+ * @param metering The metering facade (hold placement in `@RequireBudget.estimate` mode).
3042
+ * @param reflector The NestJS reflector (decorator metadata).
3043
+ * @param options The resolved options carrying the required `scopeResolver`.
3044
+ * @throws {AiTokensException} `AI_TOKENS_INVALID_CONFIG` when `scopeResolver` is absent (fail-fast at init).
3045
+ */
3046
+ constructor(budgets: BudgetService, metering: MeteringService, reflector: Reflector, options: Pick<ResolvedAiTokensOptions, 'scopeResolver'>);
3047
+ /**
3048
+ * Resolve the scope, merge decorator config, and block on any exhausted hard
3049
+ * budget; otherwise enrich the request and allow.
3050
+ *
3051
+ * @param executionContext The request execution context.
3052
+ * @returns `true` when the request may proceed.
3053
+ * @throws {AiTokensException} `AI_TOKENS_BUDGET_EXCEEDED` / `AI_TOKENS_QUOTA_EXCEEDED` when a hard budget is exhausted (or an estimate shortfall from the placed hold).
3054
+ */
3055
+ canActivate(executionContext: ExecutionContext): Promise<boolean>;
3056
+ }
3057
+
3058
+ /**
3059
+ * @fileoverview `MeteringInterceptor` — the declarative capture half of the
3060
+ * controller path (spec §11.3/§11.4). After the handler resolves it extracts the
3061
+ * raw usage from the return value (via `@Meter.extract`, default `result.usage`),
3062
+ * then either settles the guard's hold (`capture`) or records enforcing post-hoc
3063
+ * (`record({ enforce: true })`). On a handler error with a hold present it releases
3064
+ * the hold and rethrows the ORIGINAL error — never swallowing it. With
3065
+ * `exposeHeaders` it sets the three `x-ai-tokens-*` response headers as DECIMAL
3066
+ * STRINGS (bigint never crosses the HTTP boundary raw, §15.5). Handlers without a
3067
+ * `@Meter` are passed through untouched. `scopeResolver` is TRUSTED INPUT — the
3068
+ * host's verified auth context, never client body/query (§14.4).
3069
+ * @layer server
3070
+ */
3071
+
3072
+ /** The resolved-options subset the interceptor consumes. */
3073
+ type MeteringInterceptorOptions = Pick<ResolvedAiTokensOptions, 'scopeResolver'>;
3074
+ /**
3075
+ * NestJS interceptor that captures the handler's actual usage (from the return
3076
+ * value) and either settles the guard's hold or records a post-hoc enforcing
3077
+ * charge. Reads `@Meter` metadata set by the {@link Meter} decorator.
3078
+ */
3079
+ declare class MeteringInterceptor implements NestInterceptor {
3080
+ private readonly reflector;
3081
+ private readonly metering;
3082
+ private readonly logger;
3083
+ private readonly scopeResolver;
3084
+ /**
3085
+ * @param reflector The NestJS reflector (`@Meter` metadata).
3086
+ * @param metering The metering facade (capture/record).
3087
+ * @param options The resolved options (fallback `scopeResolver` when no guard ran).
3088
+ */
3089
+ constructor(reflector: Reflector, metering: MeteringService, options: MeteringInterceptorOptions);
3090
+ /**
3091
+ * Capture (or record) the handler's usage after it resolves; release the guard's
3092
+ * hold on a handler error.
3093
+ *
3094
+ * @param executionContext The request execution context.
3095
+ * @param next The downstream handler.
3096
+ * @returns The handler's result stream, unchanged in value.
3097
+ */
3098
+ intercept(executionContext: ExecutionContext, next: CallHandler): Observable<unknown>;
3099
+ /** Settle the handler's usage and set the cost headers; returns the untouched result. */
3100
+ private settle;
3101
+ /** Capture the guard's hold when present, else record enforcing post-hoc. */
3102
+ private capture;
3103
+ /** The metering context: the guard's enrichment, else the host `scopeResolver`. */
3104
+ private resolveContext;
3105
+ /** Release the guard's hold (if any) and rethrow the ORIGINAL handler error. */
3106
+ private onError;
3107
+ /** Set the three `x-ai-tokens-*` headers as decimal strings via the HTTP adapter. */
3108
+ private setHeaders;
3109
+ }
3110
+
3111
+ /**
3112
+ * @fileoverview The controller-level metering/enforcement decorators (spec §11.4).
3113
+ * `@Meter` marks a handler for the `MeteringInterceptor` and is metadata-only
3114
+ * here; `@RequireBudget` marks it for the `BudgetGuard`; `@AiFeature`
3115
+ * is a lightweight feature tag. Each stores its config under an exported symbol key
3116
+ * that the guard/interceptor read via the NestJS `Reflector`. Feature precedence is
3117
+ * `@RequireBudget.feature` > `@Meter.feature` > `@AiFeature` (§11.4). These are pure
3118
+ * metadata; they never touch a wallet, budget, or the request.
3119
+ * @layer server
3120
+ */
3121
+
3122
+ /** Reflector key for {@link Meter} metadata. */
3123
+ declare const METER_METADATA = "bymax:ai-tokens:meter";
3124
+ /** Reflector key for {@link RequireBudget} metadata. */
3125
+ declare const REQUIRE_BUDGET_METADATA = "bymax:ai-tokens:require-budget";
3126
+ /** Reflector key for {@link AiFeature} metadata. */
3127
+ declare const AI_FEATURE_METADATA = "bymax:ai-tokens:ai-feature";
3128
+ /** `@Meter` configuration — consumed by the `MeteringInterceptor`. */
3129
+ interface MeterConfig {
3130
+ /** The logical operation, e.g. `'workout.generate'`. */
3131
+ feature: string;
3132
+ /** Which scope type from the resolved context to charge (default `'user'`). */
3133
+ scope?: ScopeType;
3134
+ /** The normalizer + rating mode for the handler's return value. */
3135
+ preset?: ProviderPreset;
3136
+ /** Pull the raw usage out of the return value (default: `result.usage`). */
3137
+ extract?: (result: unknown) => unknown;
3138
+ /** Set `x-ai-tokens-*` response headers. */
3139
+ exposeHeaders?: boolean;
3140
+ /** Platform-absorbed cost — never consumes wallet/budget. */
3141
+ isSystemCost?: boolean;
3142
+ /** Cost-attribution labels. */
3143
+ tags?: string[];
3144
+ }
3145
+ /** `@RequireBudget` configuration — consumed by the `BudgetGuard`. */
3146
+ interface RequireBudgetConfig {
3147
+ /** Which scope type to enforce (default: the resolved context's scope). */
3148
+ scope?: ScopeType;
3149
+ /** Budget feature-filter matching; defaults to `@Meter`'s feature. */
3150
+ feature?: string;
3151
+ /** A static estimate → the guard places a hold the interceptor settles. */
3152
+ estimate?: HoldEstimate;
3153
+ }
3154
+ /**
3155
+ * Mark a handler for metering by the `MeteringInterceptor`. Metadata-only.
3156
+ *
3157
+ * @param config The metering configuration.
3158
+ * @returns The metadata decorator.
3159
+ */
3160
+ declare function Meter(config: MeterConfig): CustomDecorator;
3161
+ /**
3162
+ * Require budget headroom on a handler — the {@link BudgetGuard} blocks the request
3163
+ * pre-handler when a matching hard budget is exhausted.
3164
+ *
3165
+ * @param config The budget-enforcement configuration.
3166
+ * @returns The metadata decorator.
3167
+ */
3168
+ declare function RequireBudget(config?: RequireBudgetConfig): CustomDecorator;
3169
+ /**
3170
+ * Tag a handler with a feature name (`@Meter.feature` wins when both are present).
3171
+ *
3172
+ * @param name The feature name.
3173
+ * @returns The metadata decorator.
3174
+ */
3175
+ declare function AiFeature(name: string): CustomDecorator;
3176
+
3177
+ /**
3178
+ * @fileoverview The typed exception the library throws. Extends NestJS
3179
+ * `HttpException` so hosts get a stable `{ error: { code, message, details? } }`
3180
+ * body and the right HTTP status by default (spec §16.1). The public surface is
3181
+ * this class plus `AI_TOKENS_ERROR_CODES` and `AiTokensErrorResponse` (both from
3182
+ * `./shared`); the message/status maps are internal.
3183
+ * @layer server
3184
+ */
3185
+
3186
+ /**
3187
+ * A typed library error. The status defaults to the code's mapped HTTP status
3188
+ * (spec §16.2) and the body is the canonical `AiTokensErrorResponse` shape.
3189
+ *
3190
+ * @example
3191
+ * throw new AiTokensException('AI_TOKENS_BUDGET_EXCEEDED', undefined, { budgetId })
3192
+ */
3193
+ declare class AiTokensException extends HttpException {
3194
+ /**
3195
+ * @param code The error code.
3196
+ * @param statusCode The HTTP status; defaults to the code's mapped status.
3197
+ * @param details Optional structured context (never includes prompt/response text).
3198
+ */
3199
+ constructor(code: AiTokensErrorCode, statusCode?: HttpStatus, details?: Record<string, unknown>);
3200
+ }
3201
+
3202
+ export { AI_FEATURE_METADATA, AI_OPERATIONS, AI_TOKENS_ERROR_CODES, AMOUNT_FIELDS, type AccessStatus, type AdjustInput, AiFeature, type AiOperation, type AiTokensErrorCode, type AiTokensErrorResponse, type AiTokensEvent, type AiTokensEventType, AiTokensException, type AuditEventData, BYMAX_AI_TOKENS_BUDGET_COUNTER, BYMAX_AI_TOKENS_BUDGET_STORE, BYMAX_AI_TOKENS_CONTENT_STORE, BYMAX_AI_TOKENS_EVENT_SINK, BYMAX_AI_TOKENS_LEDGER_STORE, BYMAX_AI_TOKENS_LOGGER, BYMAX_AI_TOKENS_OPTIONS, BYMAX_AI_TOKENS_PRICING_STORE, BYMAX_AI_TOKENS_TELEMETRY, BYMAX_AI_TOKENS_TOKENIZER, BYMAX_AI_TOKENS_WALLET_STORE, type Budget, type BudgetDelta, type BudgetDimensionSnapshot, type BudgetExceededEventData, BudgetGuard, type BudgetLimits, type BudgetPolicy, type BudgetProjectedExceededEventData, BudgetService, type BudgetServiceOptions, type BudgetStatus, type BudgetThresholdCrossedEventData, type BudgetWindowKind, type BudgetWindowSpend, BymaxAiTokensModule, type BymaxAiTokensModuleAsyncOptions, type BymaxAiTokensModuleOptions, type BymaxAiTokensModuleOptionsFactory, type CostBreakdown, type CostEstimate, type DebitInput, type EstimateCostInput, type FormatNanoUsdOptions, type GrantInput, type Hold, type HoldEstimate, type HoldReleasedEventData, type IAiTokensStore, type IBudgetCounterStore, type IBudgetStore, type IContentStore, type IEventSink, type ILedgerStore, type IMarkupPolicy, type IPricingStore, type ITelemetrySink, type ITokenizer, type IWalletStore, type JsonSafe, type KnownProviderId, type LedgerAppendInput, type LedgerCostSummary, type LedgerFilter, LedgerService, METER_METADATA, Meter, type MeterConfig, type MeterResult, type MeteringContext, MeteringInterceptor, type MeteringScope, MeteringService, type NewPriceVersion, type NewUsageRecord, type NewWalletEntry, type NormalizedUsage, type OpenGrant, PROVIDER_IDS, type PriceMissingEventData, type PriceVersion, type PricedModel, PricingService, type ProviderId, type ProviderPreset, RELEASE_FIELDS, REQUIRE_BUDGET_METADATA, REVERSAL_LINKAGE_FIELD, type RatingMode, type RecordInput, type RefundInput, type ReportExportFormat, type ReportFilter, type ReportGroupBy, type RequestAiTokens, RequireBudget, type RequireBudgetConfig, type ResolveRateInput, SERVICE_TIERS, SETTLEMENT_FIELDS, SETTLEMENT_PATCH_KEYS, type ScopeType, type ServiceTier, StreamUsageCollector, type StreamUsageCollectorOptions, type SummarizeInput, TOKEN_CATEGORIES, type TokenCategory, type UpsertBudgetInput, type UsageNormalizer, type UsageRecord, type UsageRecordedEventData, type UsageReportOptions, UsageReportService, type UsageReversedEventData, type UsageStatus, type UsageSummary, WALLET_ENTRY_TYPES, type Wallet, type WalletBalance, type WalletDepletedEventData, type WalletEntry, type WalletEntryFilter, type WalletEntryPage, type WalletEntryType, type WalletGrantedEventData, type WalletLowBalanceEventData, type WalletRef, WalletService, type WalletServiceOptions, applyMarkup, computeCostNanoUsd, deriveIdempotencyKey, floatUsdToNanoUsd, formatNanoUsd, isLegalLedgerTransition, isLegalTransitionPatchKey, normalizeAnthropicUsage, normalizeBedrockConverseUsage, normalizeGeminiUsage, normalizeMistralUsage, normalizeOpenAiChatUsage, normalizeOpenAiCompatibleUsage, normalizeOpenAiResponsesUsage, normalizeOpenRouterUsage, normalizeVercelAiSdkUsage, perMillion, providerPresets, resolveMultiplier4dp, toJsonSafe };