@bymax-one/nest-ai-tokens 1.1.2 → 1.1.3

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 CHANGED
@@ -8,6 +8,105 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.1.3] - 2026-10-31
12
+
13
+ **Billing:** if you set prices with `upsertPrice` for a model the snapshot also prices while
14
+ `pricing.seedFromSnapshot` was on, a restart of an earlier version could replace them with the
15
+ snapshot. Run the two queries under the seed entry below to look for affected rows.
16
+
17
+ ### Fixed
18
+
19
+ - **`forRootAsync({ useFactory })` accepts a factory with typed parameters.** The declared
20
+ signature `(...args: unknown[]) => ...` rejected the form Nest code normally writes,
21
+ `useFactory: (config: ConfigService) => ...`, so every consumer had to take `unknown` and cast.
22
+ The member is now declared with method syntax, which makes its parameters bivariant, and the
23
+ variadic form still compiles. No runtime change.
24
+
25
+ **Apply:** no action required. A cast such as `(prisma: unknown) => ... prisma as PrismaService`
26
+ can be replaced by a typed parameter.
27
+
28
+ - **A disabled wallet, budget or guard service no longer injects as `null` with `forRootAsync`.**
29
+ `forRootAsync` resolves its options after the providers are declared, so it registers
30
+ `WalletService`, `BudgetService` and `BudgetGuard` even when the options leave them out, and it
31
+ used to inject `null`: a consumer got `TypeError: Cannot read properties of null` at its first
32
+ call, with a type that said `WalletService`. It now injects a stand-in whose methods throw
33
+ `AI_TOKENS_INVALID_CONFIG`; the reason names the missing options block, `wallets` or
34
+ `budgets`. The stand-in is an instance of the service's class. `forRoot` is unchanged: it does not register a disabled service.
35
+
36
+ **Apply:** code that tested the injected service for `null` to detect a disabled feature must
37
+ check its own options instead.
38
+
39
+ - **`AI_TOKENS_NOT_CONFIGURED` is documented as reserved.** Nest finishes resolving the async
40
+ options before it builds any service, so nothing raises it.
41
+
42
+ - **A restart no longer reverts prices the host set, and the snapshot seed no longer adds rows.**
43
+ With `pricing.seedFromSnapshot` on (the default), every boot re-ran each seed row through
44
+ `upsertPrice`, which closes the open row of that model and inserts a new one. A second boot
45
+ against a seeded database added one closed zero-length row and one new open row per seed row
46
+ (34 rows after two boots, 17 closed), and a rate the host had set with `upsertPrice` was
47
+ closed before its own start date while the snapshot row became the open row again, so the
48
+ snapshot price silently replaced the host's price on every restart. The seed now inserts
49
+ only the model, operation and tier combinations that have no row at all, in one transaction
50
+ under a per-combination `pg_advisory_xact_lock`, and never touches existing history. The
51
+ session-scoped `pg_try_advisory_lock` the seed used is no longer taken: a session lock stays
52
+ held until its connection closes, so whether a boot seeded depended on which old connections
53
+ were still alive. `PrismaAiTokensStore.acquireSeedLock` stays for compatibility and is
54
+ deprecated. `IPricingStore` gains the optional method `seedPrice`; a custom store without it
55
+ is seeded from `getPriceHistory` and `upsertPrice`, which is not atomic across replicas.
56
+
57
+ **Apply:** a 1.1.x database may hold duplicated zero-length rows and reverted overrides. Find
58
+ them with the queries below; deleting or re-entering rows is your call, nothing here changes
59
+ data.
60
+
61
+ ```sql
62
+ -- Rows the old seed closed at the instant they began (one per extra boot).
63
+ SELECT "provider", "model", "operation", "serviceTier", COUNT(*) AS "rows"
64
+ FROM "ai_model_prices"
65
+ WHERE "effectiveTo" = "effectiveFrom"
66
+ GROUP BY 1, 2, 3, 4;
67
+
68
+ -- Rows closed before their own start: a price set with upsertPrice that a later boot reverted.
69
+ SELECT "provider", "model", "operation", "serviceTier", "inputNanoUsdPerMillion",
70
+ "effectiveFrom", "effectiveTo"
71
+ FROM "ai_model_prices"
72
+ WHERE "effectiveTo" < "effectiveFrom";
73
+ ```
74
+
75
+ A row from the second query is a rate you set; re-enter it with `upsertPrice` at its original
76
+ `effectiveFrom`.
77
+
78
+ ### Added
79
+
80
+ - `ChainVerification`, the result of `LedgerService.verifyChain`, is exported by name from the
81
+ package root (a type only).
82
+
83
+ ### Documentation
84
+
85
+ - **The README says how to create the tables and what the public API is.** The SQL migration and
86
+ the Prisma schema fragment ship in the package under `dist/prisma/` but were not mentioned; the
87
+ setup section now names both. The API reference lists the decorators (including `@AiFeature`),
88
+ the money and ledger helpers, the catalogs, the injection tokens and `CounterValueOutOfRangeError`.
89
+ The `budgets.defaultPolicy` Type column read `'block' | 'alert'`; the type is
90
+ `'block' | 'throttle' | 'allow'`. The content sidecar section states that the library ships no
91
+ `IContentStore` implementation or table, and the configuration section describes the
92
+ `forRootAsync` and `forRoot` behaviour for a disabled wallet or budget block. The 1.0.0 entries
93
+ that named an `OtelTelemetrySink` and a six-step resolution chain were corrected to the code.
94
+
95
+ - **The README now matches the code.** The configuration table gave the wrong default for
96
+ `pricing.seedFromSnapshot` (`true`, not `false`), `budgets.defaultPolicy` (`'block'`, not
97
+ `'alert'`) and `holds.ttlSeconds` (`3600`, not `300`), and gave no default for
98
+ `wallets.creditRateNanoUsd` (`1_000_000_000n`). It told readers to pass an
99
+ `OtelTelemetrySink` that the library does not ship; the option is `telemetry.sink`, which
100
+ takes your own `ITelemetrySink`, next to `telemetry.metrics`. The Bedrock preset is
101
+ `providerPresets.bedrock`, not `providerPresets.bedrockConverse`. The artifact-size table,
102
+ the stack list (it named a `ConfigurableModuleBuilder` the module does not use) and the
103
+ `AI_TOKENS_PRICE_NOT_FOUND` description (it counted resolution steps) were corrected too.
104
+ - **The README examples compile.** Some examples called methods or read fields that do not
105
+ exist: `wallets.getOrCreate` (the method is `getBalance`, and it returns `nanoUsd` and
106
+ `credits`), `metering.summarize` (it is `UsageReportService.summarize`), `reports.export`
107
+ with a `format` property (the format is the second argument), and a markup policy typed on
108
+ `MeteringContext` (its `resolve` takes the narrower call context).
109
+
11
110
  ## [1.1.2] - 2026-10-30
12
111
 
13
112
  ### Fixed
@@ -203,7 +302,7 @@ have regressed from. They are kept because the reasoning is worth having.
203
302
  ### Added
204
303
 
205
304
  - **Nine provider normalizers** — OpenAI Chat Completions, OpenAI Responses API, OpenAI-compatible (DeepSeek, xAI, Groq, Azure OpenAI), Anthropic Messages, Google Gemini / Vertex AI, AWS Bedrock Converse, Mistral, OpenRouter, Vercel AI SDK v5/v6. All are pure functions over plain objects; no provider SDK peer dependency.
206
- - **Versioned effective-dated pricing** — `PricingService` with a six-step model-resolution chain, in-memory TTL cache, `upsertPrice()`, and idempotent `MODEL_PRICES_SEED` snapshot seed. Rates are bigint nano-USD per million tokens; past records are never re-rated.
305
+ - **Versioned effective-dated pricing** — `PricingService` with a model-resolution chain (exact, `baseModel`, alias map, normalized id, longest prefix), in-memory TTL cache, `upsertPrice()`, and idempotent `MODEL_PRICES_SEED` snapshot seed. Rates are bigint nano-USD per million tokens; past records are never re-rated.
207
306
  - **Immutable append-only ledger** — `LedgerService` with content-derived idempotency (`deriveIdempotencyKey`), exactly-once semantics (payload-hash replay detection), lifecycle transitions (pending → posted → reversed/released), filtered queries, and optional per-tenant tamper-evident hash chain (`verifyChain()`).
208
307
  - **First-class markup / margin** — `MarkupResolver` with a fixed multiplier or a per-call `IMarkupPolicy`. Every record stores both `rawCostNanoUsd` (provider cost) and `billedCostNanoUsd` (after markup). The resale lever is a first-class configuration option, not application code.
209
308
  - **Prepaid wallets** — `WalletService` with grant/debit/refund/adjust entries, configurable burn order (expiry/priority/FIFO), overdraft support, and debit-allocation tracking.
@@ -214,7 +313,7 @@ have regressed from. They are kept because the reasoning is worth having.
214
313
  - **NestJS enforcement** — `BudgetGuard` (CanActivate), `MeteringInterceptor` (NestInterceptor), and the `@Meter`, `@RequireBudget`, `@AiFeature` decorators for declarative controller-level metering.
215
314
  - **Usage reporting** — `UsageReportService` with SQL-aggregated `summarize()`, CSV/JSON `export()`, paginated history, per-model analytics, and currency conversion.
216
315
  - **Events** — Typed domain events (`ai_tokens.*`) delivered to `@nestjs/event-emitter` (optional peer) and/or an `IEventSink` port.
217
- - **OpenTelemetry** — Optional `ITelemetrySink` / `OtelTelemetrySink` via `@opentelemetry/api` (optional peer).
316
+ - **OpenTelemetry** — The `ITelemetrySink` port, which you implement over `@opentelemetry/api` (optional peer); the library ships no sink and imports nothing from `@opentelemetry/api`.
218
317
  - **PostgreSQL adapter** — `PrismaAiTokensStore` (`./prisma` subpath) implementing all four storage ports via parameterized raw SQL. Seven-table schema with migrations.
219
318
  - **Five subpaths** — `.` (server), `./shared` (zero-dep), `./prices` (data-only), `./prisma` (adapter), `./redis` (counter).
220
319
  - **Zero runtime dependencies** — `"dependencies": {}`. All runtime functionality via peer dependencies.
@@ -297,7 +396,8 @@ have regressed from. They are kept because the reasoning is worth having.
297
396
  [1.0.2]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.1...v1.0.2
298
397
  [1.1.0]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.4...v1.1.0
299
398
  [1.1.2]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.1...v1.1.2
399
+ [1.1.3]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.2...v1.1.3
300
400
  [1.1.1]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.0...v1.1.1
301
401
  [1.0.4]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.3...v1.0.4
302
402
  [1.0.3]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.2...v1.0.3
303
- [Unreleased]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.2...HEAD
403
+ [Unreleased]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.3...HEAD
package/README.md CHANGED
@@ -134,10 +134,10 @@ the feature that needs them is enabled.
134
134
 
135
135
  | Artifact | Brotli size | Budget |
136
136
  | ------------------------------------------- | ----------- | ------- |
137
- | server (`@bymax-one/nest-ai-tokens`) | 37 KB | < 40 KB |
137
+ | server (`@bymax-one/nest-ai-tokens`) | 38 KB | < 40 KB |
138
138
  | shared (`@bymax-one/nest-ai-tokens/shared`) | 5 KB | < 10 KB |
139
139
  | prisma (`@bymax-one/nest-ai-tokens/prisma`) | 10 KB | < 15 KB |
140
- | redis (`@bymax-one/nest-ai-tokens/redis`) | 1.1 KB | < 5 KB |
140
+ | redis (`@bymax-one/nest-ai-tokens/redis`) | 1.3 KB | < 5 KB |
141
141
  | prices (data-only, exempt from budget) | 1.3 KB | exempt |
142
142
 
143
143
  ---
@@ -180,6 +180,20 @@ npm i @prisma/adapter-pg
180
180
  > moves to a `prisma.config.ts` at your project root. Neither change reaches this
181
181
  > library's API — `PrismaAiTokensStore` receives whatever client you built.
182
182
 
183
+ #### Create the tables
184
+
185
+ `PrismaAiTokensStore` needs seven tables, and the package ships what creates them, inside the
186
+ installed package:
187
+
188
+ - `node_modules/@bymax-one/nest-ai-tokens/dist/prisma/migrations/0001_init.sql` — the SQL for all
189
+ seven tables. Apply it with `psql -f` or `prisma db execute --file` before the first boot.
190
+ - `node_modules/@bymax-one/nest-ai-tokens/dist/prisma/schema.prisma.fragment` — the same models
191
+ for merging into your own Prisma schema if you manage migrations with `prisma migrate`.
192
+
193
+ The SQL also creates two partial indexes (exactly one open price row per model, and a fast scan
194
+ of pending holds) that a Prisma schema cannot express. If you migrate from the fragment, add them
195
+ yourself.
196
+
183
197
  ### 2 — Register the module
184
198
 
185
199
  ```typescript
@@ -284,19 +298,26 @@ export class AiController {
284
298
 
285
299
  `BymaxAiTokensModule.forRoot(options)` / `.forRootAsync({ useFactory })` accept `BymaxAiTokensModuleOptions`:
286
300
 
287
- | Option | Type | Default | Purpose |
288
- | --------------------------- | ------------------------------------- | -------------- | ----------------------------------------------------------------------------- |
289
- | `store` | `IAiTokensStore` | — (required) | Persistence adapter (use `PrismaAiTokensStore`) |
290
- | `scopeResolver` | `(ctx) => MeteringContext` | — | Resolves the caller's scope from the request (required for guard/interceptor) |
291
- | `ratingMode` | `'rate-table' \| 'provider-reported'` | `'rate-table'` | Default cost rating mode |
292
- | `markup` | `number \| IMarkupPolicy` | `1.0` | The resale multiplier (e.g. `4.0` = 4× provider cost) |
293
- | `pricing.seedFromSnapshot` | `boolean` | `false` | Seed `MODEL_PRICES_SEED` into the registry on first boot |
294
- | `wallets.creditRateNanoUsd` | `bigint` | — | Exchange rate: 1 credit = this many nano-USD |
295
- | `wallets.burnOrder` | `'expiry' \| 'priority' \| 'fifo'` | `'expiry'` | Order in which grants are consumed |
296
- | `budgets.defaultPolicy` | `'block' \| 'alert'` | `'alert'` | Default enforcement when a budget is exhausted |
297
- | `budgets.alertThresholds` | `number[]` | `[0.8, 1.0]` | Soft-threshold events at these fractions |
298
- | `holds.ttlSeconds` | `number` | `300` | Spend-hold TTL; the reaper voids expired holds |
299
- | `telemetry` | `ITelemetrySink` | no-op | OpenTelemetry sink (pass `OtelTelemetrySink`) |
301
+ | Option | Type | Default | Purpose |
302
+ | --------------------------- | ------------------------------------- | ---------------- | ------------------------------------------------------------------------------- |
303
+ | `store` | `IAiTokensStore` | — (required) | Persistence adapter (use `PrismaAiTokensStore`) |
304
+ | `scopeResolver` | `(ctx) => MeteringContext` | — | Resolves the caller's scope from the request (required for guard/interceptor) |
305
+ | `ratingMode` | `'rate-table' \| 'provider-reported'` | `'rate-table'` | Default cost rating mode |
306
+ | `markup` | `number \| IMarkupPolicy` | `1.0` | The resale multiplier (e.g. `4.0` = 4× provider cost) |
307
+ | `pricing.seedFromSnapshot` | `boolean` | `true` | Add seed rows only for combinations with no price row; never changes a price |
308
+ | `wallets.creditRateNanoUsd` | `bigint` | `1_000_000_000n` | Exchange rate: 1 credit = this many nano-USD |
309
+ | `wallets.burnOrder` | `'expiry' \| 'priority' \| 'fifo'` | `'expiry'` | Order in which grants are consumed |
310
+ | `budgets.defaultPolicy` | `'block' \| 'throttle' \| 'allow'` | `'block'` | Default enforcement when a budget is exhausted |
311
+ | `budgets.alertThresholds` | `number[]` | `[0.8, 1.0]` | Soft-threshold events at these fractions |
312
+ | `holds.ttlSeconds` | `number` | `3600` | Spend-hold TTL; the reaper voids expired holds |
313
+ | `telemetry.sink` | `ITelemetrySink` | — (off) | Your OpenTelemetry sink; the library ships none and telemetry is off without it |
314
+ | `telemetry.metrics` | `boolean` | `true` | Emit token-usage and duration metrics when a sink is present |
315
+
316
+ With `forRootAsync`, the options arrive after the providers are declared, so `WalletService`,
317
+ `BudgetService` and `BudgetGuard` are always registered. When the options leave `wallets` or
318
+ `budgets` out, the injected object throws `AI_TOKENS_INVALID_CONFIG` on any method call, with a
319
+ reason naming the missing block. With `forRoot` they are not registered, so injecting one fails
320
+ when the application boots.
300
321
 
301
322
  ---
302
323
 
@@ -309,7 +330,7 @@ export class AiController {
309
330
  | OpenAI-compatible | DeepSeek, xAI, Groq, Azure OpenAI | `normalizeOpenAiCompatibleUsage` / `providerPresets.openaiCompatible` |
310
331
  | Anthropic | Messages API | `normalizeAnthropicUsage` / `providerPresets.anthropic` |
311
332
  | Google | Gemini / Vertex AI | `normalizeGeminiUsage` / `providerPresets.gemini` |
312
- | AWS | Bedrock Converse | `normalizeBedrockConverseUsage` / `providerPresets.bedrockConverse` |
333
+ | AWS | Bedrock Converse | `normalizeBedrockConverseUsage` / `providerPresets.bedrock` |
313
334
  | Mistral | Chat API | `normalizeMistralUsage` / `providerPresets.mistral` |
314
335
  | OpenRouter | Chat | `normalizeOpenRouterUsage` / `providerPresets.openrouter` (provider-reported cost) |
315
336
  | Vercel | AI SDK v5/v6 | `normalizeVercelAiSdkUsage` / `providerPresets.vercelAiSdk` |
@@ -325,7 +346,7 @@ The fundamental difference between "what the provider charged you" and "what you
325
346
  ```typescript
326
347
  // Configure at module level:
327
348
  BymaxAiTokensModule.forRootAsync({
328
- useFactory: (prisma) => ({
349
+ useFactory: (prisma: PrismaService) => ({
329
350
  store: new PrismaAiTokensStore(prisma),
330
351
  markup: 4.0, // 4× resale margin
331
352
  wallets: { creditRateNanoUsd: 5_000_000_000n }, // sell credits at $5 each
@@ -342,7 +363,7 @@ For per-tenant or per-feature pricing, implement `IMarkupPolicy`:
342
363
 
343
364
  ```typescript
344
365
  class MyMarkupPolicy implements IMarkupPolicy {
345
- resolve(ctx: MeteringContext): number | Promise<number> {
366
+ resolve(ctx: { feature?: string }): number | Promise<number> {
346
367
  return ctx.feature === 'premium.summarize' ? 5.0 : 3.0
347
368
  }
348
369
  }
@@ -370,8 +391,8 @@ await wallets.grant(
370
391
 
371
392
  // Credits are debited automatically when the ledger posts a billed cost.
372
393
  // Check balance:
373
- const wallet = await wallets.getOrCreate({ tenantId, ownerType: 'user', ownerId: userId })
374
- console.log(wallet.balanceNanoUsd) // current balance
394
+ const balance = await wallets.getBalance({ tenantId, ownerType: 'user', ownerId: userId })
395
+ console.log(balance.nanoUsd) // current balance
375
396
  ```
376
397
 
377
398
  ### Multi-Dimension Budgets
@@ -443,7 +464,7 @@ On abort, the collector falls back to a tokenizer estimate of the output tokens
443
464
 
444
465
  ```typescript
445
466
  // Usage summary — by feature, for the last 30 days:
446
- const summary = await metering.summarize({
467
+ const summary = await reports.summarize({
447
468
  tenantId,
448
469
  scope: { type: 'user', id: userId },
449
470
  from: thirtyDaysAgo,
@@ -452,12 +473,7 @@ const summary = await metering.summarize({
452
473
  })
453
474
 
454
475
  // Export as CSV (streaming):
455
- const csv = await reports.export({
456
- tenantId,
457
- from,
458
- to,
459
- format: 'csv',
460
- })
476
+ const csv = await reports.export({ tenantId, from, to }, 'csv')
461
477
  csv.pipe(res)
462
478
 
463
479
  // Access status / remaining budget:
@@ -495,11 +511,11 @@ All errors are `AiTokensException extends HttpException`. Use the exported `AI_T
495
511
 
496
512
  | Code | HTTP | When |
497
513
  | -------------------------------- | ---- | ------------------------------------------------------------------------------------- |
498
- | `AI_TOKENS_NOT_CONFIGURED` | 503 | Service invoked before async init completed |
514
+ | `AI_TOKENS_NOT_CONFIGURED` | 503 | Reserved; nothing raises it |
499
515
  | `AI_TOKENS_INVALID_CONFIG` | 500 | Bad markup, negative limits, missing `scopeResolver`, missing fx |
500
516
  | `AI_TOKENS_UNKNOWN_PROVIDER` | 400 | No preset/normalizer and usage is not already `NormalizedUsage` |
501
517
  | `AI_TOKENS_USAGE_MALFORMED` | 422 | Normalizer could not read required token fields |
502
- | `AI_TOKENS_PRICE_NOT_FOUND` | 422 | No effective-dated rate after 6-step resolution chain (strict mode) |
518
+ | `AI_TOKENS_PRICE_NOT_FOUND` | 422 | No effective-dated rate after the model-resolution chain (strict mode) |
503
519
  | `AI_TOKENS_FX_REQUIRED` | 500 | `currency !== 'USD'` with no `fx` resolver |
504
520
  | `AI_TOKENS_BUDGET_EXCEEDED` | 402 | A hard spend budget blocks the call |
505
521
  | `AI_TOKENS_QUOTA_EXCEEDED` | 429 | A hard token/count quota blocks the call |
@@ -522,7 +538,7 @@ import { toJsonSafe } from '@bymax-one/nest-ai-tokens'
522
538
 
523
539
  // In your controller:
524
540
  return toJsonSafe(await metering.getStatus(tenantId, scope))
525
- // bigint fields (balanceNanoUsd, billedCostNanoUsd, etc.) become decimal strings.
541
+ // bigint fields (wallet.balanceNanoUsd, budgets[].spent.nanoUsd, etc.) become decimal strings.
526
542
 
527
543
  // For display/formatting:
528
544
  import { formatNanoUsd } from '@bymax-one/nest-ai-tokens/shared'
@@ -548,6 +564,28 @@ The sections above document each of these with a runnable example. This is the i
548
564
  | `UsageReportService` | `summarize` · `export` |
549
565
  | `StreamUsageCollector` | Accumulates streamed chunks; prefers the provider's final usage over its own count |
550
566
 
567
+ `LedgerService.verifyChain(tenantId, from?, to?)` resolves to a `ChainVerification`: `{ valid: true }`
568
+ or `{ valid: false, brokenAtRecordId }`. It checks the chain that `ledger.hashChain: true` writes.
569
+
570
+ ### Decorators, helpers and catalogs
571
+
572
+ | Export | Role |
573
+ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
574
+ | `@Meter` · `@RequireBudget` · `@AiFeature(name)` | Handler metadata read by the interceptor and the guard; `@AiFeature` only tags the feature name |
575
+ | `deriveIdempotencyKey` · `toJsonSafe` · `formatNanoUsd` | Idempotency key from content, `bigint` to decimal strings for JSON, and display formatting |
576
+ | `floatUsdToNanoUsd` · `perMillion` · `computeCostNanoUsd` | The money math: the only float-to-`bigint` entry, a cost at a nano-USD-per-million rate, and a usage priced against a rate |
577
+ | `applyMarkup` · `resolveMultiplier4dp` | A markup applied in exact `bigint`, and a multiplier validated and rounded to 4 decimal places |
578
+ | `isLegalLedgerTransition` · `isLegalTransitionPatchKey` | The ledger lifecycle rules a custom store can reuse: which status changes and which columns each may touch |
579
+ | `PROVIDER_IDS` · `SERVICE_TIERS` · `AI_OPERATIONS` · `TOKEN_CATEGORIES` · `WALLET_ENTRY_TYPES` | The catalogs the types are derived from |
580
+ | `CounterValueOutOfRangeError` (`./redis`) | Thrown when a counter value leaves the safe-integer range; the budget service then falls back to the database consume |
581
+
582
+ ### Injection tokens
583
+
584
+ `BYMAX_AI_TOKENS_OPTIONS` (the resolved options) and the port tokens `BYMAX_AI_TOKENS_LEDGER_STORE`,
585
+ `_PRICING_STORE`, `_WALLET_STORE`, `_BUDGET_STORE`, `_BUDGET_COUNTER`, `_TOKENIZER`, `_TELEMETRY`,
586
+ `_EVENT_SINK`, `_CONTENT_STORE` and `_LOGGER`. The wallet and budget tokens exist only when their
587
+ options block is set (with `forRoot`; `forRootAsync` always exports them).
588
+
551
589
  ### Request-scoped enforcement
552
590
 
553
591
  | Class | Role |
@@ -672,7 +710,9 @@ There is one place text can be persisted, and it is opt-in: `IContentStore`, the
672
710
  sidecar. A host that enables it stores **masked** text under a short TTL, separate from the
673
711
  ledger, with a `purge()` that deletes by tenant, record or subject so an erasure request can
674
712
  be honoured without touching the accounting. It is off unless you provide a store — and if
675
- you do, text is being written, and where it is written is your implementation.
713
+ you do, text is being written, and where it is written is your implementation. The library ships
714
+ no `IContentStore` implementation and creates no table for one: you bring the store, its schema
715
+ and its retention job.
676
716
 
677
717
  ### Budget checks are atomic
678
718
 
@@ -713,7 +753,7 @@ that reads a tenant id from request input lets a caller bill someone else.
713
753
  ## 🧱 Tech Stack
714
754
 
715
755
  - **Runtime:** Node.js 24+
716
- - **Framework:** NestJS 11 (guard + interceptor, `ConfigurableModuleBuilder`, `Symbol()` tokens)
756
+ - **Framework:** NestJS 11 (guard + interceptor, `Symbol()` tokens)
717
757
  - **Persistence:** `@prisma/client ^6 || ^7` (peer) over PostgreSQL — both majors supported
718
758
  - **Budget acceleration:** `ioredis ^6` (peer), optional
719
759
  - **Events:** `@nestjs/event-emitter >=2` (peer), optional
@@ -421,6 +421,9 @@ function mapPriceRow(row) {
421
421
  source: row.source
422
422
  };
423
423
  }
424
+ function priceLockKey(provider, model, operation, serviceTier) {
425
+ return JSON.stringify([provider, model, operation, serviceTier]);
426
+ }
424
427
  function firstOrThrow(rows) {
425
428
  const row = rows[0];
426
429
  if (row === void 0) throw new nestAiTokens.AiTokensException("AI_TOKENS_STORE_ERROR", void 0, {});
@@ -828,7 +831,7 @@ var PrismaAiTokensStore = class {
828
831
  async upsertPrice(input) {
829
832
  const serviceTier = input.serviceTier ?? "standard";
830
833
  const effectiveFrom = input.effectiveFrom ?? /* @__PURE__ */ new Date();
831
- const key = `${input.provider}|${input.model}|${input.operation}|${serviceTier}`;
834
+ const key = priceLockKey(input.provider, input.model, input.operation, serviceTier);
832
835
  return this.prisma.$transaction(async (tx) => {
833
836
  await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${key})::bigint)`;
834
837
  await tx.$executeRaw(client.Prisma.sql`
@@ -843,6 +846,33 @@ var PrismaAiTokensStore = class {
843
846
  return mapPriceRow(firstOrThrow(rows));
844
847
  });
845
848
  }
849
+ /**
850
+ * Insert a price row only when its (provider, model, operation, serviceTier) tuple has no
851
+ * row yet. The check and the insert run in one transaction under the same per-tuple
852
+ * `pg_advisory_xact_lock` as {@link upsertPrice}, so concurrent boots serialize on the
853
+ * tuple, the loser finds the winner's row and inserts nothing, and the lock is released
854
+ * when the transaction ends rather than when the connection does.
855
+ */
856
+ async seedPrice(input) {
857
+ const serviceTier = input.serviceTier ?? "standard";
858
+ const effectiveFrom = input.effectiveFrom ?? /* @__PURE__ */ new Date();
859
+ const key = priceLockKey(input.provider, input.model, input.operation, serviceTier);
860
+ return this.prisma.$transaction(async (tx) => {
861
+ await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${key})::bigint)`;
862
+ const existing = await tx.$queryRaw(client.Prisma.sql`
863
+ SELECT "id" FROM "ai_model_prices"
864
+ WHERE "provider" = ${input.provider} AND "model" = ${input.model} AND "operation" = ${input.operation}
865
+ AND "serviceTier" = ${serviceTier}
866
+ LIMIT 1
867
+ `);
868
+ if (existing.length > 0) return false;
869
+ await tx.$queryRaw(client.Prisma.sql`
870
+ INSERT INTO "ai_model_prices" (${PRICE_COLUMNS_SQL}) VALUES (${client.Prisma.join(priceValues(input, serviceTier, effectiveFrom))})
871
+ RETURNING *
872
+ `);
873
+ return true;
874
+ });
875
+ }
846
876
  async getPriceHistory(provider, model, operation, serviceTier) {
847
877
  const tier = serviceTier === void 0 ? client.Prisma.empty : client.Prisma.sql`AND "serviceTier" = ${serviceTier}`;
848
878
  const rows = await this.prisma.$queryRaw(client.Prisma.sql`
@@ -858,7 +888,12 @@ var PrismaAiTokensStore = class {
858
888
  `);
859
889
  return rows.map((row) => ({ model: row.model, operation: row.operation, serviceTier: row.serviceTier }));
860
890
  }
861
- /** Advisory seed lock (§6.4): the first caller wins; concurrent boots seed exactly once. */
891
+ /**
892
+ * Session-scoped advisory lock: the first caller on a connection wins, and the lock is held
893
+ * until that connection closes, not when a boot ends.
894
+ *
895
+ * @deprecated Use {@link seedPrice}, which seeds atomically under a transaction-scoped lock.
896
+ */
862
897
  async acquireSeedLock(key) {
863
898
  const rows = await this.prisma.$queryRaw(
864
899
  client.Prisma.sql`SELECT pg_try_advisory_lock(hashtext(${key})::bigint) AS "acquired"`
@@ -505,6 +505,15 @@ interface IPricingStore {
505
505
  resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
506
506
  /** Close the current open row (`effectiveTo = now`) and insert the new open row. */
507
507
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
508
+ /**
509
+ * Insert `input` only when no row, open or closed, exists for its
510
+ * (provider, model, operation, serviceTier) tuple; resolves `true` when it inserted.
511
+ * Atomic per tuple, so concurrent boots cannot both insert, and it never closes or
512
+ * alters an existing row. The snapshot seed uses it so a restart cannot revert a rate
513
+ * the host set with {@link IPricingStore.upsertPrice}. A store without it is seeded
514
+ * from {@link IPricingStore.getPriceHistory} and `upsertPrice`, which is not atomic.
515
+ */
516
+ seedPrice?(input: NewPriceVersion): Promise<boolean>;
508
517
  /** Full effective-dated history for a tuple (newest first). */
509
518
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
510
519
  /** All distinct (model, operation, serviceTier) for a provider — powers §6.6 prefix matching. */
@@ -615,9 +624,22 @@ declare class PrismaAiTokensStore implements IAiTokensStore {
615
624
  lastHash(tenantId: string): Promise<string | null>;
616
625
  resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
617
626
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
627
+ /**
628
+ * Insert a price row only when its (provider, model, operation, serviceTier) tuple has no
629
+ * row yet. The check and the insert run in one transaction under the same per-tuple
630
+ * `pg_advisory_xact_lock` as {@link upsertPrice}, so concurrent boots serialize on the
631
+ * tuple, the loser finds the winner's row and inserts nothing, and the lock is released
632
+ * when the transaction ends rather than when the connection does.
633
+ */
634
+ seedPrice(input: NewPriceVersion): Promise<boolean>;
618
635
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
619
636
  listModels(provider: ProviderId): Promise<PricedModel[]>;
620
- /** Advisory seed lock (§6.4): the first caller wins; concurrent boots seed exactly once. */
637
+ /**
638
+ * Session-scoped advisory lock: the first caller on a connection wins, and the lock is held
639
+ * until that connection closes, not when a boot ends.
640
+ *
641
+ * @deprecated Use {@link seedPrice}, which seeds atomically under a transaction-scoped lock.
642
+ */
621
643
  acquireSeedLock(key: string): Promise<boolean>;
622
644
  /** Insert one usage row and return it; shared by the plain and chained paths. */
623
645
  private insert;
@@ -505,6 +505,15 @@ interface IPricingStore {
505
505
  resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
506
506
  /** Close the current open row (`effectiveTo = now`) and insert the new open row. */
507
507
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
508
+ /**
509
+ * Insert `input` only when no row, open or closed, exists for its
510
+ * (provider, model, operation, serviceTier) tuple; resolves `true` when it inserted.
511
+ * Atomic per tuple, so concurrent boots cannot both insert, and it never closes or
512
+ * alters an existing row. The snapshot seed uses it so a restart cannot revert a rate
513
+ * the host set with {@link IPricingStore.upsertPrice}. A store without it is seeded
514
+ * from {@link IPricingStore.getPriceHistory} and `upsertPrice`, which is not atomic.
515
+ */
516
+ seedPrice?(input: NewPriceVersion): Promise<boolean>;
508
517
  /** Full effective-dated history for a tuple (newest first). */
509
518
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
510
519
  /** All distinct (model, operation, serviceTier) for a provider — powers §6.6 prefix matching. */
@@ -615,9 +624,22 @@ declare class PrismaAiTokensStore implements IAiTokensStore {
615
624
  lastHash(tenantId: string): Promise<string | null>;
616
625
  resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
617
626
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
627
+ /**
628
+ * Insert a price row only when its (provider, model, operation, serviceTier) tuple has no
629
+ * row yet. The check and the insert run in one transaction under the same per-tuple
630
+ * `pg_advisory_xact_lock` as {@link upsertPrice}, so concurrent boots serialize on the
631
+ * tuple, the loser finds the winner's row and inserts nothing, and the lock is released
632
+ * when the transaction ends rather than when the connection does.
633
+ */
634
+ seedPrice(input: NewPriceVersion): Promise<boolean>;
618
635
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
619
636
  listModels(provider: ProviderId): Promise<PricedModel[]>;
620
- /** Advisory seed lock (§6.4): the first caller wins; concurrent boots seed exactly once. */
637
+ /**
638
+ * Session-scoped advisory lock: the first caller on a connection wins, and the lock is held
639
+ * until that connection closes, not when a boot ends.
640
+ *
641
+ * @deprecated Use {@link seedPrice}, which seeds atomically under a transaction-scoped lock.
642
+ */
621
643
  acquireSeedLock(key: string): Promise<boolean>;
622
644
  /** Insert one usage row and return it; shared by the plain and chained paths. */
623
645
  private insert;
@@ -419,6 +419,9 @@ function mapPriceRow(row) {
419
419
  source: row.source
420
420
  };
421
421
  }
422
+ function priceLockKey(provider, model, operation, serviceTier) {
423
+ return JSON.stringify([provider, model, operation, serviceTier]);
424
+ }
422
425
  function firstOrThrow(rows) {
423
426
  const row = rows[0];
424
427
  if (row === void 0) throw new AiTokensException("AI_TOKENS_STORE_ERROR", void 0, {});
@@ -826,7 +829,7 @@ var PrismaAiTokensStore = class {
826
829
  async upsertPrice(input) {
827
830
  const serviceTier = input.serviceTier ?? "standard";
828
831
  const effectiveFrom = input.effectiveFrom ?? /* @__PURE__ */ new Date();
829
- const key = `${input.provider}|${input.model}|${input.operation}|${serviceTier}`;
832
+ const key = priceLockKey(input.provider, input.model, input.operation, serviceTier);
830
833
  return this.prisma.$transaction(async (tx) => {
831
834
  await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${key})::bigint)`;
832
835
  await tx.$executeRaw(Prisma.sql`
@@ -841,6 +844,33 @@ var PrismaAiTokensStore = class {
841
844
  return mapPriceRow(firstOrThrow(rows));
842
845
  });
843
846
  }
847
+ /**
848
+ * Insert a price row only when its (provider, model, operation, serviceTier) tuple has no
849
+ * row yet. The check and the insert run in one transaction under the same per-tuple
850
+ * `pg_advisory_xact_lock` as {@link upsertPrice}, so concurrent boots serialize on the
851
+ * tuple, the loser finds the winner's row and inserts nothing, and the lock is released
852
+ * when the transaction ends rather than when the connection does.
853
+ */
854
+ async seedPrice(input) {
855
+ const serviceTier = input.serviceTier ?? "standard";
856
+ const effectiveFrom = input.effectiveFrom ?? /* @__PURE__ */ new Date();
857
+ const key = priceLockKey(input.provider, input.model, input.operation, serviceTier);
858
+ return this.prisma.$transaction(async (tx) => {
859
+ await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${key})::bigint)`;
860
+ const existing = await tx.$queryRaw(Prisma.sql`
861
+ SELECT "id" FROM "ai_model_prices"
862
+ WHERE "provider" = ${input.provider} AND "model" = ${input.model} AND "operation" = ${input.operation}
863
+ AND "serviceTier" = ${serviceTier}
864
+ LIMIT 1
865
+ `);
866
+ if (existing.length > 0) return false;
867
+ await tx.$queryRaw(Prisma.sql`
868
+ INSERT INTO "ai_model_prices" (${PRICE_COLUMNS_SQL}) VALUES (${Prisma.join(priceValues(input, serviceTier, effectiveFrom))})
869
+ RETURNING *
870
+ `);
871
+ return true;
872
+ });
873
+ }
844
874
  async getPriceHistory(provider, model, operation, serviceTier) {
845
875
  const tier = serviceTier === void 0 ? Prisma.empty : Prisma.sql`AND "serviceTier" = ${serviceTier}`;
846
876
  const rows = await this.prisma.$queryRaw(Prisma.sql`
@@ -856,7 +886,12 @@ var PrismaAiTokensStore = class {
856
886
  `);
857
887
  return rows.map((row) => ({ model: row.model, operation: row.operation, serviceTier: row.serviceTier }));
858
888
  }
859
- /** Advisory seed lock (§6.4): the first caller wins; concurrent boots seed exactly once. */
889
+ /**
890
+ * Session-scoped advisory lock: the first caller on a connection wins, and the lock is held
891
+ * until that connection closes, not when a boot ends.
892
+ *
893
+ * @deprecated Use {@link seedPrice}, which seeds atomically under a transaction-scoped lock.
894
+ */
860
895
  async acquireSeedLock(key) {
861
896
  const rows = await this.prisma.$queryRaw(
862
897
  Prisma.sql`SELECT pg_try_advisory_lock(hashtext(${key})::bigint) AS "acquired"`
@@ -79,6 +79,7 @@ var WALLET_ENTRY_TYPES = ["grant", "debit", "refund", "adjustment", "expiry"];
79
79
 
80
80
  // src/shared/constants/error-codes.constants.ts
81
81
  var AI_TOKENS_ERROR_CODES = {
82
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
82
83
  AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED",
83
84
  AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG",
84
85
  AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER",
@@ -1193,7 +1194,6 @@ function normalizeModelId(id) {
1193
1194
 
1194
1195
  // src/server/services/pricing.service.ts
1195
1196
  var SEED_EFFECTIVE_FROM = /* @__PURE__ */ new Date(0);
1196
- var SEED_LOCK_KEY = "ai-tokens:model-prices";
1197
1197
  exports.PricingService = class PricingService {
1198
1198
  /**
1199
1199
  * @param options The resolved module options (pricing config lives here).
@@ -1260,20 +1260,26 @@ exports.PricingService = class PricingService {
1260
1260
  getPriceHistory(provider, model, operation, serviceTier) {
1261
1261
  return this.store.getPriceHistory(provider, model, operation, serviceTier);
1262
1262
  }
1263
- /** Seed the registry from the pinned snapshot; idempotent and advisory-locked. */
1263
+ /**
1264
+ * Seed the registry from the pinned snapshot, adding only the tuples that have no row at
1265
+ * all. A tuple that already has history, including one the host re-priced with
1266
+ * `upsertPrice`, is left exactly as it is, so running this on every boot is a no-op once
1267
+ * the registry is seeded.
1268
+ */
1264
1269
  async seedFromSnapshot() {
1265
- const lockable = this.store;
1266
- if (typeof lockable.acquireSeedLock === "function") {
1267
- const acquired = await lockable.acquireSeedLock(SEED_LOCK_KEY);
1268
- if (!acquired) return;
1269
- }
1270
1270
  const { MODEL_PRICES_SEED } = await import('@bymax-one/nest-ai-tokens/prices');
1271
1271
  for (const row of MODEL_PRICES_SEED) {
1272
- await this.store.upsertPrice({ ...row, effectiveFrom: SEED_EFFECTIVE_FROM });
1272
+ const input = { ...row, effectiveFrom: SEED_EFFECTIVE_FROM };
1273
+ if (typeof this.store.seedPrice === "function") {
1274
+ await this.store.seedPrice(input);
1275
+ continue;
1276
+ }
1277
+ const history = await this.store.getPriceHistory(row.provider, row.model, row.operation, row.serviceTier);
1278
+ if (history.length === 0) await this.store.upsertPrice(input);
1273
1279
  }
1274
1280
  this.cache.clear();
1275
1281
  }
1276
- /** The §6.6 six-step model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1282
+ /** The §6.6 model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1277
1283
  async resolveUncached(provider, model, operation, serviceTier, at, baseModel) {
1278
1284
  const exact = await this.store.resolveRate(provider, model, operation, serviceTier, at);
1279
1285
  if (exact !== null) return exact;
@@ -4078,6 +4084,26 @@ function writeHeader(response, name, value) {
4078
4084
  if (typeof response.setHeader === "function") response.setHeader(name, value);
4079
4085
  else if (typeof response.header === "function") response.header(name, value);
4080
4086
  }
4087
+
4088
+ // src/server/disabled-service.ts
4089
+ var DISABLED = /* @__PURE__ */ Symbol("bymax.ai-tokens.disabled-service");
4090
+ function disabledService(service, block) {
4091
+ const fail = () => {
4092
+ throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
4093
+ reason: `${block} not configured: set options.${block} to enable ${service.name}`
4094
+ });
4095
+ };
4096
+ const standIn = Object.create(service.prototype);
4097
+ for (const name of Object.getOwnPropertyNames(service.prototype)) {
4098
+ if (name !== "constructor") Object.defineProperty(standIn, name, { value: fail, enumerable: false });
4099
+ }
4100
+ Object.defineProperty(standIn, DISABLED, { value: true, enumerable: false });
4101
+ return standIn;
4102
+ }
4103
+ function liveService(service) {
4104
+ if (service === null || service === void 0) return null;
4105
+ return service[DISABLED] === true ? null : service;
4106
+ }
4081
4107
  var REAP_BATCH_SIZE = 100;
4082
4108
  var MS_PER_SECOND = 1e3;
4083
4109
  var SCAN_FAILED_LOG_KEY = "ai_tokens.hold_reaper.scan_failed";
@@ -4208,7 +4234,7 @@ function buildServiceProviders() {
4208
4234
  return [
4209
4235
  {
4210
4236
  provide: exports.MeteringService,
4211
- useFactory: (ledger, pricing, markup, options, dispatcher, telemetrySink, wallets, budgets) => new exports.MeteringService(ledger, pricing, markup, options, createMeteringEventHooks(dispatcher), wallets, budgets, void 0, new TelemetryEmitter(telemetrySink), new ContentCapture(options.content)),
4237
+ useFactory: (ledger, pricing, markup, options, dispatcher, telemetrySink, wallets, budgets) => new exports.MeteringService(ledger, pricing, markup, options, createMeteringEventHooks(dispatcher), liveService(wallets), liveService(budgets), void 0, new TelemetryEmitter(telemetrySink), new ContentCapture(options.content)),
4212
4238
  inject: [
4213
4239
  exports.LedgerService,
4214
4240
  exports.PricingService,
@@ -4238,21 +4264,21 @@ function buildServiceProviders() {
4238
4264
  function walletServiceProvider() {
4239
4265
  return {
4240
4266
  provide: exports.WalletService,
4241
- useFactory: (options, store, dispatcher) => options.wallets.enabled ? new exports.WalletService(store, options.wallets, createWalletEventHooks(dispatcher)) : null,
4267
+ useFactory: (options, store, dispatcher) => options.wallets.enabled ? new exports.WalletService(store, options.wallets, createWalletEventHooks(dispatcher)) : disabledService(exports.WalletService, "wallets"),
4242
4268
  inject: [BYMAX_AI_TOKENS_OPTIONS, BYMAX_AI_TOKENS_WALLET_STORE, EventDispatcher]
4243
4269
  };
4244
4270
  }
4245
4271
  function budgetServiceProvider() {
4246
4272
  return {
4247
4273
  provide: exports.BudgetService,
4248
- useFactory: (options, store, ledger, dispatcher) => options.budgets.enabled ? new exports.BudgetService(store, ledger, options.budgets, void 0, createBudgetEventHooks(dispatcher)) : null,
4274
+ useFactory: (options, store, ledger, dispatcher) => options.budgets.enabled ? new exports.BudgetService(store, ledger, options.budgets, void 0, createBudgetEventHooks(dispatcher)) : disabledService(exports.BudgetService, "budgets"),
4249
4275
  inject: [BYMAX_AI_TOKENS_OPTIONS, BYMAX_AI_TOKENS_BUDGET_STORE, exports.LedgerService, EventDispatcher]
4250
4276
  };
4251
4277
  }
4252
4278
  function budgetGuardProvider() {
4253
4279
  return {
4254
4280
  provide: exports.BudgetGuard,
4255
- useFactory: (options, budgets, metering, reflector) => options.budgets.enabled ? new exports.BudgetGuard(budgets, metering, reflector, options) : null,
4281
+ useFactory: (options, budgets, metering, reflector) => options.budgets.enabled ? new exports.BudgetGuard(budgets, metering, reflector, options) : disabledService(exports.BudgetGuard, "budgets"),
4256
4282
  inject: [BYMAX_AI_TOKENS_OPTIONS, exports.BudgetService, exports.MeteringService, core.Reflector]
4257
4283
  };
4258
4284
  }
@@ -4299,7 +4325,7 @@ async function safeResolve(load) {
4299
4325
  }
4300
4326
  function buildAsyncOptionsProviders(descriptor) {
4301
4327
  if (descriptor.useFactory !== void 0) {
4302
- const factory = descriptor.useFactory;
4328
+ const factory = descriptor.useFactory.bind(descriptor);
4303
4329
  return [
4304
4330
  {
4305
4331
  provide: BYMAX_AI_TOKENS_OPTIONS,
@@ -75,6 +75,7 @@ type WalletEntryType = (typeof WALLET_ENTRY_TYPES)[number];
75
75
  */
76
76
  /** Every error code the library can raise, mapped to itself for value access. */
77
77
  declare const AI_TOKENS_ERROR_CODES: {
78
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
78
79
  readonly AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED";
79
80
  readonly AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG";
80
81
  readonly AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER";
@@ -1188,6 +1189,15 @@ interface IPricingStore {
1188
1189
  resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
1189
1190
  /** Close the current open row (`effectiveTo = now`) and insert the new open row. */
1190
1191
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
1192
+ /**
1193
+ * Insert `input` only when no row, open or closed, exists for its
1194
+ * (provider, model, operation, serviceTier) tuple; resolves `true` when it inserted.
1195
+ * Atomic per tuple, so concurrent boots cannot both insert, and it never closes or
1196
+ * alters an existing row. The snapshot seed uses it so a restart cannot revert a rate
1197
+ * the host set with {@link IPricingStore.upsertPrice}. A store without it is seeded
1198
+ * from {@link IPricingStore.getPriceHistory} and `upsertPrice`, which is not atomic.
1199
+ */
1200
+ seedPrice?(input: NewPriceVersion): Promise<boolean>;
1191
1201
  /** Full effective-dated history for a tuple (newest first). */
1192
1202
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
1193
1203
  /** All distinct (model, operation, serviceTier) for a provider — powers §6.6 prefix matching. */
@@ -1575,11 +1585,24 @@ interface BymaxAiTokensModuleOptionsFactory {
1575
1585
  }
1576
1586
  /**
1577
1587
  * 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).
1588
+ * standard NestJS async dynamic-module options shape. One of `useFactory`, `useClass`
1589
+ * or `useExisting` is required, and `useFactory` takes precedence.
1580
1590
  */
1581
1591
  interface BymaxAiTokensModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
1582
- useFactory?: (...args: unknown[]) => BymaxAiTokensModuleOptions | Promise<BymaxAiTokensModuleOptions>;
1592
+ /**
1593
+ * Factory returning the module options. Its parameters are whatever `inject` resolves to, so
1594
+ * declare them with the types you expect:
1595
+ *
1596
+ * ```ts
1597
+ * useFactory: (config: ConfigService) => ({ store: new PrismaAiTokensStore(prisma) })
1598
+ * ```
1599
+ *
1600
+ * Declared with method syntax on purpose: that makes the parameters bivariant, so a factory
1601
+ * with typed parameters is accepted where `(...args: unknown[])` would reject it. Nest resolves
1602
+ * the arguments from `inject` at run time, so no signature can make them statically safe, and
1603
+ * method syntax keeps the factory callable, which a `never[]` rest parameter would not.
1604
+ */
1605
+ useFactory?(...args: unknown[]): BymaxAiTokensModuleOptions | Promise<BymaxAiTokensModuleOptions>;
1583
1606
  inject?: readonly (string | symbol | Type<unknown>)[];
1584
1607
  useExisting?: Type<BymaxAiTokensModuleOptionsFactory>;
1585
1608
  useClass?: Type<BymaxAiTokensModuleOptionsFactory>;
@@ -1757,7 +1780,7 @@ interface ResolvedAiTokensOptions {
1757
1780
 
1758
1781
  /**
1759
1782
  * @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
1783
+ * model-resolution chain, an in-memory TTL cache, price upserts, and the
1761
1784
  * idempotent snapshot seed (§6.3/§6.4). A call is priced at the rate in effect at
1762
1785
  * its timestamp; batch/flex/priority tiers must find their own row (no silent
1763
1786
  * fallback to standard rates).
@@ -1776,7 +1799,7 @@ interface ResolveRateInput {
1776
1799
  baseModel?: string;
1777
1800
  }
1778
1801
  /**
1779
- * Effective-dated rate resolution with a six-step model-resolution chain, an
1802
+ * Effective-dated rate resolution with a model-resolution chain, an
1780
1803
  * in-memory TTL cache, price upserts, and idempotent snapshot seed. Past
1781
1804
  * records are never re-rated (point-in-time pricing). See file overview.
1782
1805
  */
@@ -1812,9 +1835,14 @@ declare class PricingService implements OnModuleInit {
1812
1835
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
1813
1836
  /** Full effective-dated history for a tuple. */
1814
1837
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
1815
- /** Seed the registry from the pinned snapshot; idempotent and advisory-locked. */
1838
+ /**
1839
+ * Seed the registry from the pinned snapshot, adding only the tuples that have no row at
1840
+ * all. A tuple that already has history, including one the host re-priced with
1841
+ * `upsertPrice`, is left exactly as it is, so running this on every boot is a no-op once
1842
+ * the registry is seeded.
1843
+ */
1816
1844
  seedFromSnapshot(): Promise<void>;
1817
- /** The §6.6 six-step model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1845
+ /** The §6.6 model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1818
1846
  private resolveUncached;
1819
1847
  /** §6.6 step 5: longest-`startsWith` match among priced models for provider+operation+tier. */
1820
1848
  private resolveByLongestPrefix;
@@ -3206,4 +3234,4 @@ declare class AiTokensException extends HttpException {
3206
3234
  constructor(code: AiTokensErrorCode, statusCode?: HttpStatus, details?: Record<string, unknown>);
3207
3235
  }
3208
3236
 
3209
- 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 };
3237
+ 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 ChainVerification, 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 };
@@ -75,6 +75,7 @@ type WalletEntryType = (typeof WALLET_ENTRY_TYPES)[number];
75
75
  */
76
76
  /** Every error code the library can raise, mapped to itself for value access. */
77
77
  declare const AI_TOKENS_ERROR_CODES: {
78
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
78
79
  readonly AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED";
79
80
  readonly AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG";
80
81
  readonly AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER";
@@ -1188,6 +1189,15 @@ interface IPricingStore {
1188
1189
  resolveRate(provider: ProviderId, model: string, operation: AiOperation, serviceTier: ServiceTier, at: Date): Promise<PriceVersion | null>;
1189
1190
  /** Close the current open row (`effectiveTo = now`) and insert the new open row. */
1190
1191
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
1192
+ /**
1193
+ * Insert `input` only when no row, open or closed, exists for its
1194
+ * (provider, model, operation, serviceTier) tuple; resolves `true` when it inserted.
1195
+ * Atomic per tuple, so concurrent boots cannot both insert, and it never closes or
1196
+ * alters an existing row. The snapshot seed uses it so a restart cannot revert a rate
1197
+ * the host set with {@link IPricingStore.upsertPrice}. A store without it is seeded
1198
+ * from {@link IPricingStore.getPriceHistory} and `upsertPrice`, which is not atomic.
1199
+ */
1200
+ seedPrice?(input: NewPriceVersion): Promise<boolean>;
1191
1201
  /** Full effective-dated history for a tuple (newest first). */
1192
1202
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
1193
1203
  /** All distinct (model, operation, serviceTier) for a provider — powers §6.6 prefix matching. */
@@ -1575,11 +1585,24 @@ interface BymaxAiTokensModuleOptionsFactory {
1575
1585
  }
1576
1586
  /**
1577
1587
  * 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).
1588
+ * standard NestJS async dynamic-module options shape. One of `useFactory`, `useClass`
1589
+ * or `useExisting` is required, and `useFactory` takes precedence.
1580
1590
  */
1581
1591
  interface BymaxAiTokensModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
1582
- useFactory?: (...args: unknown[]) => BymaxAiTokensModuleOptions | Promise<BymaxAiTokensModuleOptions>;
1592
+ /**
1593
+ * Factory returning the module options. Its parameters are whatever `inject` resolves to, so
1594
+ * declare them with the types you expect:
1595
+ *
1596
+ * ```ts
1597
+ * useFactory: (config: ConfigService) => ({ store: new PrismaAiTokensStore(prisma) })
1598
+ * ```
1599
+ *
1600
+ * Declared with method syntax on purpose: that makes the parameters bivariant, so a factory
1601
+ * with typed parameters is accepted where `(...args: unknown[])` would reject it. Nest resolves
1602
+ * the arguments from `inject` at run time, so no signature can make them statically safe, and
1603
+ * method syntax keeps the factory callable, which a `never[]` rest parameter would not.
1604
+ */
1605
+ useFactory?(...args: unknown[]): BymaxAiTokensModuleOptions | Promise<BymaxAiTokensModuleOptions>;
1583
1606
  inject?: readonly (string | symbol | Type<unknown>)[];
1584
1607
  useExisting?: Type<BymaxAiTokensModuleOptionsFactory>;
1585
1608
  useClass?: Type<BymaxAiTokensModuleOptionsFactory>;
@@ -1757,7 +1780,7 @@ interface ResolvedAiTokensOptions {
1757
1780
 
1758
1781
  /**
1759
1782
  * @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
1783
+ * model-resolution chain, an in-memory TTL cache, price upserts, and the
1761
1784
  * idempotent snapshot seed (§6.3/§6.4). A call is priced at the rate in effect at
1762
1785
  * its timestamp; batch/flex/priority tiers must find their own row (no silent
1763
1786
  * fallback to standard rates).
@@ -1776,7 +1799,7 @@ interface ResolveRateInput {
1776
1799
  baseModel?: string;
1777
1800
  }
1778
1801
  /**
1779
- * Effective-dated rate resolution with a six-step model-resolution chain, an
1802
+ * Effective-dated rate resolution with a model-resolution chain, an
1780
1803
  * in-memory TTL cache, price upserts, and idempotent snapshot seed. Past
1781
1804
  * records are never re-rated (point-in-time pricing). See file overview.
1782
1805
  */
@@ -1812,9 +1835,14 @@ declare class PricingService implements OnModuleInit {
1812
1835
  upsertPrice(input: NewPriceVersion): Promise<PriceVersion>;
1813
1836
  /** Full effective-dated history for a tuple. */
1814
1837
  getPriceHistory(provider: ProviderId, model: string, operation: AiOperation, serviceTier?: ServiceTier): Promise<PriceVersion[]>;
1815
- /** Seed the registry from the pinned snapshot; idempotent and advisory-locked. */
1838
+ /**
1839
+ * Seed the registry from the pinned snapshot, adding only the tuples that have no row at
1840
+ * all. A tuple that already has history, including one the host re-priced with
1841
+ * `upsertPrice`, is left exactly as it is, so running this on every boot is a no-op once
1842
+ * the registry is seeded.
1843
+ */
1816
1844
  seedFromSnapshot(): Promise<void>;
1817
- /** The §6.6 six-step model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1845
+ /** The §6.6 model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1818
1846
  private resolveUncached;
1819
1847
  /** §6.6 step 5: longest-`startsWith` match among priced models for provider+operation+tier. */
1820
1848
  private resolveByLongestPrefix;
@@ -3206,4 +3234,4 @@ declare class AiTokensException extends HttpException {
3206
3234
  constructor(code: AiTokensErrorCode, statusCode?: HttpStatus, details?: Record<string, unknown>);
3207
3235
  }
3208
3236
 
3209
- 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 };
3237
+ 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 ChainVerification, 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 };
@@ -77,6 +77,7 @@ var WALLET_ENTRY_TYPES = ["grant", "debit", "refund", "adjustment", "expiry"];
77
77
 
78
78
  // src/shared/constants/error-codes.constants.ts
79
79
  var AI_TOKENS_ERROR_CODES = {
80
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
80
81
  AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED",
81
82
  AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG",
82
83
  AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER",
@@ -1191,7 +1192,6 @@ function normalizeModelId(id) {
1191
1192
 
1192
1193
  // src/server/services/pricing.service.ts
1193
1194
  var SEED_EFFECTIVE_FROM = /* @__PURE__ */ new Date(0);
1194
- var SEED_LOCK_KEY = "ai-tokens:model-prices";
1195
1195
  var PricingService = class {
1196
1196
  /**
1197
1197
  * @param options The resolved module options (pricing config lives here).
@@ -1258,20 +1258,26 @@ var PricingService = class {
1258
1258
  getPriceHistory(provider, model, operation, serviceTier) {
1259
1259
  return this.store.getPriceHistory(provider, model, operation, serviceTier);
1260
1260
  }
1261
- /** Seed the registry from the pinned snapshot; idempotent and advisory-locked. */
1261
+ /**
1262
+ * Seed the registry from the pinned snapshot, adding only the tuples that have no row at
1263
+ * all. A tuple that already has history, including one the host re-priced with
1264
+ * `upsertPrice`, is left exactly as it is, so running this on every boot is a no-op once
1265
+ * the registry is seeded.
1266
+ */
1262
1267
  async seedFromSnapshot() {
1263
- const lockable = this.store;
1264
- if (typeof lockable.acquireSeedLock === "function") {
1265
- const acquired = await lockable.acquireSeedLock(SEED_LOCK_KEY);
1266
- if (!acquired) return;
1267
- }
1268
1268
  const { MODEL_PRICES_SEED } = await import('@bymax-one/nest-ai-tokens/prices');
1269
1269
  for (const row of MODEL_PRICES_SEED) {
1270
- await this.store.upsertPrice({ ...row, effectiveFrom: SEED_EFFECTIVE_FROM });
1270
+ const input = { ...row, effectiveFrom: SEED_EFFECTIVE_FROM };
1271
+ if (typeof this.store.seedPrice === "function") {
1272
+ await this.store.seedPrice(input);
1273
+ continue;
1274
+ }
1275
+ const history = await this.store.getPriceHistory(row.provider, row.model, row.operation, row.serviceTier);
1276
+ if (history.length === 0) await this.store.upsertPrice(input);
1271
1277
  }
1272
1278
  this.cache.clear();
1273
1279
  }
1274
- /** The §6.6 six-step model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1280
+ /** The §6.6 model-resolution chain (exact → baseModel → alias → normalized → prefix). */
1275
1281
  async resolveUncached(provider, model, operation, serviceTier, at, baseModel) {
1276
1282
  const exact = await this.store.resolveRate(provider, model, operation, serviceTier, at);
1277
1283
  if (exact !== null) return exact;
@@ -4076,6 +4082,26 @@ function writeHeader(response, name, value) {
4076
4082
  if (typeof response.setHeader === "function") response.setHeader(name, value);
4077
4083
  else if (typeof response.header === "function") response.header(name, value);
4078
4084
  }
4085
+
4086
+ // src/server/disabled-service.ts
4087
+ var DISABLED = /* @__PURE__ */ Symbol("bymax.ai-tokens.disabled-service");
4088
+ function disabledService(service, block) {
4089
+ const fail = () => {
4090
+ throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
4091
+ reason: `${block} not configured: set options.${block} to enable ${service.name}`
4092
+ });
4093
+ };
4094
+ const standIn = Object.create(service.prototype);
4095
+ for (const name of Object.getOwnPropertyNames(service.prototype)) {
4096
+ if (name !== "constructor") Object.defineProperty(standIn, name, { value: fail, enumerable: false });
4097
+ }
4098
+ Object.defineProperty(standIn, DISABLED, { value: true, enumerable: false });
4099
+ return standIn;
4100
+ }
4101
+ function liveService(service) {
4102
+ if (service === null || service === void 0) return null;
4103
+ return service[DISABLED] === true ? null : service;
4104
+ }
4079
4105
  var REAP_BATCH_SIZE = 100;
4080
4106
  var MS_PER_SECOND = 1e3;
4081
4107
  var SCAN_FAILED_LOG_KEY = "ai_tokens.hold_reaper.scan_failed";
@@ -4206,7 +4232,7 @@ function buildServiceProviders() {
4206
4232
  return [
4207
4233
  {
4208
4234
  provide: MeteringService,
4209
- useFactory: (ledger, pricing, markup, options, dispatcher, telemetrySink, wallets, budgets) => new MeteringService(ledger, pricing, markup, options, createMeteringEventHooks(dispatcher), wallets, budgets, void 0, new TelemetryEmitter(telemetrySink), new ContentCapture(options.content)),
4235
+ useFactory: (ledger, pricing, markup, options, dispatcher, telemetrySink, wallets, budgets) => new MeteringService(ledger, pricing, markup, options, createMeteringEventHooks(dispatcher), liveService(wallets), liveService(budgets), void 0, new TelemetryEmitter(telemetrySink), new ContentCapture(options.content)),
4210
4236
  inject: [
4211
4237
  LedgerService,
4212
4238
  PricingService,
@@ -4236,21 +4262,21 @@ function buildServiceProviders() {
4236
4262
  function walletServiceProvider() {
4237
4263
  return {
4238
4264
  provide: WalletService,
4239
- useFactory: (options, store, dispatcher) => options.wallets.enabled ? new WalletService(store, options.wallets, createWalletEventHooks(dispatcher)) : null,
4265
+ useFactory: (options, store, dispatcher) => options.wallets.enabled ? new WalletService(store, options.wallets, createWalletEventHooks(dispatcher)) : disabledService(WalletService, "wallets"),
4240
4266
  inject: [BYMAX_AI_TOKENS_OPTIONS, BYMAX_AI_TOKENS_WALLET_STORE, EventDispatcher]
4241
4267
  };
4242
4268
  }
4243
4269
  function budgetServiceProvider() {
4244
4270
  return {
4245
4271
  provide: BudgetService,
4246
- useFactory: (options, store, ledger, dispatcher) => options.budgets.enabled ? new BudgetService(store, ledger, options.budgets, void 0, createBudgetEventHooks(dispatcher)) : null,
4272
+ useFactory: (options, store, ledger, dispatcher) => options.budgets.enabled ? new BudgetService(store, ledger, options.budgets, void 0, createBudgetEventHooks(dispatcher)) : disabledService(BudgetService, "budgets"),
4247
4273
  inject: [BYMAX_AI_TOKENS_OPTIONS, BYMAX_AI_TOKENS_BUDGET_STORE, LedgerService, EventDispatcher]
4248
4274
  };
4249
4275
  }
4250
4276
  function budgetGuardProvider() {
4251
4277
  return {
4252
4278
  provide: BudgetGuard,
4253
- useFactory: (options, budgets, metering, reflector) => options.budgets.enabled ? new BudgetGuard(budgets, metering, reflector, options) : null,
4279
+ useFactory: (options, budgets, metering, reflector) => options.budgets.enabled ? new BudgetGuard(budgets, metering, reflector, options) : disabledService(BudgetGuard, "budgets"),
4254
4280
  inject: [BYMAX_AI_TOKENS_OPTIONS, BudgetService, MeteringService, Reflector]
4255
4281
  };
4256
4282
  }
@@ -4297,7 +4323,7 @@ async function safeResolve(load) {
4297
4323
  }
4298
4324
  function buildAsyncOptionsProviders(descriptor) {
4299
4325
  if (descriptor.useFactory !== void 0) {
4300
- const factory = descriptor.useFactory;
4326
+ const factory = descriptor.useFactory.bind(descriptor);
4301
4327
  return [
4302
4328
  {
4303
4329
  provide: BYMAX_AI_TOKENS_OPTIONS,
@@ -49,6 +49,7 @@ var WALLET_ENTRY_TYPES = ["grant", "debit", "refund", "adjustment", "expiry"];
49
49
 
50
50
  // src/shared/constants/error-codes.constants.ts
51
51
  var AI_TOKENS_ERROR_CODES = {
52
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
52
53
  AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED",
53
54
  AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG",
54
55
  AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER",
@@ -70,6 +70,7 @@ type WalletEntryType = (typeof WALLET_ENTRY_TYPES)[number];
70
70
  */
71
71
  /** Every error code the library can raise, mapped to itself for value access. */
72
72
  declare const AI_TOKENS_ERROR_CODES: {
73
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
73
74
  readonly AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED";
74
75
  readonly AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG";
75
76
  readonly AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER";
@@ -70,6 +70,7 @@ type WalletEntryType = (typeof WALLET_ENTRY_TYPES)[number];
70
70
  */
71
71
  /** Every error code the library can raise, mapped to itself for value access. */
72
72
  declare const AI_TOKENS_ERROR_CODES: {
73
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
73
74
  readonly AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED";
74
75
  readonly AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG";
75
76
  readonly AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER";
@@ -47,6 +47,7 @@ var WALLET_ENTRY_TYPES = ["grant", "debit", "refund", "adjustment", "expiry"];
47
47
 
48
48
  // src/shared/constants/error-codes.constants.ts
49
49
  var AI_TOKENS_ERROR_CODES = {
50
+ /** Reserved: nothing raises it. Nest resolves the async options before it builds any service. */
50
51
  AI_TOKENS_NOT_CONFIGURED: "AI_TOKENS_NOT_CONFIGURED",
51
52
  AI_TOKENS_INVALID_CONFIG: "AI_TOKENS_INVALID_CONFIG",
52
53
  AI_TOKENS_UNKNOWN_PROVIDER: "AI_TOKENS_UNKNOWN_PROVIDER",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-ai-tokens",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "AI token metering & usage-based billing for NestJS 11: provider usage normalizer (×9), versioned effective-dated pricing, immutable append-only ledger, prepaid wallets, multi-dimension budgets, and markup/margin. Zero runtime dependencies.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",