@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,677 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Catalog of the AI providers this library recognizes out of the
|
|
5
|
+
* box. Custom OpenAI-compatible providers register their own id at runtime via
|
|
6
|
+
* `providerPresets.openaiCompatible(id)`, so the public {@link ProviderId} type
|
|
7
|
+
* (see `types/catalogs.ts`) widens this closed list with `(string & {})`.
|
|
8
|
+
* @layer shared
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The provider identifiers shipped with the library. Frozen tuple so the derived
|
|
12
|
+
* {@link KnownProviderId} union stays in sync with the runtime values.
|
|
13
|
+
*/
|
|
14
|
+
declare const PROVIDER_IDS: readonly ["openai", "azure-openai", "anthropic", "gemini", "vertex", "mistral", "bedrock", "openrouter", "deepseek", "xai", "groq"];
|
|
15
|
+
/** A provider id known to the library at build time (excludes custom ids). */
|
|
16
|
+
type KnownProviderId = (typeof PROVIDER_IDS)[number];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @fileoverview Catalog of AI operation kinds a usage record can describe. The
|
|
20
|
+
* operation is part of the price-resolution key; `'responses'` bills identically
|
|
21
|
+
* to `'chat'` and shares its price rows (see spec §5.1).
|
|
22
|
+
* @layer shared
|
|
23
|
+
*/
|
|
24
|
+
/** Every operation kind the library can meter and price. */
|
|
25
|
+
declare const AI_OPERATIONS: readonly ["chat", "responses", "embeddings", "image", "video", "audio", "rerank", "moderation"];
|
|
26
|
+
/** A logical AI operation kind. */
|
|
27
|
+
type AiOperation = (typeof AI_OPERATIONS)[number];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @fileoverview Catalog of response service tiers. Providers may silently
|
|
31
|
+
* downgrade a request (OpenAI returns `service_tier: "default"` when priority
|
|
32
|
+
* capacity is exhausted), so pricing keys off the tier reported in the RESPONSE,
|
|
33
|
+
* which is what the normalizers read (see spec §5.1).
|
|
34
|
+
* @layer shared
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Service tier of the actual response. `'batch'` is the Batch API (~50% discount);
|
|
38
|
+
* `'flex'` is OpenAI flex processing (batch rates, synchronous); `'priority'` is
|
|
39
|
+
* paid premium; `'standard'` is the default.
|
|
40
|
+
*/
|
|
41
|
+
declare const SERVICE_TIERS: readonly ["standard", "batch", "flex", "priority"];
|
|
42
|
+
/** A response service tier. */
|
|
43
|
+
type ServiceTier = (typeof SERVICE_TIERS)[number];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @fileoverview Catalog of the token categories that are rated independently.
|
|
47
|
+
* Cache reads/writes are first-class because a naive input/output-only model
|
|
48
|
+
* over-bills cached traffic by up to 10× (see spec §5.4).
|
|
49
|
+
* @layer shared
|
|
50
|
+
*/
|
|
51
|
+
/** Every token category that carries its own per-million rate. */
|
|
52
|
+
declare const TOKEN_CATEGORIES: readonly ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h", "reasoning", "audioIn", "audioOut", "imageIn", "imageOut"];
|
|
53
|
+
/** A single rated token category. */
|
|
54
|
+
type TokenCategory = (typeof TOKEN_CATEGORIES)[number];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @fileoverview Catalog of wallet entry kinds. Balances are the signed sum of
|
|
58
|
+
* these append-only entries (see spec §9.1).
|
|
59
|
+
* @layer shared
|
|
60
|
+
*/
|
|
61
|
+
/** Every kind of entry that can appear in a wallet's append-only ledger. */
|
|
62
|
+
declare const WALLET_ENTRY_TYPES: readonly ["grant", "debit", "refund", "adjustment", "expiry"];
|
|
63
|
+
/** A wallet entry kind. */
|
|
64
|
+
type WalletEntryType = (typeof WALLET_ENTRY_TYPES)[number];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @fileoverview Canonical catalog types that the rest of the library is generic
|
|
68
|
+
* over: provider/operation/tier unions, the metering scope, the rating mode, and
|
|
69
|
+
* the pure normalizer/preset contracts. Framework-free.
|
|
70
|
+
* @layer shared
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A provider identifier. Widened with `(string & {})` so custom
|
|
75
|
+
* OpenAI-compatible providers can register their own id while known ids keep
|
|
76
|
+
* editor autocomplete.
|
|
77
|
+
*/
|
|
78
|
+
type ProviderId = KnownProviderId | (string & {});
|
|
79
|
+
/** The payer subject whose wallet/budget a call consumes. */
|
|
80
|
+
interface MeteringScope {
|
|
81
|
+
/** `'key'` scopes spend their owner's wallet — they cannot own money. */
|
|
82
|
+
type: 'tenant' | 'team' | 'user' | 'key';
|
|
83
|
+
id: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @fileoverview The immutable ledger row for one AI call (or its hold), its
|
|
88
|
+
* lifecycle status, the ledger query filter, and the append insert type.
|
|
89
|
+
* @layer shared
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/** Lifecycle status of a usage record (see spec §8.3). */
|
|
93
|
+
type UsageStatus = 'pending' | 'posted' | 'reversed' | 'released';
|
|
94
|
+
/**
|
|
95
|
+
* One append-only ledger entry for a single AI call (or its hold). All money is
|
|
96
|
+
* integer nano-USD; token counts are plain `number`.
|
|
97
|
+
*/
|
|
98
|
+
interface UsageRecord {
|
|
99
|
+
id: string;
|
|
100
|
+
tenantId: string;
|
|
101
|
+
/** The PAYER — the subject whose wallet/budget is consumed. */
|
|
102
|
+
scope: MeteringScope;
|
|
103
|
+
/** Optional distinct beneficiary (reporting dimension only). */
|
|
104
|
+
beneficiary?: MeteringScope;
|
|
105
|
+
/** Optional actor who triggered the call (audit). */
|
|
106
|
+
requestedBy?: string;
|
|
107
|
+
provider: ProviderId;
|
|
108
|
+
/** Model reported by the response (pricing follows this). */
|
|
109
|
+
model: string;
|
|
110
|
+
/** Model the host requested, when supplied (drift audit; §6.6). */
|
|
111
|
+
requestedModel?: string;
|
|
112
|
+
operation: AiOperation;
|
|
113
|
+
serviceTier: ServiceTier;
|
|
114
|
+
/** Caller-supplied logical operation, e.g. `'workout.generate'`. */
|
|
115
|
+
feature: string;
|
|
116
|
+
/** Free-form cost-attribution labels (≤ 10), e.g. `['team:research']`. */
|
|
117
|
+
tags: string[];
|
|
118
|
+
inputTokens: number;
|
|
119
|
+
outputTokens: number;
|
|
120
|
+
cacheReadTokens: number;
|
|
121
|
+
cacheWrite5mTokens: number;
|
|
122
|
+
cacheWrite1hTokens: number;
|
|
123
|
+
reasoningTokens: number;
|
|
124
|
+
audioInTokens: number;
|
|
125
|
+
audioOutTokens: number;
|
|
126
|
+
imageInTokens: number;
|
|
127
|
+
imageOutTokens: number;
|
|
128
|
+
totalTokens: number;
|
|
129
|
+
/** Non-token line items actually rated, e.g. `{ web_search_requests: 2 }`. */
|
|
130
|
+
extraUnits?: Record<string, number>;
|
|
131
|
+
/** `null` for provider-reported mode or a priceMissing record. */
|
|
132
|
+
priceVersionId: string | null;
|
|
133
|
+
/** Provider cost (tokens + surcharges). */
|
|
134
|
+
rawCostNanoUsd: bigint;
|
|
135
|
+
/** The non-token share of `rawCostNanoUsd`. */
|
|
136
|
+
surchargeNanoUsd: bigint;
|
|
137
|
+
/** After markup — what the customer pays. */
|
|
138
|
+
billedCostNanoUsd: bigint;
|
|
139
|
+
/** The resolved 4-dp markup value actually applied. */
|
|
140
|
+
markupMultiplier: number;
|
|
141
|
+
/** `'USD'` in v0.1 (§7.4). */
|
|
142
|
+
currency: string;
|
|
143
|
+
/** Rated at 0 in non-strict mode; a backfill target. */
|
|
144
|
+
priceMissing: boolean;
|
|
145
|
+
status: UsageStatus;
|
|
146
|
+
/** Set on a posted record when a compensating record reversed it (§8.5). */
|
|
147
|
+
reversedByRecordId?: string;
|
|
148
|
+
/** Set on a compensating record, pointing back at the record it negates. */
|
|
149
|
+
reversesRecordId?: string;
|
|
150
|
+
/** Unique per `(tenantId, idempotencyKey)`. */
|
|
151
|
+
idempotencyKey: string;
|
|
152
|
+
/** Ties an invoice line back to app logs. */
|
|
153
|
+
correlationId?: string;
|
|
154
|
+
/** Provider request id. */
|
|
155
|
+
requestId?: string;
|
|
156
|
+
/** Platform-absorbed — never consumes wallet/budget. */
|
|
157
|
+
isSystemCost: boolean;
|
|
158
|
+
/** e.g. `'workout_generation_retry'`. */
|
|
159
|
+
systemCostCategory?: string;
|
|
160
|
+
/** True when this record consumed wallet/budget (reconciliation predicate §10.7). */
|
|
161
|
+
enforced: boolean;
|
|
162
|
+
/** Previous chain hash (optional per-tenant hash chain, §8.6). */
|
|
163
|
+
prevHash?: string;
|
|
164
|
+
/** This record's chain hash (optional per-tenant hash chain, §8.6). */
|
|
165
|
+
hash?: string;
|
|
166
|
+
/** When the LLM call happened. */
|
|
167
|
+
occurredAt: Date;
|
|
168
|
+
createdAt: Date;
|
|
169
|
+
/** Pending-row settlements only. */
|
|
170
|
+
updatedAt: Date;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Caller-supplied fields for `ILedgerStore.append`. The store computes
|
|
174
|
+
* `id`/`hash`/`prevHash`/`createdAt`/`updatedAt`.
|
|
175
|
+
*/
|
|
176
|
+
type NewUsageRecord = Omit<UsageRecord, 'id' | 'prevHash' | 'hash' | 'createdAt' | 'updatedAt'>;
|
|
177
|
+
/** Query filter for the ledger store. */
|
|
178
|
+
interface LedgerFilter {
|
|
179
|
+
tenantId: string;
|
|
180
|
+
scope?: MeteringScope;
|
|
181
|
+
beneficiary?: MeteringScope;
|
|
182
|
+
feature?: string;
|
|
183
|
+
features?: string[];
|
|
184
|
+
provider?: ProviderId;
|
|
185
|
+
model?: string;
|
|
186
|
+
operation?: AiOperation;
|
|
187
|
+
serviceTier?: ServiceTier;
|
|
188
|
+
tags?: string[];
|
|
189
|
+
isSystemCost?: boolean;
|
|
190
|
+
systemCostCategory?: string;
|
|
191
|
+
status?: UsageStatus[];
|
|
192
|
+
enforcedOnly?: boolean;
|
|
193
|
+
from?: Date;
|
|
194
|
+
to?: Date;
|
|
195
|
+
limit?: number;
|
|
196
|
+
offset?: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @fileoverview The effective-dated price row and its insert type. Rates are
|
|
201
|
+
* integer nano-USD per 1,000,000 tokens (or per unit for non-token line items),
|
|
202
|
+
* so every cost computation is exact integer math.
|
|
203
|
+
* @layer shared
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* One immutable, effective-dated price version for a (provider, model, operation,
|
|
208
|
+
* serviceTier) tuple. A call is rated at the row in effect at its timestamp.
|
|
209
|
+
*/
|
|
210
|
+
interface PriceVersion {
|
|
211
|
+
id: string;
|
|
212
|
+
provider: ProviderId;
|
|
213
|
+
model: string;
|
|
214
|
+
operation: AiOperation;
|
|
215
|
+
/** Part of the resolution key; `'standard'` is the default. */
|
|
216
|
+
serviceTier: ServiceTier;
|
|
217
|
+
/** Rates as integer nano-USD per 1,000,000 tokens. */
|
|
218
|
+
inputNanoUsdPerMillion: bigint;
|
|
219
|
+
outputNanoUsdPerMillion: bigint;
|
|
220
|
+
cacheReadNanoUsdPerMillion: bigint;
|
|
221
|
+
cacheWrite5mNanoUsdPerMillion: bigint;
|
|
222
|
+
cacheWrite1hNanoUsdPerMillion: bigint;
|
|
223
|
+
/** Usually equals output; kept separate for models that price reasoning differently. */
|
|
224
|
+
reasoningNanoUsdPerMillion: bigint;
|
|
225
|
+
audioInNanoUsdPerMillion: bigint;
|
|
226
|
+
audioOutNanoUsdPerMillion: bigint;
|
|
227
|
+
imageInNanoUsdPerMillion: bigint;
|
|
228
|
+
imageOutNanoUsdPerMillion: bigint;
|
|
229
|
+
/**
|
|
230
|
+
* Long-context tier threshold: when total input (all input-side categories)
|
|
231
|
+
* exceeds this, the tier rates replace the base input/output rates for the
|
|
232
|
+
* WHOLE call (all-or-nothing, matching Gemini's billing).
|
|
233
|
+
*/
|
|
234
|
+
tierThresholdTokens?: number;
|
|
235
|
+
tierInputNanoUsdPerMillion?: bigint;
|
|
236
|
+
tierOutputNanoUsdPerMillion?: bigint;
|
|
237
|
+
/**
|
|
238
|
+
* Non-token line items in nano-USD PER UNIT, matched against
|
|
239
|
+
* `NormalizedUsage.serverToolUse` / `MeteringContext.extraUnits` by EXACT key —
|
|
240
|
+
* so the key must equal the count key the normalizer emits (e.g. Anthropic's
|
|
241
|
+
* `usage.server_tool_use.web_search_requests`) — e.g.
|
|
242
|
+
* `{ web_search_requests: 10_000_000n }` ($0.01 per web-search call).
|
|
243
|
+
*/
|
|
244
|
+
unitRates?: Record<string, bigint>;
|
|
245
|
+
currency: 'USD';
|
|
246
|
+
effectiveFrom: Date;
|
|
247
|
+
/** `null` = current; set when a newer row supersedes it. */
|
|
248
|
+
effectiveTo: Date | null;
|
|
249
|
+
/** Provenance: `'snapshot' | 'manual' | 'import'`. */
|
|
250
|
+
source: string;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Caller-supplied fields for `upsertPrice`. Rates default to `0n` and
|
|
254
|
+
* `serviceTier` to `'standard'`; the store assigns `id` and closes the previous
|
|
255
|
+
* open row's `effectiveTo`.
|
|
256
|
+
*/
|
|
257
|
+
type NewPriceVersion = Partial<Omit<PriceVersion, 'id' | 'effectiveTo'>> & Pick<PriceVersion, 'provider' | 'model' | 'operation'>;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @fileoverview Wallet types: the owner reference, the materialized wallet, and
|
|
261
|
+
* the append-only entry ledger whose signed sum is the balance (see spec §9.1).
|
|
262
|
+
* @layer shared
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Identifies the owner of a wallet. `'key'` scopes deliberately cannot own money
|
|
267
|
+
* — API keys spend their owner's wallet.
|
|
268
|
+
*/
|
|
269
|
+
interface WalletRef {
|
|
270
|
+
tenantId: string;
|
|
271
|
+
ownerType: 'tenant' | 'team' | 'user';
|
|
272
|
+
ownerId: string;
|
|
273
|
+
}
|
|
274
|
+
/** A prepaid credit wallet with a materialized nano-USD balance. */
|
|
275
|
+
interface Wallet {
|
|
276
|
+
id: string;
|
|
277
|
+
tenantId: string;
|
|
278
|
+
ownerType: WalletRef['ownerType'];
|
|
279
|
+
ownerId: string;
|
|
280
|
+
/** Materialized, kept transactionally consistent with Σ entries. */
|
|
281
|
+
balanceNanoUsd: bigint;
|
|
282
|
+
createdAt: Date;
|
|
283
|
+
updatedAt: Date;
|
|
284
|
+
}
|
|
285
|
+
/** One append-only entry in a wallet's ledger. */
|
|
286
|
+
interface WalletEntry {
|
|
287
|
+
id: string;
|
|
288
|
+
walletId: string;
|
|
289
|
+
type: WalletEntryType;
|
|
290
|
+
/** `+` = credit, `−` = debit. */
|
|
291
|
+
amountNanoUsd: bigint;
|
|
292
|
+
/** Grant burn priority (lower first). */
|
|
293
|
+
priority: number;
|
|
294
|
+
effectiveAt: Date;
|
|
295
|
+
/** Grants only. */
|
|
296
|
+
expiresAt?: Date;
|
|
297
|
+
/** Present on usage-driven debits/refunds. */
|
|
298
|
+
usageRecordId?: string;
|
|
299
|
+
/** Unique per wallet. */
|
|
300
|
+
idempotencyKey: string;
|
|
301
|
+
reason?: string;
|
|
302
|
+
createdAt: Date;
|
|
303
|
+
}
|
|
304
|
+
/** Caller-supplied fields for `IWalletStore.appendEntry`. The store assigns `id`/`createdAt`. */
|
|
305
|
+
type NewWalletEntry = Omit<WalletEntry, 'id' | 'createdAt'>;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @fileoverview Budget types: the budget definition, its window/policy unions,
|
|
309
|
+
* and the read-side status DTOs a usage meter renders (see spec §10).
|
|
310
|
+
* @layer shared
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
/** How a budget's window repeats. `{ customSeconds }` is a fixed-length rolling window. */
|
|
314
|
+
type BudgetWindowKind = 'day' | 'week' | 'month' | 'total' | {
|
|
315
|
+
customSeconds: number;
|
|
316
|
+
};
|
|
317
|
+
/** What happens when a budget is exceeded. */
|
|
318
|
+
type BudgetPolicy = 'block' | 'throttle' | 'allow';
|
|
319
|
+
/** A spend/token/count cap for a scope over a window. */
|
|
320
|
+
interface Budget {
|
|
321
|
+
id: string;
|
|
322
|
+
tenantId: string;
|
|
323
|
+
scope: MeteringScope;
|
|
324
|
+
/** Restrict which usage counts; empty/absent = all features. */
|
|
325
|
+
features?: string[];
|
|
326
|
+
/** Billed-spend cap. */
|
|
327
|
+
limitNanoUsd?: bigint;
|
|
328
|
+
/** Total-token cap. */
|
|
329
|
+
limitTokens?: number;
|
|
330
|
+
/** Operation-count cap (posted, enforced, non-reversed records). */
|
|
331
|
+
limitCount?: number;
|
|
332
|
+
window: BudgetWindowKind;
|
|
333
|
+
/**
|
|
334
|
+
* Per-budget window anchor. Absent → calendar UTC anchoring. Month windows
|
|
335
|
+
* clamp short months (a Jan 31 anchor yields Feb 28/29, Mar 31, …).
|
|
336
|
+
*/
|
|
337
|
+
anchorAt?: Date;
|
|
338
|
+
/** Optional budget lifetime: enforcement ignores the budget after this instant. */
|
|
339
|
+
expiresAt?: Date;
|
|
340
|
+
/** Fractions of the limit that trigger soft alert events. */
|
|
341
|
+
softThresholds: number[];
|
|
342
|
+
policy: BudgetPolicy;
|
|
343
|
+
createdAt: Date;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @fileoverview Reporting types: the query filter and the aggregated usage
|
|
348
|
+
* summary (including the cache-savings figure) produced by `UsageReportService`.
|
|
349
|
+
* @layer shared
|
|
350
|
+
*/
|
|
351
|
+
|
|
352
|
+
/** One aggregated row of a usage report. */
|
|
353
|
+
interface UsageSummary {
|
|
354
|
+
/** groupBy key → value; empty groupBy = one grand-total row. */
|
|
355
|
+
group: Record<string, string>;
|
|
356
|
+
records: number;
|
|
357
|
+
totalTokens: number;
|
|
358
|
+
tokens: Record<TokenCategory, number>;
|
|
359
|
+
rawCostNanoUsd: bigint;
|
|
360
|
+
surchargeNanoUsd: bigint;
|
|
361
|
+
billedCostNanoUsd: bigint;
|
|
362
|
+
/** Σ cacheReadTokens × (inputRate − cacheReadRate) — the "you saved $X via caching" figure. */
|
|
363
|
+
cacheSavingsNanoUsd: bigint;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @fileoverview The budget persistence port (spec §15.1). Multi-dimension
|
|
368
|
+
* budgets with atomic conditional consume, signed window adjustment, and window
|
|
369
|
+
* rotation. Only validated at init when the budget feature is enabled.
|
|
370
|
+
* @layer server
|
|
371
|
+
*/
|
|
372
|
+
|
|
373
|
+
/** A signed or absolute per-window spend delta across the three budget dimensions. */
|
|
374
|
+
interface BudgetDelta {
|
|
375
|
+
nanoUsd: bigint;
|
|
376
|
+
tokens: number;
|
|
377
|
+
count: number;
|
|
378
|
+
}
|
|
379
|
+
/** Limits checked by {@link IBudgetStore.conditionalConsume} (absent = unlimited). */
|
|
380
|
+
interface BudgetLimits {
|
|
381
|
+
nanoUsd?: bigint;
|
|
382
|
+
tokens?: number;
|
|
383
|
+
count?: number;
|
|
384
|
+
}
|
|
385
|
+
/** A window's materialized spend counters. */
|
|
386
|
+
interface BudgetWindowSpend {
|
|
387
|
+
spentNanoUsd: bigint;
|
|
388
|
+
spentTokens: number;
|
|
389
|
+
spentCount: number;
|
|
390
|
+
}
|
|
391
|
+
/** The budget port. */
|
|
392
|
+
interface IBudgetStore {
|
|
393
|
+
/** Create or replace a budget. */
|
|
394
|
+
upsert(budget: Omit<Budget, 'id' | 'createdAt'> & {
|
|
395
|
+
id?: string;
|
|
396
|
+
}): Promise<Budget>;
|
|
397
|
+
/** Delete a budget. */
|
|
398
|
+
remove(budgetId: string): Promise<void>;
|
|
399
|
+
/** Load one budget by id — the rotateWindow/reconcileWindow paths read it by id (§10.5). */
|
|
400
|
+
findBudgetById(budgetId: string): Promise<Budget | null>;
|
|
401
|
+
/**
|
|
402
|
+
* Every budget for the tenant scoped to the EXACT scope or tenant-wide (§10.3). A
|
|
403
|
+
* single {@link MeteringScope} carries no team/user lineage, so the tenant is the
|
|
404
|
+
* only ancestor resolvable — intermediate ancestors are not matched. Each budget
|
|
405
|
+
* returned consumes independently; a subject-level budget never exempts the tenant cap.
|
|
406
|
+
*/
|
|
407
|
+
findMatching(tenantId: string, scope: MeteringScope): Promise<Budget[]>;
|
|
408
|
+
/**
|
|
409
|
+
* Atomic multi-dimension conditional consume (§10.8). `false` = a limit would be
|
|
410
|
+
* exceeded. Creates the window row on first touch.
|
|
411
|
+
*/
|
|
412
|
+
conditionalConsume(budgetId: string, windowStart: Date, delta: BudgetDelta, limits: BudgetLimits): Promise<boolean>;
|
|
413
|
+
/** Signed release/adjust of a window's counters (capture delta, release, reverse). */
|
|
414
|
+
adjustWindow(budgetId: string, windowStart: Date, delta: BudgetDelta): Promise<void>;
|
|
415
|
+
/** A window's current spend, or `null` when it has no row yet. */
|
|
416
|
+
getWindow(budgetId: string, windowStart: Date): Promise<BudgetWindowSpend | null>;
|
|
417
|
+
/** Move a budget's active window start (rotateWindow support). */
|
|
418
|
+
setWindowStart(budgetId: string, windowStart: Date): Promise<void>;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @fileoverview The ledger persistence port (spec §15.1). The append-only usage
|
|
423
|
+
* ledger: idempotent append, atomic state transitions, and aggregate queries.
|
|
424
|
+
* Implemented by the official Prisma adapter or a custom store.
|
|
425
|
+
* @layer server
|
|
426
|
+
*/
|
|
427
|
+
|
|
428
|
+
/** Aggregate cost totals returned by {@link ILedgerStore.sumCost}. */
|
|
429
|
+
interface LedgerCostSummary {
|
|
430
|
+
rawCostNanoUsd: bigint;
|
|
431
|
+
billedCostNanoUsd: bigint;
|
|
432
|
+
surchargeNanoUsd: bigint;
|
|
433
|
+
totalTokens: number;
|
|
434
|
+
records: number;
|
|
435
|
+
}
|
|
436
|
+
/** A report group-by dimension (spec §13.1). */
|
|
437
|
+
type ReportGroupBy = 'day' | 'week' | 'month' | 'feature' | 'provider' | 'model' | 'operation' | 'serviceTier' | 'scope' | 'beneficiary' | 'tag' | 'systemCostCategory';
|
|
438
|
+
/** The append-only usage ledger port. */
|
|
439
|
+
interface ILedgerStore {
|
|
440
|
+
/**
|
|
441
|
+
* Upsert on `(tenantId, idempotencyKey)` — a replay returns the existing record
|
|
442
|
+
* iff the payload hash matches; a different hash is a conflict (§8.4). When
|
|
443
|
+
* `hashChain` is true and the record is `posted`, the store reads the tenant's
|
|
444
|
+
* last chain hash, computes this record's `prevHash`/`hash`, and persists them
|
|
445
|
+
* ATOMICALLY under a per-tenant lock (§8.6) — the read-compute-write is one
|
|
446
|
+
* serialized operation so concurrent appends never fork the chain. When false
|
|
447
|
+
* (the default), no chain lookup or hashing occurs.
|
|
448
|
+
*/
|
|
449
|
+
append(record: NewUsageRecord, payloadHash: string, hashChain?: boolean): Promise<UsageRecord>;
|
|
450
|
+
/**
|
|
451
|
+
* State transitions only (§8.3): `pending→posted` (settle, amounts patched),
|
|
452
|
+
* `pending→released`, `posted→reversed` (annotation only — amount fields
|
|
453
|
+
* rejected). Atomic: returns `null` when the record was not in the expected
|
|
454
|
+
* source state (how exactly one reaper replica wins an expired hold). When
|
|
455
|
+
* `hashChain` is true and `to` is `posted` (settlement), the store computes and
|
|
456
|
+
* persists the record's chain `prevHash`/`hash` under the same per-tenant lock (§8.6).
|
|
457
|
+
*/
|
|
458
|
+
transition(id: string, from: UsageStatus, to: UsageStatus, patch?: Partial<UsageRecord>, hashChain?: boolean): Promise<UsageRecord | null>;
|
|
459
|
+
/** Look up a record by its per-tenant idempotency key. */
|
|
460
|
+
findByIdempotencyKey(tenantId: string, key: string): Promise<UsageRecord | null>;
|
|
461
|
+
/**
|
|
462
|
+
* Load one record by its global id — the reversal path loads the original to
|
|
463
|
+
* negate it into a compensating record (§8.5). Returns `null` when absent.
|
|
464
|
+
*/
|
|
465
|
+
findById(id: string): Promise<UsageRecord | null>;
|
|
466
|
+
/** Find pending holds older than `olderThan` for the reaper sweep. */
|
|
467
|
+
findExpiredHolds(olderThan: Date, limit: number): Promise<UsageRecord[]>;
|
|
468
|
+
/** Query records by filter. */
|
|
469
|
+
query(filter: LedgerFilter): Promise<UsageRecord[]>;
|
|
470
|
+
/** Aggregate cost/token totals for a filter. */
|
|
471
|
+
sumCost(filter: LedgerFilter): Promise<LedgerCostSummary>;
|
|
472
|
+
/**
|
|
473
|
+
* OPTIONAL scalable aggregation (spec §13.1): `SUM … GROUP BY` across the report
|
|
474
|
+
* dimensions, computed in the store (the official Prisma adapter uses SQL). A
|
|
475
|
+
* store that omits it falls back to `UsageReportService`'s documented
|
|
476
|
+
* query-and-aggregate-in-memory path, capped at `reporting.maxExportRows`.
|
|
477
|
+
*/
|
|
478
|
+
summarize?(filter: LedgerFilter, groupBy: ReportGroupBy[]): Promise<UsageSummary[]>;
|
|
479
|
+
/**
|
|
480
|
+
* The last posted record's hash for a tenant (hash-chain continuation, §8.6).
|
|
481
|
+
* The chain is serialized per tenant: the official Prisma adapter takes a
|
|
482
|
+
* Postgres advisory lock so the last-hash read and the chained append/settle run
|
|
483
|
+
* as one atomic operation; an in-memory store simulates this with a per-tenant
|
|
484
|
+
* mutex. Returns `null` before the tenant's first chained record.
|
|
485
|
+
*/
|
|
486
|
+
lastHash(tenantId: string): Promise<string | null>;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @fileoverview The pricing persistence port (spec §15.1). Effective-dated rate
|
|
491
|
+
* resolution, append-only price upserts, and the model list that powers §6.6
|
|
492
|
+
* prefix matching.
|
|
493
|
+
* @layer server
|
|
494
|
+
*/
|
|
495
|
+
|
|
496
|
+
/** A distinct priced model tuple, returned by {@link IPricingStore.listModels}. */
|
|
497
|
+
interface PricedModel {
|
|
498
|
+
model: string;
|
|
499
|
+
operation: AiOperation;
|
|
500
|
+
serviceTier: ServiceTier;
|
|
501
|
+
}
|
|
502
|
+
/** The effective-dated price registry port. */
|
|
503
|
+
interface IPricingStore {
|
|
504
|
+
/** The price row in effect at `at` for the tuple, or `null` when none matches. */
|
|
505
|
+
resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
|
|
506
|
+
/** Close the current open row (`effectiveTo = now`) and insert the new open row. */
|
|
507
|
+
upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
|
|
508
|
+
/** Full effective-dated history for a tuple (newest first). */
|
|
509
|
+
getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
|
|
510
|
+
/** All distinct (model, operation, serviceTier) for a provider — powers §6.6 prefix matching. */
|
|
511
|
+
listModels(provider: ProviderId): Promise<PricedModel[]>;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @fileoverview The wallet persistence port (spec §15.1). Materialized-balance
|
|
516
|
+
* wallets with atomic conditional debit, grant allocation, and reconciliation.
|
|
517
|
+
* Only validated at init when the wallet feature is enabled.
|
|
518
|
+
* @layer server
|
|
519
|
+
*/
|
|
520
|
+
|
|
521
|
+
/** An open grant with its remaining spendable value. */
|
|
522
|
+
type OpenGrant = WalletEntry & {
|
|
523
|
+
remainingNanoUsd: bigint;
|
|
524
|
+
};
|
|
525
|
+
/** A page of wallet entries with the total count. */
|
|
526
|
+
interface WalletEntryPage {
|
|
527
|
+
entries: WalletEntry[];
|
|
528
|
+
total: number;
|
|
529
|
+
}
|
|
530
|
+
/** Filter for {@link IWalletStore.listEntries}. */
|
|
531
|
+
interface WalletEntryFilter {
|
|
532
|
+
from?: Date;
|
|
533
|
+
to?: Date;
|
|
534
|
+
type?: WalletEntryType;
|
|
535
|
+
limit?: number;
|
|
536
|
+
offset?: number;
|
|
537
|
+
}
|
|
538
|
+
/** The prepaid wallet port. */
|
|
539
|
+
interface IWalletStore {
|
|
540
|
+
/** The wallet for a ref, or `null` when it does not exist yet. */
|
|
541
|
+
getWallet(ref: WalletRef): Promise<Wallet | null>;
|
|
542
|
+
/**
|
|
543
|
+
* Create the wallet when missing (idempotent) and append the entry plus any
|
|
544
|
+
* grant allocations in one transaction.
|
|
545
|
+
*/
|
|
546
|
+
appendEntry(ref: WalletRef, entry: NewWalletEntry, allocations?: {
|
|
547
|
+
grantEntryId: string;
|
|
548
|
+
amountNanoUsd: bigint;
|
|
549
|
+
}[]): Promise<WalletEntry>;
|
|
550
|
+
/** Atomic conditional debit against the materialized balance. `null` = insufficient (§9.4). */
|
|
551
|
+
conditionalDebit(ref: WalletRef, entry: NewWalletEntry, overdraftNanoUsd: bigint): Promise<WalletEntry | null>;
|
|
552
|
+
/** Open grants with remaining value, ordered per `order` — feeds allocation. */
|
|
553
|
+
openGrants(ref: WalletRef, order: 'expiry' | 'priority' | 'fifo'): Promise<OpenGrant[]>;
|
|
554
|
+
/** A page of entries for a ref. */
|
|
555
|
+
listEntries(ref: WalletRef, filter?: WalletEntryFilter): Promise<WalletEntryPage>;
|
|
556
|
+
/** Recompute the materialized balance from Σ entries. */
|
|
557
|
+
reconcile(ref: WalletRef): Promise<Wallet>;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @fileoverview The aggregate persistence bundle (spec §4.1). Ledger + pricing
|
|
562
|
+
* are always required; the wallet and budget halves are `Partial` and validated
|
|
563
|
+
* at init only when the corresponding feature is configured.
|
|
564
|
+
* @layer server
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The single store object a host provides. `forRoot()` fans it out under each
|
|
569
|
+
* per-port DI token; a host may override any individual port by binding its token
|
|
570
|
+
* (§4.6).
|
|
571
|
+
*/
|
|
572
|
+
interface IAiTokensStore extends ILedgerStore, IPricingStore, Partial<IWalletStore>, Partial<IBudgetStore> {
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @fileoverview `PrismaAiTokensStore` — the official PostgreSQL adapter (spec
|
|
577
|
+
* §15.1–15.3). It implements the ledger + pricing halves of `IAiTokensStore`
|
|
578
|
+
* against any `@prisma/client` instance, talking to PostgreSQL EXCLUSIVELY through
|
|
579
|
+
* PARAMETERIZED raw SQL (`$queryRaw`/`$executeRaw`/`$transaction`) so it never
|
|
580
|
+
* depends on generated model delegates and stays valid against the host's schema.
|
|
581
|
+
* Column identifiers are hardcoded constants (never interpolated from input) and
|
|
582
|
+
* every value is a bound parameter, so the raw SQL is injection-safe. Money is
|
|
583
|
+
* `BIGINT` nano-USD; `unitRates` round-trip as JSON of decimal strings; the
|
|
584
|
+
* `markupMultiplier` `Decimal(10,4)` maps to a `number` at the boundary (exact by
|
|
585
|
+
* the 4-dp rule, §7.2). Store errors map to the domain catalog (§15.2); a payload
|
|
586
|
+
* hash mismatch on the `(tenantId, idempotencyKey)` unique index becomes the
|
|
587
|
+
* exactly-once conflict, and unknown driver errors become `AI_TOKENS_STORE_ERROR`
|
|
588
|
+
* (connection details are never leaked). Row shapes, mappers, and SQL builders live
|
|
589
|
+
* in `./adapter-sql`; the wallet/budget halves use the same conditional-write
|
|
590
|
+
* discipline as the ledger half (§9.4, §10.8).
|
|
591
|
+
* @layer prisma
|
|
592
|
+
*/
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The official PostgreSQL adapter. Construct it with the host's `PrismaClient`
|
|
596
|
+
* (the host's schema must include the shipped models, §15.3).
|
|
597
|
+
*/
|
|
598
|
+
declare class PrismaAiTokensStore implements IAiTokensStore {
|
|
599
|
+
private readonly prisma;
|
|
600
|
+
private readonly burnOrder;
|
|
601
|
+
/**
|
|
602
|
+
* @param prisma The host's Prisma client (talks to PostgreSQL).
|
|
603
|
+
* @param options Optional wallet grant burn order (must match the module's `wallets.burnOrder`; default `'expiry'`).
|
|
604
|
+
*/
|
|
605
|
+
constructor(prisma: PrismaClient, options?: {
|
|
606
|
+
burnOrder?: 'expiry' | 'priority' | 'fifo';
|
|
607
|
+
});
|
|
608
|
+
append(record: NewUsageRecord, payloadHash: string, hashChain?: boolean): Promise<UsageRecord>;
|
|
609
|
+
transition(id: string, from: UsageStatus, to: UsageStatus, patch?: Partial<UsageRecord>, hashChain?: boolean): Promise<UsageRecord | null>;
|
|
610
|
+
findByIdempotencyKey(tenantId: string, key: string): Promise<UsageRecord | null>;
|
|
611
|
+
findById(id: string): Promise<UsageRecord | null>;
|
|
612
|
+
findExpiredHolds(olderThan: Date, limit: number): Promise<UsageRecord[]>;
|
|
613
|
+
query(filter: LedgerFilter): Promise<UsageRecord[]>;
|
|
614
|
+
sumCost(filter: LedgerFilter): Promise<LedgerCostSummary>;
|
|
615
|
+
lastHash(tenantId: string): Promise<string | null>;
|
|
616
|
+
resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
|
|
617
|
+
upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
|
|
618
|
+
getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
|
|
619
|
+
listModels(provider: ProviderId): Promise<PricedModel[]>;
|
|
620
|
+
/** Advisory seed lock (§6.4): the first caller wins; concurrent boots seed exactly once. */
|
|
621
|
+
acquireSeedLock(key: string): Promise<boolean>;
|
|
622
|
+
/** Insert one usage row and return it; shared by the plain and chained paths. */
|
|
623
|
+
private insert;
|
|
624
|
+
/** Insert a `posted` row under the per-tenant chain lock (§8.6). */
|
|
625
|
+
private insertChained;
|
|
626
|
+
/** Settle a hold `pending → posted`, computing its chain hash under the lock (§8.6). */
|
|
627
|
+
private settleChained;
|
|
628
|
+
/** Atomic status transition via a conditional `UPDATE`; 0 rows → `null` (§8.3). */
|
|
629
|
+
private updateStatus;
|
|
630
|
+
/** The tenant's last chain hash (most recently created settled row). */
|
|
631
|
+
private lastHashWithin;
|
|
632
|
+
getWallet(ref: WalletRef): Promise<Wallet | null>;
|
|
633
|
+
appendEntry(ref: WalletRef, entry: NewWalletEntry, allocations?: {
|
|
634
|
+
grantEntryId: string;
|
|
635
|
+
amountNanoUsd: bigint;
|
|
636
|
+
}[]): Promise<WalletEntry>;
|
|
637
|
+
conditionalDebit(ref: WalletRef, entry: NewWalletEntry, overdraftNanoUsd: bigint): Promise<WalletEntry | null>;
|
|
638
|
+
openGrants(ref: WalletRef, order: 'expiry' | 'priority' | 'fifo'): Promise<OpenGrant[]>;
|
|
639
|
+
listEntries(ref: WalletRef, filter?: WalletEntryFilter): Promise<WalletEntryPage>;
|
|
640
|
+
reconcile(ref: WalletRef): Promise<Wallet>;
|
|
641
|
+
upsert(input: Omit<Budget, 'id' | 'createdAt'> & {
|
|
642
|
+
id?: string;
|
|
643
|
+
}): Promise<Budget>;
|
|
644
|
+
remove(budgetId: string): Promise<void>;
|
|
645
|
+
findBudgetById(budgetId: string): Promise<Budget | null>;
|
|
646
|
+
findMatching(tenantId: string, scope: MeteringScope): Promise<Budget[]>;
|
|
647
|
+
conditionalConsume(budgetId: string, windowStart: Date, delta: BudgetDelta, limits: BudgetLimits): Promise<boolean>;
|
|
648
|
+
adjustWindow(budgetId: string, windowStart: Date, delta: BudgetDelta): Promise<void>;
|
|
649
|
+
getWindow(budgetId: string, windowStart: Date): Promise<BudgetWindowSpend | null>;
|
|
650
|
+
setWindowStart(budgetId: string, windowStart: Date): Promise<void>;
|
|
651
|
+
/** Load the wallet within a transaction (existence + id for the conditional debit). */
|
|
652
|
+
private getWalletWithin;
|
|
653
|
+
/** Fetch or (optionally) create the wallet for a ref within a transaction. */
|
|
654
|
+
private ensureWallet;
|
|
655
|
+
/** Insert a wallet entry, returning the stored row. */
|
|
656
|
+
private insertWalletEntry;
|
|
657
|
+
/** Insert the debit→grant allocation trail. */
|
|
658
|
+
private insertAllocations;
|
|
659
|
+
/** Apply a signed delta to the materialized balance. */
|
|
660
|
+
private applyBalanceDelta;
|
|
661
|
+
/** Write an `expiry` entry (with its allocation) for each expired grant with a remainder. */
|
|
662
|
+
private sweepExpiredGrants;
|
|
663
|
+
/** Greedily allocate a debit of `cost` across the open grants in burn order. */
|
|
664
|
+
private burnDown;
|
|
665
|
+
/** The open-grants query (remaining > 0, effective, not expired) ordered per burn order. */
|
|
666
|
+
private openGrantsSql;
|
|
667
|
+
/** Return the existing entry on a matching replay, throw the conflict, or `undefined` when new. */
|
|
668
|
+
private walletReplay;
|
|
669
|
+
/** Ensure a (zeroed) window row exists for first-touch consumption. */
|
|
670
|
+
private ensureWindow;
|
|
671
|
+
/** Map the P2002 replay-or-conflict path for a wallet entry (§15.2). */
|
|
672
|
+
private replayOrConflictWallet;
|
|
673
|
+
/** Return the existing record on a matching payload replay, else raise the conflict (§15.2). */
|
|
674
|
+
private replayOrConflict;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export { PrismaAiTokensStore };
|