@bymax-one/nest-ai-tokens 1.0.2 → 1.0.4
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 +54 -1
- package/README.md +5 -3
- package/dist/prisma/index.cjs +1 -1
- package/dist/prisma/index.mjs +1 -1
- package/dist/server/index.cjs +88 -75
- package/dist/server/index.d.cts +8 -1
- package/dist/server/index.d.ts +8 -1
- package/dist/server/index.mjs +88 -75
- package/dist/shared/index.cjs +2 -2
- package/dist/shared/index.mjs +2 -2
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,57 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.4] - 2026-08-10
|
|
12
|
+
|
|
13
|
+
Remediation of a local audit's settlement findings (merged in #65). No API changed.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Negative usage can no longer credit the wallet or budget.** `clampUsageCounts` floors every
|
|
18
|
+
token count — and the provider-reported cost (`providerReportedCostNanoUsd`) — at zero before the
|
|
19
|
+
signed settlement leg. A normalizer that computes a field by subtraction (OpenRouter's
|
|
20
|
+
`prompt - cached`), or a provider that reports a negative `usage.cost`, would otherwise credit
|
|
21
|
+
back more than was reserved and drive budget consumption negative.
|
|
22
|
+
- **Streaming captures are clamped like non-stream captures.** `finalizeCaptureUsage` now routes
|
|
23
|
+
the `StreamUsageCollector` final through `normalizeCaptureUsage`, so a provider-final stream
|
|
24
|
+
carrying negative counts or cost no longer bypasses the floor and reaches settlement raw.
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
|
|
28
|
+
- `RecordInput.occurredAt` documents its trust model: it is a deliberate backfill feature, honoured
|
|
29
|
+
as given because `record()`'s input is trusted host input, and a host that forwards a value an
|
|
30
|
+
untrusted end user controls must validate it first — a back-dated timestamp selects a historical
|
|
31
|
+
price and an earlier budget window.
|
|
32
|
+
|
|
33
|
+
## [1.0.3] - 2026-08-06
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Every suppression reason was dropped from the mutation report.** All 249 `// Stryker
|
|
38
|
+
disable` directives in the source wrote their justification after `--`. Stryker reads a
|
|
39
|
+
directive with one regular expression, `/^\s?Stryker (disable|restore)(?: (next-line))?
|
|
40
|
+
([a-zA-Z, ]+)(?::(.+)?)?/`, whose mutator list accepts letters, commas and spaces only
|
|
41
|
+
and which captures the reason exclusively after a colon. A `-` therefore closed the
|
|
42
|
+
mutator list and left the reason unmatched, so each directive still silenced its mutant
|
|
43
|
+
but the report recorded Stryker's fallback text, `Ignored using a comment`. The README
|
|
44
|
+
claimed the opposite — that every suppression carries its reason — and documented the
|
|
45
|
+
broken separator as the convention. The directives now use `<Mutator>: <reason>`, which
|
|
46
|
+
is the grammar the parser accepts, and the reasons reach the report.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `check:mutants` gate (`scripts/check-mutation-directives.mjs`) — validates every
|
|
51
|
+
`// Stryker` comment against the parser's own regular expression, rejecting a reason
|
|
52
|
+
written after `--`, a reason wrapped onto a second comment line (the report keeps only
|
|
53
|
+
the first fragment), and a mutator name Stryker does not know, which would silence
|
|
54
|
+
nothing at all. Wired into CI and `prepublishOnly`. Stryker warns about the last case,
|
|
55
|
+
but only during a mutation run, which on this repo happens post-merge.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- No runtime behaviour changed. `dist/` differs only in the text of those comments; the
|
|
60
|
+
server bundle moved from 38,096 to 38,145 B brotli, well inside its 40,000 B budget.
|
|
61
|
+
|
|
11
62
|
## [1.0.2] - 2026-08-05
|
|
12
63
|
|
|
13
64
|
### Fixed
|
|
@@ -164,4 +215,6 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
164
215
|
[1.0.0]: https://github.com/bymaxone/nest-ai-tokens/releases/tag/v1.0.0
|
|
165
216
|
[1.0.1]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.0...v1.0.1
|
|
166
217
|
[1.0.2]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.1...v1.0.2
|
|
167
|
-
[
|
|
218
|
+
[1.0.4]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.3...v1.0.4
|
|
219
|
+
[1.0.3]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.2...v1.0.3
|
|
220
|
+
[Unreleased]: https://github.com/bymaxone/nest-ai-tokens/compare/v1.0.4...HEAD
|
package/README.md
CHANGED
|
@@ -742,9 +742,11 @@ tests pass".
|
|
|
742
742
|
system does, `check:runtime` loads every subpath from the packed tarball in ESM and
|
|
743
743
|
CommonJS, and `check:published` compiles this README's snippets against `dist/`
|
|
744
744
|
- ✅ **Every suppression carries its reason** — no coverage directives anywhere; each
|
|
745
|
-
`// Stryker disable` in the production source states, after
|
|
746
|
-
silences is unobservable from a unit test (an internal error
|
|
747
|
-
reached only through integration)
|
|
745
|
+
`// Stryker disable` in the production source states, after the `:` Stryker reads it
|
|
746
|
+
from, why the mutant it silences is unobservable from a unit test (an internal error
|
|
747
|
+
context, a provider id reached only through integration). `check:mutants` proves the
|
|
748
|
+
reasons parse, so they reach the mutation report instead of being replaced by
|
|
749
|
+
Stryker's `Ignored using a comment` fallback
|
|
748
750
|
|
|
749
751
|
```bash
|
|
750
752
|
pnpm test # unit tests
|
package/dist/prisma/index.cjs
CHANGED
|
@@ -276,7 +276,7 @@ function computePayloadHash(record) {
|
|
|
276
276
|
// src/server/utils/hash-chain.ts
|
|
277
277
|
function chainHash(prevHash, record) {
|
|
278
278
|
return deriveIdempotencyKey({
|
|
279
|
-
// Stryker disable next-line StringLiteral
|
|
279
|
+
// Stryker disable next-line StringLiteral: empty-string fallback for null prevHash; tests only verify hash determinism and inequality, not the specific hash value for a genesis block
|
|
280
280
|
prevHash: prevHash ?? "",
|
|
281
281
|
id: record.id,
|
|
282
282
|
content: computePayloadHash(record)
|
package/dist/prisma/index.mjs
CHANGED
|
@@ -274,7 +274,7 @@ function computePayloadHash(record) {
|
|
|
274
274
|
// src/server/utils/hash-chain.ts
|
|
275
275
|
function chainHash(prevHash, record) {
|
|
276
276
|
return deriveIdempotencyKey({
|
|
277
|
-
// Stryker disable next-line StringLiteral
|
|
277
|
+
// Stryker disable next-line StringLiteral: empty-string fallback for null prevHash; tests only verify hash determinism and inequality, not the specific hash value for a genesis block
|
|
278
278
|
prevHash: prevHash ?? "",
|
|
279
279
|
id: record.id,
|
|
280
280
|
content: computePayloadHash(record)
|
package/dist/server/index.cjs
CHANGED
|
@@ -362,9 +362,9 @@ function normalizeMistralUsage(raw) {
|
|
|
362
362
|
provider: "mistral",
|
|
363
363
|
model: str(response.model) ?? "",
|
|
364
364
|
operation: "chat",
|
|
365
|
-
// Stryker disable next-line StringLiteral
|
|
365
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
366
366
|
inputTokens: requireNum(usage.prompt_tokens, "mistral", "usage.prompt_tokens"),
|
|
367
|
-
// Stryker disable next-line StringLiteral
|
|
367
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
368
368
|
outputTokens: requireNum(usage.completion_tokens, "mistral", "usage.completion_tokens"),
|
|
369
369
|
raw: usage
|
|
370
370
|
});
|
|
@@ -808,7 +808,7 @@ function validatePricing(options) {
|
|
|
808
808
|
if (cacheTtlMs === void 0) return;
|
|
809
809
|
if (!Number.isInteger(cacheTtlMs) || cacheTtlMs <= 0) {
|
|
810
810
|
throw invalidConfig(
|
|
811
|
-
// Stryker disable next-line StringLiteral
|
|
811
|
+
// Stryker disable next-line StringLiteral: error reason template is internal diagnostics
|
|
812
812
|
`pricing.cacheTtlMs must be a positive integer number of milliseconds, received ${String(cacheTtlMs)}`
|
|
813
813
|
);
|
|
814
814
|
}
|
|
@@ -1128,7 +1128,7 @@ exports.BudgetGuard = class BudgetGuard {
|
|
|
1128
1128
|
this.reflector = reflector;
|
|
1129
1129
|
if (options.scopeResolver === void 0) {
|
|
1130
1130
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
1131
|
-
// Stryker disable next-line StringLiteral
|
|
1131
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1132
1132
|
reason: "BudgetGuard requires options.scopeResolver to resolve the caller scope"
|
|
1133
1133
|
});
|
|
1134
1134
|
}
|
|
@@ -1359,7 +1359,7 @@ function computePayloadHash(record) {
|
|
|
1359
1359
|
// src/server/utils/hash-chain.ts
|
|
1360
1360
|
function chainHash(prevHash, record) {
|
|
1361
1361
|
return deriveIdempotencyKey({
|
|
1362
|
-
// Stryker disable next-line StringLiteral
|
|
1362
|
+
// Stryker disable next-line StringLiteral: empty-string fallback for null prevHash; tests only verify hash determinism and inequality, not the specific hash value for a genesis block
|
|
1363
1363
|
prevHash: prevHash ?? "",
|
|
1364
1364
|
id: record.id,
|
|
1365
1365
|
content: computePayloadHash(record)
|
|
@@ -1560,7 +1560,7 @@ exports.LedgerService = class LedgerService {
|
|
|
1560
1560
|
const original = await this.store.findById(recordId);
|
|
1561
1561
|
if (original?.status !== "posted") {
|
|
1562
1562
|
throw new AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
1563
|
-
// Stryker disable next-line StringLiteral
|
|
1563
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1564
1564
|
reason: original === null ? "record not found" : `cannot reverse a ${original.status} record`,
|
|
1565
1565
|
recordId,
|
|
1566
1566
|
requestedReason: reason
|
|
@@ -1581,7 +1581,7 @@ exports.LedgerService = class LedgerService {
|
|
|
1581
1581
|
assertLegalTransition(from2, to, patch) {
|
|
1582
1582
|
if (!isLegalLedgerTransition(from2, to)) {
|
|
1583
1583
|
throw new AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
1584
|
-
// Stryker disable next-line StringLiteral
|
|
1584
|
+
// Stryker disable next-line StringLiteral: error reason template is internal diagnostics
|
|
1585
1585
|
reason: `illegal transition ${from2} \u2192 ${to}`
|
|
1586
1586
|
});
|
|
1587
1587
|
}
|
|
@@ -1589,7 +1589,7 @@ exports.LedgerService = class LedgerService {
|
|
|
1589
1589
|
for (const key of Object.keys(patch)) {
|
|
1590
1590
|
if (!isLegalTransitionPatchKey(from2, to, key)) {
|
|
1591
1591
|
throw new AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
1592
|
-
// Stryker disable next-line StringLiteral
|
|
1592
|
+
// Stryker disable next-line StringLiteral: error reason template is internal diagnostics
|
|
1593
1593
|
reason: `${from2} \u2192 ${to} may not patch "${key}"`
|
|
1594
1594
|
});
|
|
1595
1595
|
}
|
|
@@ -1608,13 +1608,13 @@ function buildCompensatingRecord(original) {
|
|
|
1608
1608
|
return {
|
|
1609
1609
|
tenantId: original.tenantId,
|
|
1610
1610
|
scope: original.scope,
|
|
1611
|
-
// Stryker disable next-line ConditionalExpression
|
|
1611
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { beneficiary: undefined } is equivalent to {} because downstream consumers check !== undefined
|
|
1612
1612
|
...original.beneficiary !== void 0 ? { beneficiary: original.beneficiary } : {},
|
|
1613
|
-
// Stryker disable next-line ConditionalExpression
|
|
1613
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedBy: undefined } is equivalent to {}
|
|
1614
1614
|
...original.requestedBy !== void 0 ? { requestedBy: original.requestedBy } : {},
|
|
1615
1615
|
provider: original.provider,
|
|
1616
1616
|
model: original.model,
|
|
1617
|
-
// Stryker disable next-line ConditionalExpression
|
|
1617
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedModel: undefined } is equivalent to {}
|
|
1618
1618
|
...original.requestedModel !== void 0 ? { requestedModel: original.requestedModel } : {},
|
|
1619
1619
|
operation: original.operation,
|
|
1620
1620
|
serviceTier: original.serviceTier,
|
|
@@ -1630,7 +1630,7 @@ function buildCompensatingRecord(original) {
|
|
|
1630
1630
|
audioOutTokens: -original.audioOutTokens,
|
|
1631
1631
|
imageInTokens: -original.imageInTokens,
|
|
1632
1632
|
imageOutTokens: -original.imageOutTokens,
|
|
1633
|
-
// Stryker disable next-line ConditionalExpression
|
|
1633
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { extraUnits: undefined } is equivalent to {} because consumers check !== undefined
|
|
1634
1634
|
...original.extraUnits !== void 0 ? { extraUnits: negateUnits(original.extraUnits) } : {},
|
|
1635
1635
|
priceVersionId: original.priceVersionId,
|
|
1636
1636
|
rawCostNanoUsd: -original.rawCostNanoUsd,
|
|
@@ -1641,12 +1641,12 @@ function buildCompensatingRecord(original) {
|
|
|
1641
1641
|
priceMissing: original.priceMissing,
|
|
1642
1642
|
status: "posted",
|
|
1643
1643
|
reversesRecordId: original.id,
|
|
1644
|
-
// Stryker disable next-line ConditionalExpression
|
|
1644
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { correlationId: undefined } is equivalent to {}
|
|
1645
1645
|
...original.correlationId !== void 0 ? { correlationId: original.correlationId } : {},
|
|
1646
|
-
// Stryker disable next-line ConditionalExpression
|
|
1646
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestId: undefined } is equivalent to {}
|
|
1647
1647
|
...original.requestId !== void 0 ? { requestId: original.requestId } : {},
|
|
1648
1648
|
isSystemCost: original.isSystemCost,
|
|
1649
|
-
// Stryker disable next-line ConditionalExpression
|
|
1649
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { systemCostCategory: undefined } is equivalent to {}
|
|
1650
1650
|
...original.systemCostCategory !== void 0 ? { systemCostCategory: original.systemCostCategory } : {},
|
|
1651
1651
|
enforced: original.enforced,
|
|
1652
1652
|
occurredAt: original.occurredAt
|
|
@@ -1683,7 +1683,7 @@ var MarkupResolver = class {
|
|
|
1683
1683
|
return await policy.resolve(context);
|
|
1684
1684
|
} catch {
|
|
1685
1685
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
1686
|
-
// Stryker disable next-line StringLiteral
|
|
1686
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1687
1687
|
reason: "markup policy failed to resolve a multiplier"
|
|
1688
1688
|
});
|
|
1689
1689
|
}
|
|
@@ -1694,7 +1694,7 @@ var MarkupResolver = class {
|
|
|
1694
1694
|
return resolveMultiplier4dp(raw);
|
|
1695
1695
|
} catch {
|
|
1696
1696
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
1697
|
-
// Stryker disable next-line StringLiteral
|
|
1697
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1698
1698
|
reason: "markup policy returned an invalid multiplier",
|
|
1699
1699
|
value: raw
|
|
1700
1700
|
});
|
|
@@ -2126,6 +2126,19 @@ function tokenCountsOf(usage) {
|
|
|
2126
2126
|
function sumTokenCounts(counts) {
|
|
2127
2127
|
return TOKEN_FIELDS.reduce((total, field) => total + counts[field], 0);
|
|
2128
2128
|
}
|
|
2129
|
+
function clampUsageCounts(usage) {
|
|
2130
|
+
const counts = tokenCountsOf(usage);
|
|
2131
|
+
const clamped = zeroTokenCounts();
|
|
2132
|
+
for (const field of TOKEN_FIELDS) {
|
|
2133
|
+
clamped[field] = counts[field] < 0 ? 0 : counts[field];
|
|
2134
|
+
}
|
|
2135
|
+
const result = { ...usage, ...clamped };
|
|
2136
|
+
const cost = usage.providerReportedCostNanoUsd;
|
|
2137
|
+
if (cost !== void 0 && cost < 0n) {
|
|
2138
|
+
return { ...result, providerReportedCostNanoUsd: 0n };
|
|
2139
|
+
}
|
|
2140
|
+
return result;
|
|
2141
|
+
}
|
|
2129
2142
|
function isNormalizedUsage(usage) {
|
|
2130
2143
|
if (typeof usage !== "object" || usage === null) return false;
|
|
2131
2144
|
const candidate = usage;
|
|
@@ -2163,7 +2176,7 @@ async function resolveHoldEstimate(pricing, context, estimate, at) {
|
|
|
2163
2176
|
operation: estimate.operation,
|
|
2164
2177
|
serviceTier: tier,
|
|
2165
2178
|
at,
|
|
2166
|
-
// Stryker disable next-line ConditionalExpression
|
|
2179
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { baseModel: undefined } is equivalent to {} because resolveRate reads input.baseModel (undefined either way) — same cache key and same `baseModel !== undefined` resolution branch
|
|
2167
2180
|
...context.baseModel !== void 0 ? { baseModel: context.baseModel } : {}
|
|
2168
2181
|
});
|
|
2169
2182
|
const rawEstimateNanoUsd = rate === null ? 0n : computeCostNanoUsd(usage, rate).totalNanoUsd;
|
|
@@ -2187,7 +2200,7 @@ async function resolveHoldEstimate(pricing, context, estimate, at) {
|
|
|
2187
2200
|
async function resolveTokenEstimate(pricing, context, tokens, serviceTier, at) {
|
|
2188
2201
|
if (context.preset === void 0) {
|
|
2189
2202
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
2190
|
-
// Stryker disable next-line StringLiteral
|
|
2203
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2191
2204
|
reason: "a { tokens } hold estimate requires context.preset to identify the provider"
|
|
2192
2205
|
});
|
|
2193
2206
|
}
|
|
@@ -2202,17 +2215,17 @@ async function resolveTokenEstimate(pricing, context, tokens, serviceTier, at) {
|
|
|
2202
2215
|
return { provider, model, operation: "chat", serviceTier, rawEstimateNanoUsd, estimatedTokens: tokens, tokenCounts };
|
|
2203
2216
|
}
|
|
2204
2217
|
function normalizeCaptureUsage(usage, normalizer) {
|
|
2205
|
-
if (isNormalizedUsage(usage)) return usage;
|
|
2218
|
+
if (isNormalizedUsage(usage)) return clampUsageCounts(usage);
|
|
2206
2219
|
if (normalizer !== void 0) {
|
|
2207
2220
|
try {
|
|
2208
|
-
return normalizer(usage);
|
|
2221
|
+
return clampUsageCounts(normalizer(usage));
|
|
2209
2222
|
} catch (error) {
|
|
2210
2223
|
if (error instanceof AiTokensException) throw error;
|
|
2211
2224
|
throw new AiTokensException("AI_TOKENS_USAGE_MALFORMED", void 0, { reason: "the normalizer could not read the usage token fields" });
|
|
2212
2225
|
}
|
|
2213
2226
|
}
|
|
2214
2227
|
throw new AiTokensException("AI_TOKENS_USAGE_MALFORMED", void 0, {
|
|
2215
|
-
// Stryker disable next-line StringLiteral
|
|
2228
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2216
2229
|
reason: "capture requires an already-normalized usage or a preset normalizer"
|
|
2217
2230
|
});
|
|
2218
2231
|
}
|
|
@@ -2241,7 +2254,7 @@ function buildAppendInput(args) {
|
|
|
2241
2254
|
audioOutTokens: normalized.audioOutTokens,
|
|
2242
2255
|
imageInTokens: normalized.imageInTokens,
|
|
2243
2256
|
imageOutTokens: normalized.imageOutTokens,
|
|
2244
|
-
// Stryker disable next-line ConditionalExpression,EqualityOperator
|
|
2257
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: CE/EQ: when ratedUnits is empty, spreading { extraUnits: {} } vs omitting the key; downstream consumers check record.extraUnits !== undefined (absent vs present {}); unit tests don't use server tool use, so ratedUnits={} always → no observable difference
|
|
2245
2258
|
...Object.keys(ratedUnits).length > 0 ? { extraUnits: ratedUnits } : {},
|
|
2246
2259
|
priceVersionId: rating.priceVersionId,
|
|
2247
2260
|
rawCostNanoUsd: rating.rawCostNanoUsd,
|
|
@@ -2252,7 +2265,7 @@ function buildAppendInput(args) {
|
|
|
2252
2265
|
priceMissing: rating.priceMissing,
|
|
2253
2266
|
status: "posted",
|
|
2254
2267
|
isSystemCost: context.isSystemCost ?? false,
|
|
2255
|
-
// Stryker disable next-line ConditionalExpression
|
|
2268
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { systemCostCategory: undefined } is equivalent to {} because JS property access treats absent and undefined the same
|
|
2256
2269
|
...context.systemCostCategory !== void 0 ? { systemCostCategory: context.systemCostCategory } : {},
|
|
2257
2270
|
enforced,
|
|
2258
2271
|
occurredAt
|
|
@@ -2262,19 +2275,19 @@ function baseColumns(usage, context, serviceTier) {
|
|
|
2262
2275
|
return {
|
|
2263
2276
|
tenantId: context.tenantId,
|
|
2264
2277
|
scope: context.scope,
|
|
2265
|
-
// Stryker disable next-line ConditionalExpression
|
|
2278
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { beneficiary: undefined } is equivalent to {} for property access and truthiness-based filters
|
|
2266
2279
|
...context.beneficiary !== void 0 ? { beneficiary: context.beneficiary } : {},
|
|
2267
|
-
// Stryker disable next-line ConditionalExpression
|
|
2280
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedBy: undefined } is equivalent to {}
|
|
2268
2281
|
...context.requestedBy !== void 0 ? { requestedBy: context.requestedBy } : {},
|
|
2269
2282
|
provider: usage.provider,
|
|
2270
2283
|
model: usage.model,
|
|
2271
|
-
// Stryker disable next-line ConditionalExpression
|
|
2284
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedModel: undefined } is equivalent to {} for downstream consumers
|
|
2272
2285
|
...context.baseModel !== void 0 ? { requestedModel: context.baseModel } : {},
|
|
2273
2286
|
operation: usage.operation,
|
|
2274
2287
|
serviceTier,
|
|
2275
2288
|
feature: context.feature,
|
|
2276
2289
|
tags: context.tags ?? [],
|
|
2277
|
-
// Stryker disable next-line ConditionalExpression
|
|
2290
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { correlationId: undefined } is equivalent to {} because it's only read via record.correlationId
|
|
2278
2291
|
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {}
|
|
2279
2292
|
};
|
|
2280
2293
|
}
|
|
@@ -2336,7 +2349,7 @@ exports.MeteringService = class MeteringService {
|
|
|
2336
2349
|
const enforcing = context.enforce === true;
|
|
2337
2350
|
if (enforcing && this.wallets === void 0 && this.budgets === void 0) {
|
|
2338
2351
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
2339
|
-
// Stryker disable next-line StringLiteral
|
|
2352
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2340
2353
|
reason: "enforce requires the wallets or budgets feature to be enabled"
|
|
2341
2354
|
});
|
|
2342
2355
|
}
|
|
@@ -2391,7 +2404,7 @@ exports.MeteringService = class MeteringService {
|
|
|
2391
2404
|
model: input.model,
|
|
2392
2405
|
operation: input.operation,
|
|
2393
2406
|
serviceTier,
|
|
2394
|
-
// Stryker disable next-line ConditionalExpression
|
|
2407
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { feature: undefined } is equivalent to {} because markup.resolve treats absent and undefined feature the same
|
|
2395
2408
|
...input.feature !== void 0 ? { feature: input.feature } : {}
|
|
2396
2409
|
});
|
|
2397
2410
|
return { rawCostNanoUsd, billedCostNanoUsd: markup.apply(rawCostNanoUsd) };
|
|
@@ -2593,7 +2606,7 @@ exports.MeteringService = class MeteringService {
|
|
|
2593
2606
|
priceMissing: false,
|
|
2594
2607
|
status: "pending",
|
|
2595
2608
|
isSystemCost,
|
|
2596
|
-
// Stryker disable next-line ConditionalExpression
|
|
2609
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { systemCostCategory: undefined } is equivalent to {}
|
|
2597
2610
|
...context.systemCostCategory !== void 0 ? { systemCostCategory: context.systemCostCategory } : {},
|
|
2598
2611
|
enforced: !isSystemCost,
|
|
2599
2612
|
occurredAt
|
|
@@ -2659,9 +2672,9 @@ exports.MeteringService = class MeteringService {
|
|
|
2659
2672
|
if (usage instanceof StreamUsageCollector) {
|
|
2660
2673
|
const finalized = await usage.finalize();
|
|
2661
2674
|
if (usage.usedFallback && finalized.inputTokens === 0 && hold.estimatedTokens > 0) {
|
|
2662
|
-
return { ...finalized, inputTokens: hold.estimatedTokens };
|
|
2675
|
+
return normalizeCaptureUsage({ ...finalized, inputTokens: hold.estimatedTokens });
|
|
2663
2676
|
}
|
|
2664
|
-
return finalized;
|
|
2677
|
+
return normalizeCaptureUsage(finalized);
|
|
2665
2678
|
}
|
|
2666
2679
|
return normalizeCaptureUsage(usage, preset2?.normalizer);
|
|
2667
2680
|
}
|
|
@@ -2691,7 +2704,7 @@ exports.MeteringService = class MeteringService {
|
|
|
2691
2704
|
if (normalizer !== void 0) return this.normalize(normalizer, input.usage);
|
|
2692
2705
|
if (isNormalizedUsage(input.usage)) return input.usage;
|
|
2693
2706
|
throw new AiTokensException("AI_TOKENS_UNKNOWN_PROVIDER", void 0, {
|
|
2694
|
-
// Stryker disable next-line StringLiteral
|
|
2707
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2695
2708
|
reason: "raw usage requires a preset or normalizer, or an already-normalized usage"
|
|
2696
2709
|
});
|
|
2697
2710
|
}
|
|
@@ -2794,7 +2807,7 @@ exports.WalletService = class WalletService {
|
|
|
2794
2807
|
amountNanoUsd: input.amountNanoUsd,
|
|
2795
2808
|
priority: input.priority ?? 0,
|
|
2796
2809
|
effectiveAt: input.effectiveAt ?? /* @__PURE__ */ new Date(),
|
|
2797
|
-
// Stryker disable next-line ConditionalExpression
|
|
2810
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { expiresAt: undefined } is equivalent to {} because InMemoryWalletStore and consumers check expiresAt !== undefined
|
|
2798
2811
|
...input.expiresAt !== void 0 ? { expiresAt: input.expiresAt } : {},
|
|
2799
2812
|
idempotencyKey: input.idempotencyKey,
|
|
2800
2813
|
reason: input.reason
|
|
@@ -2803,7 +2816,7 @@ exports.WalletService = class WalletService {
|
|
|
2803
2816
|
walletId: entry.walletId,
|
|
2804
2817
|
entryId: entry.id,
|
|
2805
2818
|
amountNanoUsd: entry.amountNanoUsd,
|
|
2806
|
-
// Stryker disable next-line ConditionalExpression
|
|
2819
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { expiresAt: undefined } is equivalent to {} because event consumers check expiresAt !== undefined
|
|
2807
2820
|
...entry.expiresAt !== void 0 ? { expiresAt: entry.expiresAt } : {}
|
|
2808
2821
|
};
|
|
2809
2822
|
await this.events.granted(ref, granted);
|
|
@@ -2834,16 +2847,16 @@ exports.WalletService = class WalletService {
|
|
|
2834
2847
|
throw this.invalid("a debit without a usageRecordId must carry a reason");
|
|
2835
2848
|
}
|
|
2836
2849
|
const entry = {
|
|
2837
|
-
// Stryker disable next-line StringLiteral
|
|
2850
|
+
// Stryker disable next-line StringLiteral: placeholder walletId; the store's insert always assigns the real wallet id and never reads this sentinel, so any string here is equivalent
|
|
2838
2851
|
walletId: "",
|
|
2839
2852
|
type: "debit",
|
|
2840
2853
|
amountNanoUsd: -input.amountNanoUsd,
|
|
2841
2854
|
priority: 0,
|
|
2842
2855
|
effectiveAt: /* @__PURE__ */ new Date(),
|
|
2843
|
-
// Stryker disable next-line ConditionalExpression
|
|
2856
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2844
2857
|
...input.usageRecordId !== void 0 ? { usageRecordId: input.usageRecordId } : {},
|
|
2845
2858
|
idempotencyKey: input.idempotencyKey,
|
|
2846
|
-
// Stryker disable next-line ConditionalExpression
|
|
2859
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { reason: undefined } is equivalent to {} because store and consumers check reason !== undefined
|
|
2847
2860
|
...input.reason !== void 0 ? { reason: input.reason } : {}
|
|
2848
2861
|
};
|
|
2849
2862
|
const debited = await this.conditionalDebit(ref, entry, input.amountNanoUsd);
|
|
@@ -2870,7 +2883,7 @@ exports.WalletService = class WalletService {
|
|
|
2870
2883
|
amountNanoUsd: input.amountNanoUsd,
|
|
2871
2884
|
priority: 0,
|
|
2872
2885
|
effectiveAt: /* @__PURE__ */ new Date(),
|
|
2873
|
-
// Stryker disable next-line ConditionalExpression,ObjectLiteral
|
|
2886
|
+
// Stryker disable next-line ConditionalExpression,ObjectLiteral: CE true / OL {}: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2874
2887
|
...input.usageRecordId !== void 0 ? { usageRecordId: input.usageRecordId } : {},
|
|
2875
2888
|
idempotencyKey: input.idempotencyKey,
|
|
2876
2889
|
reason: input.reason
|
|
@@ -2927,8 +2940,8 @@ exports.WalletService = class WalletService {
|
|
|
2927
2940
|
amountNanoUsd: input.amountNanoUsd,
|
|
2928
2941
|
priority: 0,
|
|
2929
2942
|
effectiveAt: /* @__PURE__ */ new Date(),
|
|
2930
|
-
// Stryker disable next-line ConditionalExpression
|
|
2931
|
-
// Stryker disable next-line ConditionalExpression
|
|
2943
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2944
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2932
2945
|
...input.usageRecordId !== void 0 ? { usageRecordId: input.usageRecordId } : {},
|
|
2933
2946
|
idempotencyKey: input.idempotencyKey,
|
|
2934
2947
|
reason: input.reason
|
|
@@ -3560,11 +3573,11 @@ function featureMatches3(features, feature) {
|
|
|
3560
3573
|
}
|
|
3561
3574
|
function limitsOf(budget) {
|
|
3562
3575
|
return {
|
|
3563
|
-
// Stryker disable next-line ConditionalExpression
|
|
3576
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { nanoUsd: undefined } is equivalent to {} because all consumers check limits.nanoUsd !== undefined
|
|
3564
3577
|
...budget.limitNanoUsd !== void 0 ? { nanoUsd: budget.limitNanoUsd } : {},
|
|
3565
|
-
// Stryker disable next-line ConditionalExpression
|
|
3578
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tokens: undefined } is equivalent to {} because all consumers check limits.tokens !== undefined
|
|
3566
3579
|
...budget.limitTokens !== void 0 ? { tokens: budget.limitTokens } : {},
|
|
3567
|
-
// Stryker disable next-line ConditionalExpression
|
|
3580
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { count: undefined } is equivalent to {} because all consumers check limits.count !== undefined
|
|
3568
3581
|
...budget.limitCount !== void 0 ? { count: budget.limitCount } : {}
|
|
3569
3582
|
};
|
|
3570
3583
|
}
|
|
@@ -3604,11 +3617,11 @@ function ratio(spent, limit) {
|
|
|
3604
3617
|
}
|
|
3605
3618
|
function limitSnapshot(limits) {
|
|
3606
3619
|
return {
|
|
3607
|
-
// Stryker disable next-line ConditionalExpression
|
|
3620
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { nanoUsd: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3608
3621
|
...limits.nanoUsd !== void 0 ? { nanoUsd: limits.nanoUsd } : {},
|
|
3609
|
-
// Stryker disable next-line ConditionalExpression
|
|
3622
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tokens: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3610
3623
|
...limits.tokens !== void 0 ? { tokens: limits.tokens } : {},
|
|
3611
|
-
// Stryker disable next-line ConditionalExpression
|
|
3624
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { count: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3612
3625
|
...limits.count !== void 0 ? { count: limits.count } : {}
|
|
3613
3626
|
};
|
|
3614
3627
|
}
|
|
@@ -3617,11 +3630,11 @@ function spendSnapshot(spend) {
|
|
|
3617
3630
|
}
|
|
3618
3631
|
function remainingSnapshot(limits, spend) {
|
|
3619
3632
|
return {
|
|
3620
|
-
// Stryker disable next-line ConditionalExpression
|
|
3633
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { nanoUsd: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3621
3634
|
...limits.nanoUsd !== void 0 ? { nanoUsd: max0(limits.nanoUsd - spend.spentNanoUsd) } : {},
|
|
3622
|
-
// Stryker disable next-line ConditionalExpression
|
|
3635
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tokens: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3623
3636
|
...limits.tokens !== void 0 ? { tokens: Math.max(0, limits.tokens - spend.spentTokens) } : {},
|
|
3624
|
-
// Stryker disable next-line ConditionalExpression
|
|
3637
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { count: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3625
3638
|
...limits.count !== void 0 ? { count: Math.max(0, limits.count - spend.spentCount) } : {}
|
|
3626
3639
|
};
|
|
3627
3640
|
}
|
|
@@ -3629,7 +3642,7 @@ function buildStatus(budget, windowStart, resetsAt, spend) {
|
|
|
3629
3642
|
const limits = limitsOf(budget);
|
|
3630
3643
|
return {
|
|
3631
3644
|
budgetId: budget.id,
|
|
3632
|
-
// Stryker disable next-line ConditionalExpression
|
|
3645
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { features: undefined } is equivalent to {} because status consumers check features !== undefined
|
|
3633
3646
|
...budget.features !== void 0 ? { features: budget.features } : {},
|
|
3634
3647
|
window: budget.window,
|
|
3635
3648
|
windowStart,
|
|
@@ -3796,29 +3809,29 @@ exports.UsageReportService = __decorateClass([
|
|
|
3796
3809
|
function toLedgerFilter(filter, limit) {
|
|
3797
3810
|
return {
|
|
3798
3811
|
tenantId: filter.tenantId,
|
|
3799
|
-
// Stryker disable next-line ConditionalExpression
|
|
3812
|
+
// Stryker disable next-line ConditionalExpression: spreading { scope: undefined } is equivalent to {} because InMemoryLedgerStore uses truthiness; the 'false' variant is covered by the scope isolation test
|
|
3800
3813
|
...filter.scope !== void 0 ? { scope: filter.scope } : {},
|
|
3801
|
-
// Stryker disable next-line ConditionalExpression
|
|
3814
|
+
// Stryker disable next-line ConditionalExpression: spreading { beneficiary: undefined } is equivalent to {} for the same reason
|
|
3802
3815
|
...filter.beneficiary !== void 0 ? { beneficiary: filter.beneficiary } : {},
|
|
3803
|
-
// Stryker disable next-line ConditionalExpression
|
|
3816
|
+
// Stryker disable next-line ConditionalExpression: spreading { feature: undefined } does not change the !== undefined check in matches()
|
|
3804
3817
|
...filter.feature !== void 0 ? { feature: filter.feature } : {},
|
|
3805
|
-
// Stryker disable next-line ConditionalExpression
|
|
3818
|
+
// Stryker disable next-line ConditionalExpression: spreading { features: undefined } does not change the truthiness check in matches()
|
|
3806
3819
|
...filter.features !== void 0 ? { features: filter.features } : {},
|
|
3807
|
-
// Stryker disable next-line ConditionalExpression
|
|
3820
|
+
// Stryker disable next-line ConditionalExpression: spreading { provider: undefined } does not change the !== undefined check in matches()
|
|
3808
3821
|
...filter.provider !== void 0 ? { provider: filter.provider } : {},
|
|
3809
|
-
// Stryker disable next-line ConditionalExpression
|
|
3822
|
+
// Stryker disable next-line ConditionalExpression: spreading { model: undefined } does not change the !== undefined check in matches()
|
|
3810
3823
|
...filter.model !== void 0 ? { model: filter.model } : {},
|
|
3811
|
-
// Stryker disable next-line ConditionalExpression
|
|
3824
|
+
// Stryker disable next-line ConditionalExpression: spreading { operation: undefined } does not change the !== undefined check in matches()
|
|
3812
3825
|
...filter.operation !== void 0 ? { operation: filter.operation } : {},
|
|
3813
|
-
// Stryker disable next-line ConditionalExpression
|
|
3826
|
+
// Stryker disable next-line ConditionalExpression: spreading { serviceTier: undefined } does not change the !== undefined check in matches()
|
|
3814
3827
|
...filter.serviceTier !== void 0 ? { serviceTier: filter.serviceTier } : {},
|
|
3815
|
-
// Stryker disable next-line ConditionalExpression
|
|
3828
|
+
// Stryker disable next-line ConditionalExpression: spreading { tags: undefined } does not change the truthiness check in matches()
|
|
3816
3829
|
...filter.tags !== void 0 ? { tags: filter.tags } : {},
|
|
3817
|
-
// Stryker disable next-line ConditionalExpression
|
|
3830
|
+
// Stryker disable next-line ConditionalExpression: spreading { isSystemCost: undefined } does not change the !== undefined check in matches()
|
|
3818
3831
|
...filter.isSystemCost !== void 0 ? { isSystemCost: filter.isSystemCost } : {},
|
|
3819
|
-
// Stryker disable next-line ConditionalExpression
|
|
3832
|
+
// Stryker disable next-line ConditionalExpression: spreading { systemCostCategory: undefined } does not change the !== undefined check in matches()
|
|
3820
3833
|
...filter.systemCostCategory !== void 0 ? { systemCostCategory: filter.systemCostCategory } : {},
|
|
3821
|
-
// Stryker disable next-line ConditionalExpression
|
|
3834
|
+
// Stryker disable next-line ConditionalExpression: spreading { enforcedOnly: undefined } does not change the === true check in matches()
|
|
3822
3835
|
...filter.enforcedOnly !== void 0 ? { enforcedOnly: filter.enforcedOnly } : {},
|
|
3823
3836
|
status: filter.status ?? [...BALANCE_STATUSES2],
|
|
3824
3837
|
from: filter.from,
|
|
@@ -4004,9 +4017,9 @@ exports.MeteringInterceptor = class MeteringInterceptor {
|
|
|
4004
4017
|
const context = await this.resolveContext(executionContext, config, enrichment);
|
|
4005
4018
|
return this.metering.record({
|
|
4006
4019
|
usage,
|
|
4007
|
-
// Stryker disable next-line ConditionalExpression
|
|
4020
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { preset: undefined } is equivalent to {} because MeteringService checks preset !== undefined
|
|
4008
4021
|
...config.preset !== void 0 ? { preset: config.preset } : {},
|
|
4009
|
-
// Stryker disable next-line ConditionalExpression
|
|
4022
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tags: undefined } is equivalent to {} because MeteringService checks tags !== undefined
|
|
4010
4023
|
context: { ...context, enforce: true, isSystemCost: config.isSystemCost ?? false, ...config.tags !== void 0 ? { tags: config.tags } : {} }
|
|
4011
4024
|
});
|
|
4012
4025
|
}
|
|
@@ -4334,24 +4347,24 @@ function preset(provider, normalizer, ratingMode) {
|
|
|
4334
4347
|
return { provider, normalizer, ratingMode };
|
|
4335
4348
|
}
|
|
4336
4349
|
var providerPresets = {
|
|
4337
|
-
// Stryker disable next-line StringLiteral
|
|
4350
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests that exercise the full preset→MeteringService→UsageRecord path
|
|
4338
4351
|
openaiChat: preset("openai", normalizeOpenAiChatUsage, "rate-table"),
|
|
4339
|
-
// Stryker disable next-line StringLiteral
|
|
4352
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4340
4353
|
openaiResponses: preset("openai", normalizeOpenAiResponsesUsage, "rate-table"),
|
|
4341
|
-
// Stryker disable next-line StringLiteral
|
|
4354
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4342
4355
|
azureOpenai: preset("azure-openai", normalizeOpenAiChatUsage, "rate-table"),
|
|
4343
|
-
// Stryker disable next-line StringLiteral
|
|
4356
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4344
4357
|
anthropic: preset("anthropic", normalizeAnthropicUsage, "rate-table"),
|
|
4345
|
-
// Stryker disable next-line StringLiteral
|
|
4358
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4346
4359
|
gemini: preset("gemini", normalizeGeminiUsage, "rate-table"),
|
|
4347
|
-
// Stryker disable next-line StringLiteral
|
|
4360
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4348
4361
|
vertex: preset("vertex", normalizeGeminiUsage, "rate-table"),
|
|
4349
|
-
// Stryker disable next-line StringLiteral
|
|
4362
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4350
4363
|
bedrock: preset("bedrock", normalizeBedrockConverseUsage, "rate-table"),
|
|
4351
|
-
// Stryker disable next-line StringLiteral
|
|
4364
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4352
4365
|
mistral: preset("mistral", normalizeMistralUsage, "rate-table"),
|
|
4353
4366
|
vercelAiSdk: preset("openai", normalizeVercelAiSdkUsage, "rate-table"),
|
|
4354
|
-
// Stryker disable next-line StringLiteral
|
|
4367
|
+
// Stryker disable next-line StringLiteral: provider ID and rating mode; only observable via integration tests
|
|
4355
4368
|
openrouter: preset("openrouter", normalizeOpenRouterUsage, "provider-reported"),
|
|
4356
4369
|
/** A custom OpenAI-compatible provider (DeepSeek, xAI, Groq, …). */
|
|
4357
4370
|
openaiCompatible: (id) => preset(id, normalizeOpenAiCompatibleUsage, "rate-table")
|
package/dist/server/index.d.cts
CHANGED
|
@@ -2553,7 +2553,14 @@ interface RecordInput {
|
|
|
2553
2553
|
normalizer?: UsageNormalizer;
|
|
2554
2554
|
/** The per-call metering context (trusted input from the host's auth layer). */
|
|
2555
2555
|
context: MeteringContext;
|
|
2556
|
-
/**
|
|
2556
|
+
/**
|
|
2557
|
+
* When the call happened; defaults to now. Backfills deliberately pass the original time so a
|
|
2558
|
+
* usage record imported late still lands in the price version and budget window in effect when
|
|
2559
|
+
* it actually occurred. Because that timestamp selects both, it is honoured as given — it is
|
|
2560
|
+
* trusted host input, like the rest of {@link MeteringContext}. A host that forwards a value an
|
|
2561
|
+
* untrusted end user controls must validate it first, since a back-dated `occurredAt` would pick
|
|
2562
|
+
* a historical price and an earlier budget window.
|
|
2563
|
+
*/
|
|
2557
2564
|
occurredAt?: Date;
|
|
2558
2565
|
/**
|
|
2559
2566
|
* The ONLY path prompt/completion text enters the library (spec §14.2): masked +
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2553,7 +2553,14 @@ interface RecordInput {
|
|
|
2553
2553
|
normalizer?: UsageNormalizer;
|
|
2554
2554
|
/** The per-call metering context (trusted input from the host's auth layer). */
|
|
2555
2555
|
context: MeteringContext;
|
|
2556
|
-
/**
|
|
2556
|
+
/**
|
|
2557
|
+
* When the call happened; defaults to now. Backfills deliberately pass the original time so a
|
|
2558
|
+
* usage record imported late still lands in the price version and budget window in effect when
|
|
2559
|
+
* it actually occurred. Because that timestamp selects both, it is honoured as given — it is
|
|
2560
|
+
* trusted host input, like the rest of {@link MeteringContext}. A host that forwards a value an
|
|
2561
|
+
* untrusted end user controls must validate it first, since a back-dated `occurredAt` would pick
|
|
2562
|
+
* a historical price and an earlier budget window.
|
|
2563
|
+
*/
|
|
2557
2564
|
occurredAt?: Date;
|
|
2558
2565
|
/**
|
|
2559
2566
|
* The ONLY path prompt/completion text enters the library (spec §14.2): masked +
|
package/dist/server/index.mjs
CHANGED
|
@@ -360,9 +360,9 @@ function normalizeMistralUsage(raw) {
|
|
|
360
360
|
provider: "mistral",
|
|
361
361
|
model: str(response.model) ?? "",
|
|
362
362
|
operation: "chat",
|
|
363
|
-
// Stryker disable next-line StringLiteral
|
|
363
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
364
364
|
inputTokens: requireNum(usage.prompt_tokens, "mistral", "usage.prompt_tokens"),
|
|
365
|
-
// Stryker disable next-line StringLiteral
|
|
365
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
366
366
|
outputTokens: requireNum(usage.completion_tokens, "mistral", "usage.completion_tokens"),
|
|
367
367
|
raw: usage
|
|
368
368
|
});
|
|
@@ -806,7 +806,7 @@ function validatePricing(options) {
|
|
|
806
806
|
if (cacheTtlMs === void 0) return;
|
|
807
807
|
if (!Number.isInteger(cacheTtlMs) || cacheTtlMs <= 0) {
|
|
808
808
|
throw invalidConfig(
|
|
809
|
-
// Stryker disable next-line StringLiteral
|
|
809
|
+
// Stryker disable next-line StringLiteral: error reason template is internal diagnostics
|
|
810
810
|
`pricing.cacheTtlMs must be a positive integer number of milliseconds, received ${String(cacheTtlMs)}`
|
|
811
811
|
);
|
|
812
812
|
}
|
|
@@ -1126,7 +1126,7 @@ var BudgetGuard = class {
|
|
|
1126
1126
|
this.reflector = reflector;
|
|
1127
1127
|
if (options.scopeResolver === void 0) {
|
|
1128
1128
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
1129
|
-
// Stryker disable next-line StringLiteral
|
|
1129
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1130
1130
|
reason: "BudgetGuard requires options.scopeResolver to resolve the caller scope"
|
|
1131
1131
|
});
|
|
1132
1132
|
}
|
|
@@ -1357,7 +1357,7 @@ function computePayloadHash(record) {
|
|
|
1357
1357
|
// src/server/utils/hash-chain.ts
|
|
1358
1358
|
function chainHash(prevHash, record) {
|
|
1359
1359
|
return deriveIdempotencyKey({
|
|
1360
|
-
// Stryker disable next-line StringLiteral
|
|
1360
|
+
// Stryker disable next-line StringLiteral: empty-string fallback for null prevHash; tests only verify hash determinism and inequality, not the specific hash value for a genesis block
|
|
1361
1361
|
prevHash: prevHash ?? "",
|
|
1362
1362
|
id: record.id,
|
|
1363
1363
|
content: computePayloadHash(record)
|
|
@@ -1558,7 +1558,7 @@ var LedgerService = class {
|
|
|
1558
1558
|
const original = await this.store.findById(recordId);
|
|
1559
1559
|
if (original?.status !== "posted") {
|
|
1560
1560
|
throw new AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
1561
|
-
// Stryker disable next-line StringLiteral
|
|
1561
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1562
1562
|
reason: original === null ? "record not found" : `cannot reverse a ${original.status} record`,
|
|
1563
1563
|
recordId,
|
|
1564
1564
|
requestedReason: reason
|
|
@@ -1579,7 +1579,7 @@ var LedgerService = class {
|
|
|
1579
1579
|
assertLegalTransition(from2, to, patch) {
|
|
1580
1580
|
if (!isLegalLedgerTransition(from2, to)) {
|
|
1581
1581
|
throw new AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
1582
|
-
// Stryker disable next-line StringLiteral
|
|
1582
|
+
// Stryker disable next-line StringLiteral: error reason template is internal diagnostics
|
|
1583
1583
|
reason: `illegal transition ${from2} \u2192 ${to}`
|
|
1584
1584
|
});
|
|
1585
1585
|
}
|
|
@@ -1587,7 +1587,7 @@ var LedgerService = class {
|
|
|
1587
1587
|
for (const key of Object.keys(patch)) {
|
|
1588
1588
|
if (!isLegalTransitionPatchKey(from2, to, key)) {
|
|
1589
1589
|
throw new AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
1590
|
-
// Stryker disable next-line StringLiteral
|
|
1590
|
+
// Stryker disable next-line StringLiteral: error reason template is internal diagnostics
|
|
1591
1591
|
reason: `${from2} \u2192 ${to} may not patch "${key}"`
|
|
1592
1592
|
});
|
|
1593
1593
|
}
|
|
@@ -1606,13 +1606,13 @@ function buildCompensatingRecord(original) {
|
|
|
1606
1606
|
return {
|
|
1607
1607
|
tenantId: original.tenantId,
|
|
1608
1608
|
scope: original.scope,
|
|
1609
|
-
// Stryker disable next-line ConditionalExpression
|
|
1609
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { beneficiary: undefined } is equivalent to {} because downstream consumers check !== undefined
|
|
1610
1610
|
...original.beneficiary !== void 0 ? { beneficiary: original.beneficiary } : {},
|
|
1611
|
-
// Stryker disable next-line ConditionalExpression
|
|
1611
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedBy: undefined } is equivalent to {}
|
|
1612
1612
|
...original.requestedBy !== void 0 ? { requestedBy: original.requestedBy } : {},
|
|
1613
1613
|
provider: original.provider,
|
|
1614
1614
|
model: original.model,
|
|
1615
|
-
// Stryker disable next-line ConditionalExpression
|
|
1615
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedModel: undefined } is equivalent to {}
|
|
1616
1616
|
...original.requestedModel !== void 0 ? { requestedModel: original.requestedModel } : {},
|
|
1617
1617
|
operation: original.operation,
|
|
1618
1618
|
serviceTier: original.serviceTier,
|
|
@@ -1628,7 +1628,7 @@ function buildCompensatingRecord(original) {
|
|
|
1628
1628
|
audioOutTokens: -original.audioOutTokens,
|
|
1629
1629
|
imageInTokens: -original.imageInTokens,
|
|
1630
1630
|
imageOutTokens: -original.imageOutTokens,
|
|
1631
|
-
// Stryker disable next-line ConditionalExpression
|
|
1631
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { extraUnits: undefined } is equivalent to {} because consumers check !== undefined
|
|
1632
1632
|
...original.extraUnits !== void 0 ? { extraUnits: negateUnits(original.extraUnits) } : {},
|
|
1633
1633
|
priceVersionId: original.priceVersionId,
|
|
1634
1634
|
rawCostNanoUsd: -original.rawCostNanoUsd,
|
|
@@ -1639,12 +1639,12 @@ function buildCompensatingRecord(original) {
|
|
|
1639
1639
|
priceMissing: original.priceMissing,
|
|
1640
1640
|
status: "posted",
|
|
1641
1641
|
reversesRecordId: original.id,
|
|
1642
|
-
// Stryker disable next-line ConditionalExpression
|
|
1642
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { correlationId: undefined } is equivalent to {}
|
|
1643
1643
|
...original.correlationId !== void 0 ? { correlationId: original.correlationId } : {},
|
|
1644
|
-
// Stryker disable next-line ConditionalExpression
|
|
1644
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestId: undefined } is equivalent to {}
|
|
1645
1645
|
...original.requestId !== void 0 ? { requestId: original.requestId } : {},
|
|
1646
1646
|
isSystemCost: original.isSystemCost,
|
|
1647
|
-
// Stryker disable next-line ConditionalExpression
|
|
1647
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { systemCostCategory: undefined } is equivalent to {}
|
|
1648
1648
|
...original.systemCostCategory !== void 0 ? { systemCostCategory: original.systemCostCategory } : {},
|
|
1649
1649
|
enforced: original.enforced,
|
|
1650
1650
|
occurredAt: original.occurredAt
|
|
@@ -1681,7 +1681,7 @@ var MarkupResolver = class {
|
|
|
1681
1681
|
return await policy.resolve(context);
|
|
1682
1682
|
} catch {
|
|
1683
1683
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
1684
|
-
// Stryker disable next-line StringLiteral
|
|
1684
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1685
1685
|
reason: "markup policy failed to resolve a multiplier"
|
|
1686
1686
|
});
|
|
1687
1687
|
}
|
|
@@ -1692,7 +1692,7 @@ var MarkupResolver = class {
|
|
|
1692
1692
|
return resolveMultiplier4dp(raw);
|
|
1693
1693
|
} catch {
|
|
1694
1694
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
1695
|
-
// Stryker disable next-line StringLiteral
|
|
1695
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
1696
1696
|
reason: "markup policy returned an invalid multiplier",
|
|
1697
1697
|
value: raw
|
|
1698
1698
|
});
|
|
@@ -2124,6 +2124,19 @@ function tokenCountsOf(usage) {
|
|
|
2124
2124
|
function sumTokenCounts(counts) {
|
|
2125
2125
|
return TOKEN_FIELDS.reduce((total, field) => total + counts[field], 0);
|
|
2126
2126
|
}
|
|
2127
|
+
function clampUsageCounts(usage) {
|
|
2128
|
+
const counts = tokenCountsOf(usage);
|
|
2129
|
+
const clamped = zeroTokenCounts();
|
|
2130
|
+
for (const field of TOKEN_FIELDS) {
|
|
2131
|
+
clamped[field] = counts[field] < 0 ? 0 : counts[field];
|
|
2132
|
+
}
|
|
2133
|
+
const result = { ...usage, ...clamped };
|
|
2134
|
+
const cost = usage.providerReportedCostNanoUsd;
|
|
2135
|
+
if (cost !== void 0 && cost < 0n) {
|
|
2136
|
+
return { ...result, providerReportedCostNanoUsd: 0n };
|
|
2137
|
+
}
|
|
2138
|
+
return result;
|
|
2139
|
+
}
|
|
2127
2140
|
function isNormalizedUsage(usage) {
|
|
2128
2141
|
if (typeof usage !== "object" || usage === null) return false;
|
|
2129
2142
|
const candidate = usage;
|
|
@@ -2161,7 +2174,7 @@ async function resolveHoldEstimate(pricing, context, estimate, at) {
|
|
|
2161
2174
|
operation: estimate.operation,
|
|
2162
2175
|
serviceTier: tier,
|
|
2163
2176
|
at,
|
|
2164
|
-
// Stryker disable next-line ConditionalExpression
|
|
2177
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { baseModel: undefined } is equivalent to {} because resolveRate reads input.baseModel (undefined either way) — same cache key and same `baseModel !== undefined` resolution branch
|
|
2165
2178
|
...context.baseModel !== void 0 ? { baseModel: context.baseModel } : {}
|
|
2166
2179
|
});
|
|
2167
2180
|
const rawEstimateNanoUsd = rate === null ? 0n : computeCostNanoUsd(usage, rate).totalNanoUsd;
|
|
@@ -2185,7 +2198,7 @@ async function resolveHoldEstimate(pricing, context, estimate, at) {
|
|
|
2185
2198
|
async function resolveTokenEstimate(pricing, context, tokens, serviceTier, at) {
|
|
2186
2199
|
if (context.preset === void 0) {
|
|
2187
2200
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
2188
|
-
// Stryker disable next-line StringLiteral
|
|
2201
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2189
2202
|
reason: "a { tokens } hold estimate requires context.preset to identify the provider"
|
|
2190
2203
|
});
|
|
2191
2204
|
}
|
|
@@ -2200,17 +2213,17 @@ async function resolveTokenEstimate(pricing, context, tokens, serviceTier, at) {
|
|
|
2200
2213
|
return { provider, model, operation: "chat", serviceTier, rawEstimateNanoUsd, estimatedTokens: tokens, tokenCounts };
|
|
2201
2214
|
}
|
|
2202
2215
|
function normalizeCaptureUsage(usage, normalizer) {
|
|
2203
|
-
if (isNormalizedUsage(usage)) return usage;
|
|
2216
|
+
if (isNormalizedUsage(usage)) return clampUsageCounts(usage);
|
|
2204
2217
|
if (normalizer !== void 0) {
|
|
2205
2218
|
try {
|
|
2206
|
-
return normalizer(usage);
|
|
2219
|
+
return clampUsageCounts(normalizer(usage));
|
|
2207
2220
|
} catch (error) {
|
|
2208
2221
|
if (error instanceof AiTokensException) throw error;
|
|
2209
2222
|
throw new AiTokensException("AI_TOKENS_USAGE_MALFORMED", void 0, { reason: "the normalizer could not read the usage token fields" });
|
|
2210
2223
|
}
|
|
2211
2224
|
}
|
|
2212
2225
|
throw new AiTokensException("AI_TOKENS_USAGE_MALFORMED", void 0, {
|
|
2213
|
-
// Stryker disable next-line StringLiteral
|
|
2226
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2214
2227
|
reason: "capture requires an already-normalized usage or a preset normalizer"
|
|
2215
2228
|
});
|
|
2216
2229
|
}
|
|
@@ -2239,7 +2252,7 @@ function buildAppendInput(args) {
|
|
|
2239
2252
|
audioOutTokens: normalized.audioOutTokens,
|
|
2240
2253
|
imageInTokens: normalized.imageInTokens,
|
|
2241
2254
|
imageOutTokens: normalized.imageOutTokens,
|
|
2242
|
-
// Stryker disable next-line ConditionalExpression,EqualityOperator
|
|
2255
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: CE/EQ: when ratedUnits is empty, spreading { extraUnits: {} } vs omitting the key; downstream consumers check record.extraUnits !== undefined (absent vs present {}); unit tests don't use server tool use, so ratedUnits={} always → no observable difference
|
|
2243
2256
|
...Object.keys(ratedUnits).length > 0 ? { extraUnits: ratedUnits } : {},
|
|
2244
2257
|
priceVersionId: rating.priceVersionId,
|
|
2245
2258
|
rawCostNanoUsd: rating.rawCostNanoUsd,
|
|
@@ -2250,7 +2263,7 @@ function buildAppendInput(args) {
|
|
|
2250
2263
|
priceMissing: rating.priceMissing,
|
|
2251
2264
|
status: "posted",
|
|
2252
2265
|
isSystemCost: context.isSystemCost ?? false,
|
|
2253
|
-
// Stryker disable next-line ConditionalExpression
|
|
2266
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { systemCostCategory: undefined } is equivalent to {} because JS property access treats absent and undefined the same
|
|
2254
2267
|
...context.systemCostCategory !== void 0 ? { systemCostCategory: context.systemCostCategory } : {},
|
|
2255
2268
|
enforced,
|
|
2256
2269
|
occurredAt
|
|
@@ -2260,19 +2273,19 @@ function baseColumns(usage, context, serviceTier) {
|
|
|
2260
2273
|
return {
|
|
2261
2274
|
tenantId: context.tenantId,
|
|
2262
2275
|
scope: context.scope,
|
|
2263
|
-
// Stryker disable next-line ConditionalExpression
|
|
2276
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { beneficiary: undefined } is equivalent to {} for property access and truthiness-based filters
|
|
2264
2277
|
...context.beneficiary !== void 0 ? { beneficiary: context.beneficiary } : {},
|
|
2265
|
-
// Stryker disable next-line ConditionalExpression
|
|
2278
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedBy: undefined } is equivalent to {}
|
|
2266
2279
|
...context.requestedBy !== void 0 ? { requestedBy: context.requestedBy } : {},
|
|
2267
2280
|
provider: usage.provider,
|
|
2268
2281
|
model: usage.model,
|
|
2269
|
-
// Stryker disable next-line ConditionalExpression
|
|
2282
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { requestedModel: undefined } is equivalent to {} for downstream consumers
|
|
2270
2283
|
...context.baseModel !== void 0 ? { requestedModel: context.baseModel } : {},
|
|
2271
2284
|
operation: usage.operation,
|
|
2272
2285
|
serviceTier,
|
|
2273
2286
|
feature: context.feature,
|
|
2274
2287
|
tags: context.tags ?? [],
|
|
2275
|
-
// Stryker disable next-line ConditionalExpression
|
|
2288
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { correlationId: undefined } is equivalent to {} because it's only read via record.correlationId
|
|
2276
2289
|
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {}
|
|
2277
2290
|
};
|
|
2278
2291
|
}
|
|
@@ -2334,7 +2347,7 @@ var MeteringService = class {
|
|
|
2334
2347
|
const enforcing = context.enforce === true;
|
|
2335
2348
|
if (enforcing && this.wallets === void 0 && this.budgets === void 0) {
|
|
2336
2349
|
throw new AiTokensException("AI_TOKENS_INVALID_CONFIG", void 0, {
|
|
2337
|
-
// Stryker disable next-line StringLiteral
|
|
2350
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2338
2351
|
reason: "enforce requires the wallets or budgets feature to be enabled"
|
|
2339
2352
|
});
|
|
2340
2353
|
}
|
|
@@ -2389,7 +2402,7 @@ var MeteringService = class {
|
|
|
2389
2402
|
model: input.model,
|
|
2390
2403
|
operation: input.operation,
|
|
2391
2404
|
serviceTier,
|
|
2392
|
-
// Stryker disable next-line ConditionalExpression
|
|
2405
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { feature: undefined } is equivalent to {} because markup.resolve treats absent and undefined feature the same
|
|
2393
2406
|
...input.feature !== void 0 ? { feature: input.feature } : {}
|
|
2394
2407
|
});
|
|
2395
2408
|
return { rawCostNanoUsd, billedCostNanoUsd: markup.apply(rawCostNanoUsd) };
|
|
@@ -2591,7 +2604,7 @@ var MeteringService = class {
|
|
|
2591
2604
|
priceMissing: false,
|
|
2592
2605
|
status: "pending",
|
|
2593
2606
|
isSystemCost,
|
|
2594
|
-
// Stryker disable next-line ConditionalExpression
|
|
2607
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { systemCostCategory: undefined } is equivalent to {}
|
|
2595
2608
|
...context.systemCostCategory !== void 0 ? { systemCostCategory: context.systemCostCategory } : {},
|
|
2596
2609
|
enforced: !isSystemCost,
|
|
2597
2610
|
occurredAt
|
|
@@ -2657,9 +2670,9 @@ var MeteringService = class {
|
|
|
2657
2670
|
if (usage instanceof StreamUsageCollector) {
|
|
2658
2671
|
const finalized = await usage.finalize();
|
|
2659
2672
|
if (usage.usedFallback && finalized.inputTokens === 0 && hold.estimatedTokens > 0) {
|
|
2660
|
-
return { ...finalized, inputTokens: hold.estimatedTokens };
|
|
2673
|
+
return normalizeCaptureUsage({ ...finalized, inputTokens: hold.estimatedTokens });
|
|
2661
2674
|
}
|
|
2662
|
-
return finalized;
|
|
2675
|
+
return normalizeCaptureUsage(finalized);
|
|
2663
2676
|
}
|
|
2664
2677
|
return normalizeCaptureUsage(usage, preset2?.normalizer);
|
|
2665
2678
|
}
|
|
@@ -2689,7 +2702,7 @@ var MeteringService = class {
|
|
|
2689
2702
|
if (normalizer !== void 0) return this.normalize(normalizer, input.usage);
|
|
2690
2703
|
if (isNormalizedUsage(input.usage)) return input.usage;
|
|
2691
2704
|
throw new AiTokensException("AI_TOKENS_UNKNOWN_PROVIDER", void 0, {
|
|
2692
|
-
// Stryker disable next-line StringLiteral
|
|
2705
|
+
// Stryker disable next-line StringLiteral: error reason text is internal diagnostics
|
|
2693
2706
|
reason: "raw usage requires a preset or normalizer, or an already-normalized usage"
|
|
2694
2707
|
});
|
|
2695
2708
|
}
|
|
@@ -2792,7 +2805,7 @@ var WalletService = class {
|
|
|
2792
2805
|
amountNanoUsd: input.amountNanoUsd,
|
|
2793
2806
|
priority: input.priority ?? 0,
|
|
2794
2807
|
effectiveAt: input.effectiveAt ?? /* @__PURE__ */ new Date(),
|
|
2795
|
-
// Stryker disable next-line ConditionalExpression
|
|
2808
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { expiresAt: undefined } is equivalent to {} because InMemoryWalletStore and consumers check expiresAt !== undefined
|
|
2796
2809
|
...input.expiresAt !== void 0 ? { expiresAt: input.expiresAt } : {},
|
|
2797
2810
|
idempotencyKey: input.idempotencyKey,
|
|
2798
2811
|
reason: input.reason
|
|
@@ -2801,7 +2814,7 @@ var WalletService = class {
|
|
|
2801
2814
|
walletId: entry.walletId,
|
|
2802
2815
|
entryId: entry.id,
|
|
2803
2816
|
amountNanoUsd: entry.amountNanoUsd,
|
|
2804
|
-
// Stryker disable next-line ConditionalExpression
|
|
2817
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { expiresAt: undefined } is equivalent to {} because event consumers check expiresAt !== undefined
|
|
2805
2818
|
...entry.expiresAt !== void 0 ? { expiresAt: entry.expiresAt } : {}
|
|
2806
2819
|
};
|
|
2807
2820
|
await this.events.granted(ref, granted);
|
|
@@ -2832,16 +2845,16 @@ var WalletService = class {
|
|
|
2832
2845
|
throw this.invalid("a debit without a usageRecordId must carry a reason");
|
|
2833
2846
|
}
|
|
2834
2847
|
const entry = {
|
|
2835
|
-
// Stryker disable next-line StringLiteral
|
|
2848
|
+
// Stryker disable next-line StringLiteral: placeholder walletId; the store's insert always assigns the real wallet id and never reads this sentinel, so any string here is equivalent
|
|
2836
2849
|
walletId: "",
|
|
2837
2850
|
type: "debit",
|
|
2838
2851
|
amountNanoUsd: -input.amountNanoUsd,
|
|
2839
2852
|
priority: 0,
|
|
2840
2853
|
effectiveAt: /* @__PURE__ */ new Date(),
|
|
2841
|
-
// Stryker disable next-line ConditionalExpression
|
|
2854
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2842
2855
|
...input.usageRecordId !== void 0 ? { usageRecordId: input.usageRecordId } : {},
|
|
2843
2856
|
idempotencyKey: input.idempotencyKey,
|
|
2844
|
-
// Stryker disable next-line ConditionalExpression
|
|
2857
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { reason: undefined } is equivalent to {} because store and consumers check reason !== undefined
|
|
2845
2858
|
...input.reason !== void 0 ? { reason: input.reason } : {}
|
|
2846
2859
|
};
|
|
2847
2860
|
const debited = await this.conditionalDebit(ref, entry, input.amountNanoUsd);
|
|
@@ -2868,7 +2881,7 @@ var WalletService = class {
|
|
|
2868
2881
|
amountNanoUsd: input.amountNanoUsd,
|
|
2869
2882
|
priority: 0,
|
|
2870
2883
|
effectiveAt: /* @__PURE__ */ new Date(),
|
|
2871
|
-
// Stryker disable next-line ConditionalExpression,ObjectLiteral
|
|
2884
|
+
// Stryker disable next-line ConditionalExpression,ObjectLiteral: CE true / OL {}: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2872
2885
|
...input.usageRecordId !== void 0 ? { usageRecordId: input.usageRecordId } : {},
|
|
2873
2886
|
idempotencyKey: input.idempotencyKey,
|
|
2874
2887
|
reason: input.reason
|
|
@@ -2925,8 +2938,8 @@ var WalletService = class {
|
|
|
2925
2938
|
amountNanoUsd: input.amountNanoUsd,
|
|
2926
2939
|
priority: 0,
|
|
2927
2940
|
effectiveAt: /* @__PURE__ */ new Date(),
|
|
2928
|
-
// Stryker disable next-line ConditionalExpression
|
|
2929
|
-
// Stryker disable next-line ConditionalExpression
|
|
2941
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2942
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { usageRecordId: undefined } is equivalent to {} because store and consumers check usageRecordId !== undefined
|
|
2930
2943
|
...input.usageRecordId !== void 0 ? { usageRecordId: input.usageRecordId } : {},
|
|
2931
2944
|
idempotencyKey: input.idempotencyKey,
|
|
2932
2945
|
reason: input.reason
|
|
@@ -3558,11 +3571,11 @@ function featureMatches3(features, feature) {
|
|
|
3558
3571
|
}
|
|
3559
3572
|
function limitsOf(budget) {
|
|
3560
3573
|
return {
|
|
3561
|
-
// Stryker disable next-line ConditionalExpression
|
|
3574
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { nanoUsd: undefined } is equivalent to {} because all consumers check limits.nanoUsd !== undefined
|
|
3562
3575
|
...budget.limitNanoUsd !== void 0 ? { nanoUsd: budget.limitNanoUsd } : {},
|
|
3563
|
-
// Stryker disable next-line ConditionalExpression
|
|
3576
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tokens: undefined } is equivalent to {} because all consumers check limits.tokens !== undefined
|
|
3564
3577
|
...budget.limitTokens !== void 0 ? { tokens: budget.limitTokens } : {},
|
|
3565
|
-
// Stryker disable next-line ConditionalExpression
|
|
3578
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { count: undefined } is equivalent to {} because all consumers check limits.count !== undefined
|
|
3566
3579
|
...budget.limitCount !== void 0 ? { count: budget.limitCount } : {}
|
|
3567
3580
|
};
|
|
3568
3581
|
}
|
|
@@ -3602,11 +3615,11 @@ function ratio(spent, limit) {
|
|
|
3602
3615
|
}
|
|
3603
3616
|
function limitSnapshot(limits) {
|
|
3604
3617
|
return {
|
|
3605
|
-
// Stryker disable next-line ConditionalExpression
|
|
3618
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { nanoUsd: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3606
3619
|
...limits.nanoUsd !== void 0 ? { nanoUsd: limits.nanoUsd } : {},
|
|
3607
|
-
// Stryker disable next-line ConditionalExpression
|
|
3620
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tokens: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3608
3621
|
...limits.tokens !== void 0 ? { tokens: limits.tokens } : {},
|
|
3609
|
-
// Stryker disable next-line ConditionalExpression
|
|
3622
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { count: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3610
3623
|
...limits.count !== void 0 ? { count: limits.count } : {}
|
|
3611
3624
|
};
|
|
3612
3625
|
}
|
|
@@ -3615,11 +3628,11 @@ function spendSnapshot(spend) {
|
|
|
3615
3628
|
}
|
|
3616
3629
|
function remainingSnapshot(limits, spend) {
|
|
3617
3630
|
return {
|
|
3618
|
-
// Stryker disable next-line ConditionalExpression
|
|
3631
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { nanoUsd: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3619
3632
|
...limits.nanoUsd !== void 0 ? { nanoUsd: max0(limits.nanoUsd - spend.spentNanoUsd) } : {},
|
|
3620
|
-
// Stryker disable next-line ConditionalExpression
|
|
3633
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tokens: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3621
3634
|
...limits.tokens !== void 0 ? { tokens: Math.max(0, limits.tokens - spend.spentTokens) } : {},
|
|
3622
|
-
// Stryker disable next-line ConditionalExpression
|
|
3635
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { count: undefined } is equivalent to {} for event consumers who check field !== undefined
|
|
3623
3636
|
...limits.count !== void 0 ? { count: Math.max(0, limits.count - spend.spentCount) } : {}
|
|
3624
3637
|
};
|
|
3625
3638
|
}
|
|
@@ -3627,7 +3640,7 @@ function buildStatus(budget, windowStart, resetsAt, spend) {
|
|
|
3627
3640
|
const limits = limitsOf(budget);
|
|
3628
3641
|
return {
|
|
3629
3642
|
budgetId: budget.id,
|
|
3630
|
-
// Stryker disable next-line ConditionalExpression
|
|
3643
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { features: undefined } is equivalent to {} because status consumers check features !== undefined
|
|
3631
3644
|
...budget.features !== void 0 ? { features: budget.features } : {},
|
|
3632
3645
|
window: budget.window,
|
|
3633
3646
|
windowStart,
|
|
@@ -3794,29 +3807,29 @@ UsageReportService = __decorateClass([
|
|
|
3794
3807
|
function toLedgerFilter(filter, limit) {
|
|
3795
3808
|
return {
|
|
3796
3809
|
tenantId: filter.tenantId,
|
|
3797
|
-
// Stryker disable next-line ConditionalExpression
|
|
3810
|
+
// Stryker disable next-line ConditionalExpression: spreading { scope: undefined } is equivalent to {} because InMemoryLedgerStore uses truthiness; the 'false' variant is covered by the scope isolation test
|
|
3798
3811
|
...filter.scope !== void 0 ? { scope: filter.scope } : {},
|
|
3799
|
-
// Stryker disable next-line ConditionalExpression
|
|
3812
|
+
// Stryker disable next-line ConditionalExpression: spreading { beneficiary: undefined } is equivalent to {} for the same reason
|
|
3800
3813
|
...filter.beneficiary !== void 0 ? { beneficiary: filter.beneficiary } : {},
|
|
3801
|
-
// Stryker disable next-line ConditionalExpression
|
|
3814
|
+
// Stryker disable next-line ConditionalExpression: spreading { feature: undefined } does not change the !== undefined check in matches()
|
|
3802
3815
|
...filter.feature !== void 0 ? { feature: filter.feature } : {},
|
|
3803
|
-
// Stryker disable next-line ConditionalExpression
|
|
3816
|
+
// Stryker disable next-line ConditionalExpression: spreading { features: undefined } does not change the truthiness check in matches()
|
|
3804
3817
|
...filter.features !== void 0 ? { features: filter.features } : {},
|
|
3805
|
-
// Stryker disable next-line ConditionalExpression
|
|
3818
|
+
// Stryker disable next-line ConditionalExpression: spreading { provider: undefined } does not change the !== undefined check in matches()
|
|
3806
3819
|
...filter.provider !== void 0 ? { provider: filter.provider } : {},
|
|
3807
|
-
// Stryker disable next-line ConditionalExpression
|
|
3820
|
+
// Stryker disable next-line ConditionalExpression: spreading { model: undefined } does not change the !== undefined check in matches()
|
|
3808
3821
|
...filter.model !== void 0 ? { model: filter.model } : {},
|
|
3809
|
-
// Stryker disable next-line ConditionalExpression
|
|
3822
|
+
// Stryker disable next-line ConditionalExpression: spreading { operation: undefined } does not change the !== undefined check in matches()
|
|
3810
3823
|
...filter.operation !== void 0 ? { operation: filter.operation } : {},
|
|
3811
|
-
// Stryker disable next-line ConditionalExpression
|
|
3824
|
+
// Stryker disable next-line ConditionalExpression: spreading { serviceTier: undefined } does not change the !== undefined check in matches()
|
|
3812
3825
|
...filter.serviceTier !== void 0 ? { serviceTier: filter.serviceTier } : {},
|
|
3813
|
-
// Stryker disable next-line ConditionalExpression
|
|
3826
|
+
// Stryker disable next-line ConditionalExpression: spreading { tags: undefined } does not change the truthiness check in matches()
|
|
3814
3827
|
...filter.tags !== void 0 ? { tags: filter.tags } : {},
|
|
3815
|
-
// Stryker disable next-line ConditionalExpression
|
|
3828
|
+
// Stryker disable next-line ConditionalExpression: spreading { isSystemCost: undefined } does not change the !== undefined check in matches()
|
|
3816
3829
|
...filter.isSystemCost !== void 0 ? { isSystemCost: filter.isSystemCost } : {},
|
|
3817
|
-
// Stryker disable next-line ConditionalExpression
|
|
3830
|
+
// Stryker disable next-line ConditionalExpression: spreading { systemCostCategory: undefined } does not change the !== undefined check in matches()
|
|
3818
3831
|
...filter.systemCostCategory !== void 0 ? { systemCostCategory: filter.systemCostCategory } : {},
|
|
3819
|
-
// Stryker disable next-line ConditionalExpression
|
|
3832
|
+
// Stryker disable next-line ConditionalExpression: spreading { enforcedOnly: undefined } does not change the === true check in matches()
|
|
3820
3833
|
...filter.enforcedOnly !== void 0 ? { enforcedOnly: filter.enforcedOnly } : {},
|
|
3821
3834
|
status: filter.status ?? [...BALANCE_STATUSES2],
|
|
3822
3835
|
from: filter.from,
|
|
@@ -4002,9 +4015,9 @@ var MeteringInterceptor = class {
|
|
|
4002
4015
|
const context = await this.resolveContext(executionContext, config, enrichment);
|
|
4003
4016
|
return this.metering.record({
|
|
4004
4017
|
usage,
|
|
4005
|
-
// Stryker disable next-line ConditionalExpression
|
|
4018
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { preset: undefined } is equivalent to {} because MeteringService checks preset !== undefined
|
|
4006
4019
|
...config.preset !== void 0 ? { preset: config.preset } : {},
|
|
4007
|
-
// Stryker disable next-line ConditionalExpression
|
|
4020
|
+
// Stryker disable next-line ConditionalExpression: CE true: spreading { tags: undefined } is equivalent to {} because MeteringService checks tags !== undefined
|
|
4008
4021
|
context: { ...context, enforce: true, isSystemCost: config.isSystemCost ?? false, ...config.tags !== void 0 ? { tags: config.tags } : {} }
|
|
4009
4022
|
});
|
|
4010
4023
|
}
|
|
@@ -4332,24 +4345,24 @@ function preset(provider, normalizer, ratingMode) {
|
|
|
4332
4345
|
return { provider, normalizer, ratingMode };
|
|
4333
4346
|
}
|
|
4334
4347
|
var providerPresets = {
|
|
4335
|
-
// Stryker disable next-line StringLiteral
|
|
4348
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests that exercise the full preset→MeteringService→UsageRecord path
|
|
4336
4349
|
openaiChat: preset("openai", normalizeOpenAiChatUsage, "rate-table"),
|
|
4337
|
-
// Stryker disable next-line StringLiteral
|
|
4350
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4338
4351
|
openaiResponses: preset("openai", normalizeOpenAiResponsesUsage, "rate-table"),
|
|
4339
|
-
// Stryker disable next-line StringLiteral
|
|
4352
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4340
4353
|
azureOpenai: preset("azure-openai", normalizeOpenAiChatUsage, "rate-table"),
|
|
4341
|
-
// Stryker disable next-line StringLiteral
|
|
4354
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4342
4355
|
anthropic: preset("anthropic", normalizeAnthropicUsage, "rate-table"),
|
|
4343
|
-
// Stryker disable next-line StringLiteral
|
|
4356
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4344
4357
|
gemini: preset("gemini", normalizeGeminiUsage, "rate-table"),
|
|
4345
|
-
// Stryker disable next-line StringLiteral
|
|
4358
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4346
4359
|
vertex: preset("vertex", normalizeGeminiUsage, "rate-table"),
|
|
4347
|
-
// Stryker disable next-line StringLiteral
|
|
4360
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4348
4361
|
bedrock: preset("bedrock", normalizeBedrockConverseUsage, "rate-table"),
|
|
4349
|
-
// Stryker disable next-line StringLiteral
|
|
4362
|
+
// Stryker disable next-line StringLiteral: provider ID; only observable via integration tests
|
|
4350
4363
|
mistral: preset("mistral", normalizeMistralUsage, "rate-table"),
|
|
4351
4364
|
vercelAiSdk: preset("openai", normalizeVercelAiSdkUsage, "rate-table"),
|
|
4352
|
-
// Stryker disable next-line StringLiteral
|
|
4365
|
+
// Stryker disable next-line StringLiteral: provider ID and rating mode; only observable via integration tests
|
|
4353
4366
|
openrouter: preset("openrouter", normalizeOpenRouterUsage, "provider-reported"),
|
|
4354
4367
|
/** A custom OpenAI-compatible provider (DeepSeek, xAI, Groq, …). */
|
|
4355
4368
|
openaiCompatible: (id) => preset(id, normalizeOpenAiCompatibleUsage, "rate-table")
|
package/dist/shared/index.cjs
CHANGED
|
@@ -332,9 +332,9 @@ function normalizeMistralUsage(raw) {
|
|
|
332
332
|
provider: "mistral",
|
|
333
333
|
model: str(response.model) ?? "",
|
|
334
334
|
operation: "chat",
|
|
335
|
-
// Stryker disable next-line StringLiteral
|
|
335
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
336
336
|
inputTokens: requireNum(usage.prompt_tokens, "mistral", "usage.prompt_tokens"),
|
|
337
|
-
// Stryker disable next-line StringLiteral
|
|
337
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
338
338
|
outputTokens: requireNum(usage.completion_tokens, "mistral", "usage.completion_tokens"),
|
|
339
339
|
raw: usage
|
|
340
340
|
});
|
package/dist/shared/index.mjs
CHANGED
|
@@ -330,9 +330,9 @@ function normalizeMistralUsage(raw) {
|
|
|
330
330
|
provider: "mistral",
|
|
331
331
|
model: str(response.model) ?? "",
|
|
332
332
|
operation: "chat",
|
|
333
|
-
// Stryker disable next-line StringLiteral
|
|
333
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
334
334
|
inputTokens: requireNum(usage.prompt_tokens, "mistral", "usage.prompt_tokens"),
|
|
335
|
-
// Stryker disable next-line StringLiteral
|
|
335
|
+
// Stryker disable next-line StringLiteral: provider and field names in error messages are internal diagnostics
|
|
336
336
|
outputTokens: requireNum(usage.completion_tokens, "mistral", "usage.completion_tokens"),
|
|
337
337
|
raw: usage
|
|
338
338
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-ai-tokens",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "pnpm clean && tsup",
|
|
78
78
|
"check:exports": "attw --pack . --profile strict",
|
|
79
|
+
"check:mutants": "node scripts/check-mutation-directives.mjs",
|
|
79
80
|
"check:published": "node scripts/check-published-surface.mjs",
|
|
80
81
|
"check:runtime": "node scripts/check-consumer-runtime.mjs",
|
|
81
82
|
"clean": "node -e \"const {rmSync}=require('node:fs');for(const d of ['dist','coverage'])rmSync(d,{recursive:true,force:true})\"",
|
|
@@ -84,10 +85,10 @@
|
|
|
84
85
|
"lint": "eslint src",
|
|
85
86
|
"lint:fix": "eslint src --fix",
|
|
86
87
|
"mutation": "stryker run",
|
|
88
|
+
"mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
|
|
87
89
|
"mutation:dry-run": "stryker run --dryRunOnly",
|
|
88
|
-
"mutation:incremental": "stryker run --incremental",
|
|
89
90
|
"prepare": "prisma generate",
|
|
90
|
-
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
|
|
91
|
+
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
|
|
91
92
|
"release": "npm publish --provenance --access public",
|
|
92
93
|
"size": "node scripts/check-size.mjs",
|
|
93
94
|
"test": "jest",
|