@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.
- package/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/prices/index.cjs +218 -0
- package/dist/prices/index.d.cts +131 -0
- package/dist/prices/index.d.ts +131 -0
- package/dist/prices/index.mjs +216 -0
- package/dist/prisma/index.cjs +1197 -0
- package/dist/prisma/index.d.cts +677 -0
- package/dist/prisma/index.d.ts +677 -0
- package/dist/prisma/index.mjs +1195 -0
- package/dist/prisma/migrations/0001_init.sql +198 -0
- package/dist/prisma/schema.prisma.fragment +197 -0
- package/dist/redis/index.cjs +110 -0
- package/dist/redis/index.d.cts +98 -0
- package/dist/redis/index.d.ts +98 -0
- package/dist/redis/index.mjs +107 -0
- package/dist/server/index.cjs +4405 -0
- package/dist/server/index.d.cts +3202 -0
- package/dist/server/index.d.ts +3202 -0
- package/dist/server/index.mjs +4354 -0
- package/dist/shared/index.cjs +659 -0
- package/dist/shared/index.d.cts +1044 -0
- package/dist/shared/index.d.ts +1044 -0
- package/dist/shared/index.mjs +629 -0
- package/package.json +216 -0
|
@@ -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 };
|