@bymax-one/nest-ai-tokens 1.1.1 → 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 +134 -4
- package/README.md +72 -32
- package/dist/prisma/index.cjs +37 -2
- package/dist/prisma/index.d.cts +23 -1
- package/dist/prisma/index.d.ts +23 -1
- package/dist/prisma/index.mjs +37 -2
- package/dist/server/index.cjs +46 -20
- package/dist/server/index.d.cts +42 -14
- package/dist/server/index.d.ts +42 -14
- package/dist/server/index.mjs +46 -20
- package/dist/shared/index.cjs +1 -0
- package/dist/shared/index.d.cts +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.mjs +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,134 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [Unreleased]
|
|
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
|
+
|
|
110
|
+
## [1.1.2] - 2026-10-30
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
|
|
114
|
+
- **`PricingService.resolveRate` no longer serves one request's price to another.** The
|
|
115
|
+
resolution cache key joined provider, model, baseModel, operation, tier and time bucket
|
|
116
|
+
with `|`, which can also appear inside a model id. Two different requests could spell the
|
|
117
|
+
same key — `model: 'b|c'` with no `baseModel` against `model: 'b'` with `baseModel: 'c|'` —
|
|
118
|
+
and the second received the first's rate from the cache. The key is now a JSON array of
|
|
119
|
+
the fields, so no value can move the boundary between them. (#117)
|
|
120
|
+
|
|
121
|
+
**Apply:** no action required. Requests whose model names contain `|` now resolve their
|
|
122
|
+
own price instead of a colliding entry's.
|
|
123
|
+
|
|
124
|
+
### Security
|
|
125
|
+
|
|
126
|
+
- Advisories on transitive **dev** packages are cleared through `pnpm-workspace.yaml`
|
|
127
|
+
overrides: body-parser, multer, qs and zod (#115). deepmerge GHSA-gcqx-w48m-w2h4 has no
|
|
128
|
+
patched release and is reached only through `jest-config`, so it is ignored in
|
|
129
|
+
`osv-scanner.toml` until 2027-01-27. None of these reach a consumer install: the package
|
|
130
|
+
has no runtime dependencies.
|
|
131
|
+
|
|
132
|
+
### Documentation
|
|
133
|
+
|
|
134
|
+
- `SECURITY.md`: only the latest minor line (1.1.x) receives security fixes; 1.0.x is
|
|
135
|
+
unsupported and consumers should upgrade.
|
|
136
|
+
|
|
9
137
|
## [1.1.1] - 2026-10-23
|
|
10
138
|
|
|
11
139
|
### Fixed
|
|
@@ -62,7 +190,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
|
|
|
62
190
|
|
|
63
191
|
## [1.0.4] - 2026-08-10
|
|
64
192
|
|
|
65
|
-
Remediation of a local audit's settlement findings (merged in #65). No API
|
|
193
|
+
Remediation of a local audit's settlement findings (merged in #65). No API changes.
|
|
66
194
|
|
|
67
195
|
### Fixed
|
|
68
196
|
|
|
@@ -174,7 +302,7 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
174
302
|
### Added
|
|
175
303
|
|
|
176
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.
|
|
177
|
-
- **Versioned effective-dated pricing** — `PricingService` with a
|
|
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.
|
|
178
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()`).
|
|
179
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.
|
|
180
308
|
- **Prepaid wallets** — `WalletService` with grant/debit/refund/adjust entries, configurable burn order (expiry/priority/FIFO), overdraft support, and debit-allocation tracking.
|
|
@@ -185,7 +313,7 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
185
313
|
- **NestJS enforcement** — `BudgetGuard` (CanActivate), `MeteringInterceptor` (NestInterceptor), and the `@Meter`, `@RequireBudget`, `@AiFeature` decorators for declarative controller-level metering.
|
|
186
314
|
- **Usage reporting** — `UsageReportService` with SQL-aggregated `summarize()`, CSV/JSON `export()`, paginated history, per-model analytics, and currency conversion.
|
|
187
315
|
- **Events** — Typed domain events (`ai_tokens.*`) delivered to `@nestjs/event-emitter` (optional peer) and/or an `IEventSink` port.
|
|
188
|
-
- **OpenTelemetry** —
|
|
316
|
+
- **OpenTelemetry** — The `ITelemetrySink` port, which you implement over `@opentelemetry/api` (optional peer); the library ships no sink and imports nothing from `@opentelemetry/api`.
|
|
189
317
|
- **PostgreSQL adapter** — `PrismaAiTokensStore` (`./prisma` subpath) implementing all four storage ports via parameterized raw SQL. Seven-table schema with migrations.
|
|
190
318
|
- **Five subpaths** — `.` (server), `./shared` (zero-dep), `./prices` (data-only), `./prisma` (adapter), `./redis` (counter).
|
|
191
319
|
- **Zero runtime dependencies** — `"dependencies": {}`. All runtime functionality via peer dependencies.
|
|
@@ -267,7 +395,9 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
267
395
|
[1.0.1]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.0...v1.0.1
|
|
268
396
|
[1.0.2]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.1...v1.0.2
|
|
269
397
|
[1.1.0]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.4...v1.1.0
|
|
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
|
|
270
400
|
[1.1.1]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.0...v1.1.1
|
|
271
401
|
[1.0.4]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.3...v1.0.4
|
|
272
402
|
[1.0.3]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.2...v1.0.3
|
|
273
|
-
[Unreleased]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.1.
|
|
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`) |
|
|
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.
|
|
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
|
|
288
|
-
| --------------------------- | ------------------------------------- |
|
|
289
|
-
| `store` | `IAiTokensStore` | — (required)
|
|
290
|
-
| `scopeResolver` | `(ctx) => MeteringContext` | —
|
|
291
|
-
| `ratingMode` | `'rate-table' \| 'provider-reported'` | `'rate-table'`
|
|
292
|
-
| `markup` | `number \| IMarkupPolicy` | `1.0`
|
|
293
|
-
| `pricing.seedFromSnapshot` | `boolean` | `
|
|
294
|
-
| `wallets.creditRateNanoUsd` | `bigint` |
|
|
295
|
-
| `wallets.burnOrder` | `'expiry' \| 'priority' \| 'fifo'` | `'expiry'`
|
|
296
|
-
| `budgets.defaultPolicy` | `'block' \| '
|
|
297
|
-
| `budgets.alertThresholds` | `number[]` | `[0.8, 1.0]`
|
|
298
|
-
| `holds.ttlSeconds` | `number` | `
|
|
299
|
-
| `telemetry`
|
|
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.
|
|
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:
|
|
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
|
|
374
|
-
console.log(
|
|
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
|
|
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 |
|
|
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
|
|
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,
|
|
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, `
|
|
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
|
package/dist/prisma/index.cjs
CHANGED
|
@@ -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 =
|
|
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
|
-
/**
|
|
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"`
|
package/dist/prisma/index.d.cts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/prisma/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/prisma/index.mjs
CHANGED
|
@@ -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 =
|
|
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
|
-
/**
|
|
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"`
|
package/dist/server/index.cjs
CHANGED
|
@@ -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).
|
|
@@ -1224,7 +1224,7 @@ exports.PricingService = class PricingService {
|
|
|
1224
1224
|
const serviceTier = input.serviceTier ?? "standard";
|
|
1225
1225
|
const operation = input.operation === "responses" ? "chat" : input.operation;
|
|
1226
1226
|
const bucket = Math.floor(input.at.getTime() / this.options.pricing.cacheTtlMs);
|
|
1227
|
-
const cacheKey =
|
|
1227
|
+
const cacheKey = JSON.stringify([input.provider, input.model, input.baseModel, operation, serviceTier, bucket]);
|
|
1228
1228
|
let resolved;
|
|
1229
1229
|
const cached = this.cache.get(cacheKey);
|
|
1230
1230
|
if (cached !== void 0 && this.now() < cached.expiresAt) {
|
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -2775,10 +2781,10 @@ exports.WalletService = class WalletService {
|
|
|
2775
2781
|
options;
|
|
2776
2782
|
events;
|
|
2777
2783
|
/**
|
|
2778
|
-
* The current balance
|
|
2779
|
-
*
|
|
2780
|
-
*
|
|
2781
|
-
*
|
|
2784
|
+
* The current balance (§9.2). Reads the materialized balance; expiry is applied
|
|
2785
|
+
* lazily on debit, so a grant that lapses purely with the passage of time is still
|
|
2786
|
+
* counted until the next debit or `reconcile` (the documented §9.3
|
|
2787
|
+
* lazy-materialization trade-off).
|
|
2782
2788
|
*
|
|
2783
2789
|
* @param ref The wallet owner (a `'tenant' | 'team' | 'user'`, never `'key'`).
|
|
2784
2790
|
* @returns The nano-USD balance and its presentation credits.
|
|
@@ -2965,7 +2971,7 @@ exports.WalletService = class WalletService {
|
|
|
2965
2971
|
* `ai_tokens.audit`.
|
|
2966
2972
|
*
|
|
2967
2973
|
* @param ref The wallet owner.
|
|
2968
|
-
* @returns The reconciled
|
|
2974
|
+
* @returns The reconciled nano-USD balance and its presentation credits.
|
|
2969
2975
|
*/
|
|
2970
2976
|
async reconcile(ref) {
|
|
2971
2977
|
this.assertOwner(ref);
|
|
@@ -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)) :
|
|
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)) :
|
|
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) :
|
|
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,
|
package/dist/server/index.d.cts
CHANGED
|
@@ -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
|
|
1579
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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;
|
|
@@ -2224,10 +2252,10 @@ declare class WalletService {
|
|
|
2224
2252
|
*/
|
|
2225
2253
|
constructor(store: IWalletStore, options: WalletServiceOptions, events?: WalletEventHooks);
|
|
2226
2254
|
/**
|
|
2227
|
-
* The current balance
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2255
|
+
* The current balance (§9.2). Reads the materialized balance; expiry is applied
|
|
2256
|
+
* lazily on debit, so a grant that lapses purely with the passage of time is still
|
|
2257
|
+
* counted until the next debit or `reconcile` (the documented §9.3
|
|
2258
|
+
* lazy-materialization trade-off).
|
|
2231
2259
|
*
|
|
2232
2260
|
* @param ref The wallet owner (a `'tenant' | 'team' | 'user'`, never `'key'`).
|
|
2233
2261
|
* @returns The nano-USD balance and its presentation credits.
|
|
@@ -2306,7 +2334,7 @@ declare class WalletService {
|
|
|
2306
2334
|
* `ai_tokens.audit`.
|
|
2307
2335
|
*
|
|
2308
2336
|
* @param ref The wallet owner.
|
|
2309
|
-
* @returns The reconciled
|
|
2337
|
+
* @returns The reconciled nano-USD balance and its presentation credits.
|
|
2310
2338
|
*/
|
|
2311
2339
|
reconcile(ref: WalletRef): Promise<WalletBalance>;
|
|
2312
2340
|
/** Append a non-debit entry, mapping a store idempotency/missing-wallet signal to the catalog. */
|
|
@@ -3119,7 +3147,7 @@ declare class MeteringInterceptor implements NestInterceptor {
|
|
|
3119
3147
|
* @fileoverview The controller-level metering/enforcement decorators (spec §11.4).
|
|
3120
3148
|
* `@Meter` marks a handler for the `MeteringInterceptor` and is metadata-only
|
|
3121
3149
|
* here; `@RequireBudget` marks it for the `BudgetGuard`; `@AiFeature`
|
|
3122
|
-
* is a lightweight feature tag. Each stores its config under an exported
|
|
3150
|
+
* is a lightweight feature tag. Each stores its config under an exported string key
|
|
3123
3151
|
* that the guard/interceptor read via the NestJS `Reflector`. Feature precedence is
|
|
3124
3152
|
* `@RequireBudget.feature` > `@Meter.feature` > `@AiFeature` (§11.4). These are pure
|
|
3125
3153
|
* metadata; they never touch a wallet, budget, or the request.
|
|
@@ -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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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
|
|
1579
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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;
|
|
@@ -2224,10 +2252,10 @@ declare class WalletService {
|
|
|
2224
2252
|
*/
|
|
2225
2253
|
constructor(store: IWalletStore, options: WalletServiceOptions, events?: WalletEventHooks);
|
|
2226
2254
|
/**
|
|
2227
|
-
* The current balance
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2255
|
+
* The current balance (§9.2). Reads the materialized balance; expiry is applied
|
|
2256
|
+
* lazily on debit, so a grant that lapses purely with the passage of time is still
|
|
2257
|
+
* counted until the next debit or `reconcile` (the documented §9.3
|
|
2258
|
+
* lazy-materialization trade-off).
|
|
2231
2259
|
*
|
|
2232
2260
|
* @param ref The wallet owner (a `'tenant' | 'team' | 'user'`, never `'key'`).
|
|
2233
2261
|
* @returns The nano-USD balance and its presentation credits.
|
|
@@ -2306,7 +2334,7 @@ declare class WalletService {
|
|
|
2306
2334
|
* `ai_tokens.audit`.
|
|
2307
2335
|
*
|
|
2308
2336
|
* @param ref The wallet owner.
|
|
2309
|
-
* @returns The reconciled
|
|
2337
|
+
* @returns The reconciled nano-USD balance and its presentation credits.
|
|
2310
2338
|
*/
|
|
2311
2339
|
reconcile(ref: WalletRef): Promise<WalletBalance>;
|
|
2312
2340
|
/** Append a non-debit entry, mapping a store idempotency/missing-wallet signal to the catalog. */
|
|
@@ -3119,7 +3147,7 @@ declare class MeteringInterceptor implements NestInterceptor {
|
|
|
3119
3147
|
* @fileoverview The controller-level metering/enforcement decorators (spec §11.4).
|
|
3120
3148
|
* `@Meter` marks a handler for the `MeteringInterceptor` and is metadata-only
|
|
3121
3149
|
* here; `@RequireBudget` marks it for the `BudgetGuard`; `@AiFeature`
|
|
3122
|
-
* is a lightweight feature tag. Each stores its config under an exported
|
|
3150
|
+
* is a lightweight feature tag. Each stores its config under an exported string key
|
|
3123
3151
|
* that the guard/interceptor read via the NestJS `Reflector`. Feature precedence is
|
|
3124
3152
|
* `@RequireBudget.feature` > `@Meter.feature` > `@AiFeature` (§11.4). These are pure
|
|
3125
3153
|
* metadata; they never touch a wallet, budget, or the request.
|
|
@@ -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 };
|
package/dist/server/index.mjs
CHANGED
|
@@ -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).
|
|
@@ -1222,7 +1222,7 @@ var PricingService = class {
|
|
|
1222
1222
|
const serviceTier = input.serviceTier ?? "standard";
|
|
1223
1223
|
const operation = input.operation === "responses" ? "chat" : input.operation;
|
|
1224
1224
|
const bucket = Math.floor(input.at.getTime() / this.options.pricing.cacheTtlMs);
|
|
1225
|
-
const cacheKey =
|
|
1225
|
+
const cacheKey = JSON.stringify([input.provider, input.model, input.baseModel, operation, serviceTier, bucket]);
|
|
1226
1226
|
let resolved;
|
|
1227
1227
|
const cached = this.cache.get(cacheKey);
|
|
1228
1228
|
if (cached !== void 0 && this.now() < cached.expiresAt) {
|
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -2773,10 +2779,10 @@ var WalletService = class {
|
|
|
2773
2779
|
options;
|
|
2774
2780
|
events;
|
|
2775
2781
|
/**
|
|
2776
|
-
* The current balance
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
2779
|
-
*
|
|
2782
|
+
* The current balance (§9.2). Reads the materialized balance; expiry is applied
|
|
2783
|
+
* lazily on debit, so a grant that lapses purely with the passage of time is still
|
|
2784
|
+
* counted until the next debit or `reconcile` (the documented §9.3
|
|
2785
|
+
* lazy-materialization trade-off).
|
|
2780
2786
|
*
|
|
2781
2787
|
* @param ref The wallet owner (a `'tenant' | 'team' | 'user'`, never `'key'`).
|
|
2782
2788
|
* @returns The nano-USD balance and its presentation credits.
|
|
@@ -2963,7 +2969,7 @@ var WalletService = class {
|
|
|
2963
2969
|
* `ai_tokens.audit`.
|
|
2964
2970
|
*
|
|
2965
2971
|
* @param ref The wallet owner.
|
|
2966
|
-
* @returns The reconciled
|
|
2972
|
+
* @returns The reconciled nano-USD balance and its presentation credits.
|
|
2967
2973
|
*/
|
|
2968
2974
|
async reconcile(ref) {
|
|
2969
2975
|
this.assertOwner(ref);
|
|
@@ -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)) :
|
|
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)) :
|
|
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) :
|
|
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,
|
package/dist/shared/index.cjs
CHANGED
|
@@ -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",
|
package/dist/shared/index.d.cts
CHANGED
|
@@ -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";
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/shared/index.mjs
CHANGED
|
@@ -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.
|
|
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",
|