@bymax-one/nest-ai-tokens 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/prices/index.cjs +218 -0
- package/dist/prices/index.d.cts +131 -0
- package/dist/prices/index.d.ts +131 -0
- package/dist/prices/index.mjs +216 -0
- package/dist/prisma/index.cjs +1197 -0
- package/dist/prisma/index.d.cts +677 -0
- package/dist/prisma/index.d.ts +677 -0
- package/dist/prisma/index.mjs +1195 -0
- package/dist/prisma/migrations/0001_init.sql +198 -0
- package/dist/prisma/schema.prisma.fragment +197 -0
- package/dist/redis/index.cjs +110 -0
- package/dist/redis/index.d.cts +98 -0
- package/dist/redis/index.d.ts +98 -0
- package/dist/redis/index.mjs +107 -0
- package/dist/server/index.cjs +4405 -0
- package/dist/server/index.d.cts +3202 -0
- package/dist/server/index.d.ts +3202 -0
- package/dist/server/index.mjs +4354 -0
- package/dist/shared/index.cjs +659 -0
- package/dist/shared/index.d.cts +1044 -0
- package/dist/shared/index.d.ts +1044 -0
- package/dist/shared/index.mjs +629 -0
- package/package.json +216 -0
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var client = require('@prisma/client');
|
|
5
|
+
var nestAiTokens = require('@bymax-one/nest-ai-tokens');
|
|
6
|
+
|
|
7
|
+
// src/prisma/index.ts
|
|
8
|
+
|
|
9
|
+
// src/server/services/ledger-idempotency-conflict.ts
|
|
10
|
+
var LedgerIdempotencyConflict = class extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* @param tenantId The tenant whose key collided.
|
|
13
|
+
* @param idempotencyKey The key reused with a different payload.
|
|
14
|
+
*/
|
|
15
|
+
constructor(tenantId, idempotencyKey) {
|
|
16
|
+
super("Ledger idempotency-key payload mismatch");
|
|
17
|
+
this.tenantId = tenantId;
|
|
18
|
+
this.idempotencyKey = idempotencyKey;
|
|
19
|
+
this.name = "LedgerIdempotencyConflict";
|
|
20
|
+
}
|
|
21
|
+
tenantId;
|
|
22
|
+
idempotencyKey;
|
|
23
|
+
/** Structural brand matched by {@link isLedgerIdempotencyConflict} across bundles. */
|
|
24
|
+
isAiTokensLedgerConflict = true;
|
|
25
|
+
};
|
|
26
|
+
function isLedgerIdempotencyConflict(error) {
|
|
27
|
+
if (typeof error !== "object" || error === null) return false;
|
|
28
|
+
return error.isAiTokensLedgerConflict === true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/shared/constants/ledger-fields.constants.ts
|
|
32
|
+
var AMOUNT_FIELDS = [
|
|
33
|
+
"inputTokens",
|
|
34
|
+
"outputTokens",
|
|
35
|
+
"cacheReadTokens",
|
|
36
|
+
"cacheWrite5mTokens",
|
|
37
|
+
"cacheWrite1hTokens",
|
|
38
|
+
"reasoningTokens",
|
|
39
|
+
"audioInTokens",
|
|
40
|
+
"audioOutTokens",
|
|
41
|
+
"imageInTokens",
|
|
42
|
+
"imageOutTokens",
|
|
43
|
+
"totalTokens",
|
|
44
|
+
"rawCostNanoUsd",
|
|
45
|
+
"surchargeNanoUsd",
|
|
46
|
+
"billedCostNanoUsd"
|
|
47
|
+
];
|
|
48
|
+
var SETTLEMENT_FIELDS = [
|
|
49
|
+
...AMOUNT_FIELDS,
|
|
50
|
+
"priceVersionId",
|
|
51
|
+
"priceMissing",
|
|
52
|
+
"markupMultiplier"
|
|
53
|
+
];
|
|
54
|
+
var RELEASE_FIELDS = ["correlationId", "requestId"];
|
|
55
|
+
var REVERSAL_LINKAGE_FIELD = "reversedByRecordId";
|
|
56
|
+
var SETTLEMENT_PATCH_KEYS = /* @__PURE__ */ new Set([...SETTLEMENT_FIELDS, REVERSAL_LINKAGE_FIELD]);
|
|
57
|
+
var RELEASE_FIELD_SET = new Set(RELEASE_FIELDS);
|
|
58
|
+
function isLegalTransitionPatchKey(from, to, key) {
|
|
59
|
+
if (from === "pending" && to === "posted") return SETTLEMENT_PATCH_KEYS.has(key);
|
|
60
|
+
if (from === "pending" && to === "released") return RELEASE_FIELD_SET.has(key);
|
|
61
|
+
if (from === "posted" && to === "reversed") return key === REVERSAL_LINKAGE_FIELD;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/shared/utils/sha256.ts
|
|
66
|
+
var K_WORDS = [
|
|
67
|
+
1116352408,
|
|
68
|
+
1899447441,
|
|
69
|
+
3049323471,
|
|
70
|
+
3921009573,
|
|
71
|
+
961987163,
|
|
72
|
+
1508970993,
|
|
73
|
+
2453635748,
|
|
74
|
+
2870763221,
|
|
75
|
+
3624381080,
|
|
76
|
+
310598401,
|
|
77
|
+
607225278,
|
|
78
|
+
1426881987,
|
|
79
|
+
1925078388,
|
|
80
|
+
2162078206,
|
|
81
|
+
2614888103,
|
|
82
|
+
3248222580,
|
|
83
|
+
3835390401,
|
|
84
|
+
4022224774,
|
|
85
|
+
264347078,
|
|
86
|
+
604807628,
|
|
87
|
+
770255983,
|
|
88
|
+
1249150122,
|
|
89
|
+
1555081692,
|
|
90
|
+
1996064986,
|
|
91
|
+
2554220882,
|
|
92
|
+
2821834349,
|
|
93
|
+
2952996808,
|
|
94
|
+
3210313671,
|
|
95
|
+
3336571891,
|
|
96
|
+
3584528711,
|
|
97
|
+
113926993,
|
|
98
|
+
338241895,
|
|
99
|
+
666307205,
|
|
100
|
+
773529912,
|
|
101
|
+
1294757372,
|
|
102
|
+
1396182291,
|
|
103
|
+
1695183700,
|
|
104
|
+
1986661051,
|
|
105
|
+
2177026350,
|
|
106
|
+
2456956037,
|
|
107
|
+
2730485921,
|
|
108
|
+
2820302411,
|
|
109
|
+
3259730800,
|
|
110
|
+
3345764771,
|
|
111
|
+
3516065817,
|
|
112
|
+
3600352804,
|
|
113
|
+
4094571909,
|
|
114
|
+
275423344,
|
|
115
|
+
430227734,
|
|
116
|
+
506948616,
|
|
117
|
+
659060556,
|
|
118
|
+
883997877,
|
|
119
|
+
958139571,
|
|
120
|
+
1322822218,
|
|
121
|
+
1537002063,
|
|
122
|
+
1747873779,
|
|
123
|
+
1955562222,
|
|
124
|
+
2024104815,
|
|
125
|
+
2227730452,
|
|
126
|
+
2361852424,
|
|
127
|
+
2428436474,
|
|
128
|
+
2756734187,
|
|
129
|
+
3204031479,
|
|
130
|
+
3329325298
|
|
131
|
+
];
|
|
132
|
+
var INITIAL_HASH = [
|
|
133
|
+
1779033703,
|
|
134
|
+
3144134277,
|
|
135
|
+
1013904242,
|
|
136
|
+
2773480762,
|
|
137
|
+
1359893119,
|
|
138
|
+
2600822924,
|
|
139
|
+
528734635,
|
|
140
|
+
1541459225
|
|
141
|
+
];
|
|
142
|
+
var kView = new DataView(new ArrayBuffer(64 * 4));
|
|
143
|
+
K_WORDS.forEach((value, index) => {
|
|
144
|
+
kView.setUint32(index * 4, value);
|
|
145
|
+
});
|
|
146
|
+
function rotr(x, n) {
|
|
147
|
+
return (x >>> n | x << 32 - n) >>> 0;
|
|
148
|
+
}
|
|
149
|
+
function expandSchedule(w) {
|
|
150
|
+
for (let i = 16; i < 64; i++) {
|
|
151
|
+
const w15 = w.getUint32((i - 15) * 4);
|
|
152
|
+
const w2 = w.getUint32((i - 2) * 4);
|
|
153
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
|
|
154
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
|
|
155
|
+
w.setUint32(i * 4, w.getUint32((i - 16) * 4) + s0 + w.getUint32((i - 7) * 4) + s1 >>> 0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function compressBlock(h, w) {
|
|
159
|
+
let a = h.getUint32(0);
|
|
160
|
+
let b = h.getUint32(4);
|
|
161
|
+
let c = h.getUint32(8);
|
|
162
|
+
let d = h.getUint32(12);
|
|
163
|
+
let e = h.getUint32(16);
|
|
164
|
+
let f = h.getUint32(20);
|
|
165
|
+
let g = h.getUint32(24);
|
|
166
|
+
let hh = h.getUint32(28);
|
|
167
|
+
for (let i = 0; i < 64; i++) {
|
|
168
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
169
|
+
const ch = e & f ^ ~e & g;
|
|
170
|
+
const t1 = hh + s1 + ch + kView.getUint32(i * 4) + w.getUint32(i * 4) >>> 0;
|
|
171
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
172
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
173
|
+
const t2 = s0 + maj >>> 0;
|
|
174
|
+
hh = g;
|
|
175
|
+
g = f;
|
|
176
|
+
f = e;
|
|
177
|
+
e = d + t1 >>> 0;
|
|
178
|
+
d = c;
|
|
179
|
+
c = b;
|
|
180
|
+
b = a;
|
|
181
|
+
a = t1 + t2 >>> 0;
|
|
182
|
+
}
|
|
183
|
+
const next = [a, b, c, d, e, f, g, hh];
|
|
184
|
+
next.forEach((value, index) => {
|
|
185
|
+
h.setUint32(index * 4, h.getUint32(index * 4) + value >>> 0);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function sha256Hex(input) {
|
|
189
|
+
const message = new TextEncoder().encode(input);
|
|
190
|
+
const bitLength = message.length * 8;
|
|
191
|
+
const paddedLength = Math.ceil((message.length + 9) / 64) * 64;
|
|
192
|
+
const bytes = new Uint8Array(paddedLength);
|
|
193
|
+
bytes.set(message);
|
|
194
|
+
bytes[message.length] = 128;
|
|
195
|
+
const view = new DataView(bytes.buffer);
|
|
196
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
|
|
197
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0);
|
|
198
|
+
const hView = new DataView(new ArrayBuffer(32));
|
|
199
|
+
INITIAL_HASH.forEach((value, index) => {
|
|
200
|
+
hView.setUint32(index * 4, value);
|
|
201
|
+
});
|
|
202
|
+
const wView = new DataView(new ArrayBuffer(64 * 4));
|
|
203
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
204
|
+
for (let i = 0; i < 16; i++) wView.setUint32(i * 4, view.getUint32(offset + i * 4));
|
|
205
|
+
expandSchedule(wView);
|
|
206
|
+
compressBlock(hView, wView);
|
|
207
|
+
}
|
|
208
|
+
let hex = "";
|
|
209
|
+
for (let i = 0; i < 8; i++) hex += hView.getUint32(i * 4).toString(16).padStart(8, "0");
|
|
210
|
+
return hex;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/shared/utils/idempotency.ts
|
|
214
|
+
function canonicalize(value) {
|
|
215
|
+
if (value === null || value === void 0) return "null";
|
|
216
|
+
if (typeof value === "bigint") return JSON.stringify(`${value.toString()}n`);
|
|
217
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
218
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
219
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString());
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
return `[${value.map((element) => canonicalize(element)).join(",")}]`;
|
|
222
|
+
}
|
|
223
|
+
if (typeof value === "object") {
|
|
224
|
+
const record = value;
|
|
225
|
+
const members = Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`);
|
|
226
|
+
return `{${members.join(",")}}`;
|
|
227
|
+
}
|
|
228
|
+
return "null";
|
|
229
|
+
}
|
|
230
|
+
function deriveIdempotencyKey(payload) {
|
|
231
|
+
return sha256Hex(canonicalize(payload));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/server/utils/payload-hash.ts
|
|
235
|
+
function computePayloadHash(record) {
|
|
236
|
+
return deriveIdempotencyKey({
|
|
237
|
+
scope: record.scope,
|
|
238
|
+
beneficiary: record.beneficiary,
|
|
239
|
+
requestedBy: record.requestedBy,
|
|
240
|
+
provider: record.provider,
|
|
241
|
+
model: record.model,
|
|
242
|
+
requestedModel: record.requestedModel,
|
|
243
|
+
operation: record.operation,
|
|
244
|
+
serviceTier: record.serviceTier,
|
|
245
|
+
feature: record.feature,
|
|
246
|
+
tags: record.tags,
|
|
247
|
+
inputTokens: record.inputTokens,
|
|
248
|
+
outputTokens: record.outputTokens,
|
|
249
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
250
|
+
cacheWrite5mTokens: record.cacheWrite5mTokens,
|
|
251
|
+
cacheWrite1hTokens: record.cacheWrite1hTokens,
|
|
252
|
+
reasoningTokens: record.reasoningTokens,
|
|
253
|
+
audioInTokens: record.audioInTokens,
|
|
254
|
+
audioOutTokens: record.audioOutTokens,
|
|
255
|
+
imageInTokens: record.imageInTokens,
|
|
256
|
+
imageOutTokens: record.imageOutTokens,
|
|
257
|
+
totalTokens: record.totalTokens,
|
|
258
|
+
extraUnits: record.extraUnits,
|
|
259
|
+
priceVersionId: record.priceVersionId,
|
|
260
|
+
rawCostNanoUsd: record.rawCostNanoUsd,
|
|
261
|
+
surchargeNanoUsd: record.surchargeNanoUsd,
|
|
262
|
+
billedCostNanoUsd: record.billedCostNanoUsd,
|
|
263
|
+
markupMultiplier: record.markupMultiplier,
|
|
264
|
+
currency: record.currency,
|
|
265
|
+
priceMissing: record.priceMissing,
|
|
266
|
+
correlationId: record.correlationId,
|
|
267
|
+
requestId: record.requestId,
|
|
268
|
+
isSystemCost: record.isSystemCost,
|
|
269
|
+
systemCostCategory: record.systemCostCategory,
|
|
270
|
+
enforced: record.enforced,
|
|
271
|
+
reversesRecordId: record.reversesRecordId,
|
|
272
|
+
occurredAt: record.occurredAt
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/server/utils/hash-chain.ts
|
|
277
|
+
function chainHash(prevHash, record) {
|
|
278
|
+
return deriveIdempotencyKey({
|
|
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
|
+
prevHash: prevHash ?? "",
|
|
281
|
+
id: record.id,
|
|
282
|
+
content: computePayloadHash(record)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
var USAGE_COLUMNS = [
|
|
286
|
+
"id",
|
|
287
|
+
"tenantId",
|
|
288
|
+
"scopeType",
|
|
289
|
+
"scopeId",
|
|
290
|
+
"beneficiaryType",
|
|
291
|
+
"beneficiaryId",
|
|
292
|
+
"requestedBy",
|
|
293
|
+
"provider",
|
|
294
|
+
"model",
|
|
295
|
+
"requestedModel",
|
|
296
|
+
"operation",
|
|
297
|
+
"serviceTier",
|
|
298
|
+
"feature",
|
|
299
|
+
"tags",
|
|
300
|
+
"inputTokens",
|
|
301
|
+
"outputTokens",
|
|
302
|
+
"cacheReadTokens",
|
|
303
|
+
"cacheWrite5mTokens",
|
|
304
|
+
"cacheWrite1hTokens",
|
|
305
|
+
"reasoningTokens",
|
|
306
|
+
"audioInTokens",
|
|
307
|
+
"audioOutTokens",
|
|
308
|
+
"imageInTokens",
|
|
309
|
+
"imageOutTokens",
|
|
310
|
+
"totalTokens",
|
|
311
|
+
"extraUnits",
|
|
312
|
+
"priceVersionId",
|
|
313
|
+
"rawCostNanoUsd",
|
|
314
|
+
"surchargeNanoUsd",
|
|
315
|
+
"billedCostNanoUsd",
|
|
316
|
+
"markupMultiplier",
|
|
317
|
+
"currency",
|
|
318
|
+
"priceMissing",
|
|
319
|
+
"status",
|
|
320
|
+
"reversedByRecordId",
|
|
321
|
+
"reversesRecordId",
|
|
322
|
+
"idempotencyKey",
|
|
323
|
+
"payloadHash",
|
|
324
|
+
"correlationId",
|
|
325
|
+
"requestId",
|
|
326
|
+
"isSystemCost",
|
|
327
|
+
"systemCostCategory",
|
|
328
|
+
"enforced",
|
|
329
|
+
"prevHash",
|
|
330
|
+
"hash",
|
|
331
|
+
"occurredAt"
|
|
332
|
+
];
|
|
333
|
+
function scopeOf(type, id) {
|
|
334
|
+
if (type === null || id === null) return void 0;
|
|
335
|
+
return { type, id };
|
|
336
|
+
}
|
|
337
|
+
function mapUsageRow(row) {
|
|
338
|
+
const beneficiary = scopeOf(row.beneficiaryType, row.beneficiaryId);
|
|
339
|
+
return {
|
|
340
|
+
id: row.id,
|
|
341
|
+
tenantId: row.tenantId,
|
|
342
|
+
scope: { type: row.scopeType, id: row.scopeId },
|
|
343
|
+
...beneficiary !== void 0 ? { beneficiary } : {},
|
|
344
|
+
...row.requestedBy !== null ? { requestedBy: row.requestedBy } : {},
|
|
345
|
+
provider: row.provider,
|
|
346
|
+
model: row.model,
|
|
347
|
+
...row.requestedModel !== null ? { requestedModel: row.requestedModel } : {},
|
|
348
|
+
operation: row.operation,
|
|
349
|
+
serviceTier: row.serviceTier,
|
|
350
|
+
feature: row.feature,
|
|
351
|
+
tags: row.tags,
|
|
352
|
+
inputTokens: row.inputTokens,
|
|
353
|
+
outputTokens: row.outputTokens,
|
|
354
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
355
|
+
cacheWrite5mTokens: row.cacheWrite5mTokens,
|
|
356
|
+
cacheWrite1hTokens: row.cacheWrite1hTokens,
|
|
357
|
+
reasoningTokens: row.reasoningTokens,
|
|
358
|
+
audioInTokens: row.audioInTokens,
|
|
359
|
+
audioOutTokens: row.audioOutTokens,
|
|
360
|
+
imageInTokens: row.imageInTokens,
|
|
361
|
+
imageOutTokens: row.imageOutTokens,
|
|
362
|
+
totalTokens: row.totalTokens,
|
|
363
|
+
...row.extraUnits !== null ? { extraUnits: row.extraUnits } : {},
|
|
364
|
+
priceVersionId: row.priceVersionId,
|
|
365
|
+
rawCostNanoUsd: row.rawCostNanoUsd,
|
|
366
|
+
surchargeNanoUsd: row.surchargeNanoUsd,
|
|
367
|
+
billedCostNanoUsd: row.billedCostNanoUsd,
|
|
368
|
+
markupMultiplier: row.markupMultiplier.toNumber(),
|
|
369
|
+
currency: row.currency,
|
|
370
|
+
priceMissing: row.priceMissing,
|
|
371
|
+
status: row.status,
|
|
372
|
+
...row.reversedByRecordId !== null ? { reversedByRecordId: row.reversedByRecordId } : {},
|
|
373
|
+
...row.reversesRecordId !== null ? { reversesRecordId: row.reversesRecordId } : {},
|
|
374
|
+
idempotencyKey: row.idempotencyKey,
|
|
375
|
+
...row.correlationId !== null ? { correlationId: row.correlationId } : {},
|
|
376
|
+
...row.requestId !== null ? { requestId: row.requestId } : {},
|
|
377
|
+
isSystemCost: row.isSystemCost,
|
|
378
|
+
...row.systemCostCategory !== null ? { systemCostCategory: row.systemCostCategory } : {},
|
|
379
|
+
enforced: row.enforced,
|
|
380
|
+
...row.prevHash !== null ? { prevHash: row.prevHash } : {},
|
|
381
|
+
...row.hash !== null ? { hash: row.hash } : {},
|
|
382
|
+
occurredAt: row.occurredAt,
|
|
383
|
+
createdAt: row.createdAt,
|
|
384
|
+
updatedAt: row.updatedAt
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function serializeUnitRates(units) {
|
|
388
|
+
const out = {};
|
|
389
|
+
for (const [unit, rate] of Object.entries(units)) out[unit] = rate.toString();
|
|
390
|
+
return JSON.stringify(out);
|
|
391
|
+
}
|
|
392
|
+
function parseUnitRates(units) {
|
|
393
|
+
const out = {};
|
|
394
|
+
for (const [unit, rate] of Object.entries(units)) out[unit] = BigInt(rate);
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
function mapPriceRow(row) {
|
|
398
|
+
return {
|
|
399
|
+
id: row.id,
|
|
400
|
+
provider: row.provider,
|
|
401
|
+
model: row.model,
|
|
402
|
+
operation: row.operation,
|
|
403
|
+
serviceTier: row.serviceTier,
|
|
404
|
+
inputNanoUsdPerMillion: row.inputNanoUsdPerMillion,
|
|
405
|
+
outputNanoUsdPerMillion: row.outputNanoUsdPerMillion,
|
|
406
|
+
cacheReadNanoUsdPerMillion: row.cacheReadNanoUsdPerMillion,
|
|
407
|
+
cacheWrite5mNanoUsdPerMillion: row.cacheWrite5mNanoUsdPerMillion,
|
|
408
|
+
cacheWrite1hNanoUsdPerMillion: row.cacheWrite1hNanoUsdPerMillion,
|
|
409
|
+
reasoningNanoUsdPerMillion: row.reasoningNanoUsdPerMillion,
|
|
410
|
+
audioInNanoUsdPerMillion: row.audioInNanoUsdPerMillion,
|
|
411
|
+
audioOutNanoUsdPerMillion: row.audioOutNanoUsdPerMillion,
|
|
412
|
+
imageInNanoUsdPerMillion: row.imageInNanoUsdPerMillion,
|
|
413
|
+
imageOutNanoUsdPerMillion: row.imageOutNanoUsdPerMillion,
|
|
414
|
+
...row.tierThresholdTokens !== null ? { tierThresholdTokens: row.tierThresholdTokens } : {},
|
|
415
|
+
...row.tierInputNanoUsdPerMillion !== null ? { tierInputNanoUsdPerMillion: row.tierInputNanoUsdPerMillion } : {},
|
|
416
|
+
...row.tierOutputNanoUsdPerMillion !== null ? { tierOutputNanoUsdPerMillion: row.tierOutputNanoUsdPerMillion } : {},
|
|
417
|
+
...row.unitRates !== null ? { unitRates: parseUnitRates(row.unitRates) } : {},
|
|
418
|
+
currency: "USD",
|
|
419
|
+
effectiveFrom: row.effectiveFrom,
|
|
420
|
+
effectiveTo: row.effectiveTo,
|
|
421
|
+
source: row.source
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function firstOrThrow(rows) {
|
|
425
|
+
const row = rows[0];
|
|
426
|
+
if (row === void 0) throw new nestAiTokens.AiTokensException("AI_TOKENS_STORE_ERROR", void 0, {});
|
|
427
|
+
return row;
|
|
428
|
+
}
|
|
429
|
+
var PG_UNIQUE_VIOLATION = "23505";
|
|
430
|
+
function isUniqueViolation(error) {
|
|
431
|
+
if (!(error instanceof client.Prisma.PrismaClientKnownRequestError)) return false;
|
|
432
|
+
if (error.code === "P2002") return true;
|
|
433
|
+
const meta = error.meta;
|
|
434
|
+
if (meta?.code === PG_UNIQUE_VIOLATION) return true;
|
|
435
|
+
const cause = meta?.driverAdapterError?.cause;
|
|
436
|
+
return cause?.originalCode === PG_UNIQUE_VIOLATION || cause?.kind === "UniqueConstraintViolation";
|
|
437
|
+
}
|
|
438
|
+
function storeError(error) {
|
|
439
|
+
const code = error instanceof client.Prisma.PrismaClientKnownRequestError ? error.code : void 0;
|
|
440
|
+
return new nestAiTokens.AiTokensException("AI_TOKENS_STORE_ERROR", void 0, code === void 0 ? {} : { code });
|
|
441
|
+
}
|
|
442
|
+
function usageValues(id, record, payloadHash, prevHash, hash) {
|
|
443
|
+
const extraUnits = record.extraUnits === void 0 ? client.Prisma.sql`NULL` : client.Prisma.sql`${JSON.stringify(record.extraUnits)}::jsonb`;
|
|
444
|
+
return [
|
|
445
|
+
client.Prisma.sql`${id}`,
|
|
446
|
+
client.Prisma.sql`${record.tenantId}`,
|
|
447
|
+
client.Prisma.sql`${record.scope.type}`,
|
|
448
|
+
client.Prisma.sql`${record.scope.id}`,
|
|
449
|
+
client.Prisma.sql`${record.beneficiary?.type ?? null}`,
|
|
450
|
+
client.Prisma.sql`${record.beneficiary?.id ?? null}`,
|
|
451
|
+
client.Prisma.sql`${record.requestedBy ?? null}`,
|
|
452
|
+
client.Prisma.sql`${record.provider}`,
|
|
453
|
+
client.Prisma.sql`${record.model}`,
|
|
454
|
+
client.Prisma.sql`${record.requestedModel ?? null}`,
|
|
455
|
+
client.Prisma.sql`${record.operation}`,
|
|
456
|
+
client.Prisma.sql`${record.serviceTier}`,
|
|
457
|
+
client.Prisma.sql`${record.feature}`,
|
|
458
|
+
client.Prisma.sql`${record.tags}::text[]`,
|
|
459
|
+
client.Prisma.sql`${record.inputTokens}`,
|
|
460
|
+
client.Prisma.sql`${record.outputTokens}`,
|
|
461
|
+
client.Prisma.sql`${record.cacheReadTokens}`,
|
|
462
|
+
client.Prisma.sql`${record.cacheWrite5mTokens}`,
|
|
463
|
+
client.Prisma.sql`${record.cacheWrite1hTokens}`,
|
|
464
|
+
client.Prisma.sql`${record.reasoningTokens}`,
|
|
465
|
+
client.Prisma.sql`${record.audioInTokens}`,
|
|
466
|
+
client.Prisma.sql`${record.audioOutTokens}`,
|
|
467
|
+
client.Prisma.sql`${record.imageInTokens}`,
|
|
468
|
+
client.Prisma.sql`${record.imageOutTokens}`,
|
|
469
|
+
client.Prisma.sql`${record.totalTokens}`,
|
|
470
|
+
extraUnits,
|
|
471
|
+
client.Prisma.sql`${record.priceVersionId}`,
|
|
472
|
+
client.Prisma.sql`${record.rawCostNanoUsd}`,
|
|
473
|
+
client.Prisma.sql`${record.surchargeNanoUsd}`,
|
|
474
|
+
client.Prisma.sql`${record.billedCostNanoUsd}`,
|
|
475
|
+
client.Prisma.sql`${record.markupMultiplier}`,
|
|
476
|
+
client.Prisma.sql`${record.currency}`,
|
|
477
|
+
client.Prisma.sql`${record.priceMissing}`,
|
|
478
|
+
client.Prisma.sql`${record.status}`,
|
|
479
|
+
client.Prisma.sql`${record.reversedByRecordId ?? null}`,
|
|
480
|
+
client.Prisma.sql`${record.reversesRecordId ?? null}`,
|
|
481
|
+
client.Prisma.sql`${record.idempotencyKey}`,
|
|
482
|
+
client.Prisma.sql`${payloadHash}`,
|
|
483
|
+
client.Prisma.sql`${record.correlationId ?? null}`,
|
|
484
|
+
client.Prisma.sql`${record.requestId ?? null}`,
|
|
485
|
+
client.Prisma.sql`${record.isSystemCost}`,
|
|
486
|
+
client.Prisma.sql`${record.systemCostCategory ?? null}`,
|
|
487
|
+
client.Prisma.sql`${record.enforced}`,
|
|
488
|
+
client.Prisma.sql`${prevHash}`,
|
|
489
|
+
client.Prisma.sql`${hash}`,
|
|
490
|
+
client.Prisma.sql`${record.occurredAt}`
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
var USAGE_COLUMNS_SQL = client.Prisma.raw(USAGE_COLUMNS.map((column) => `"${column}"`).join(", "));
|
|
494
|
+
var PRICE_COLUMNS = [
|
|
495
|
+
"id",
|
|
496
|
+
"provider",
|
|
497
|
+
"model",
|
|
498
|
+
"operation",
|
|
499
|
+
"serviceTier",
|
|
500
|
+
"inputNanoUsdPerMillion",
|
|
501
|
+
"outputNanoUsdPerMillion",
|
|
502
|
+
"cacheReadNanoUsdPerMillion",
|
|
503
|
+
"cacheWrite5mNanoUsdPerMillion",
|
|
504
|
+
"cacheWrite1hNanoUsdPerMillion",
|
|
505
|
+
"reasoningNanoUsdPerMillion",
|
|
506
|
+
"audioInNanoUsdPerMillion",
|
|
507
|
+
"audioOutNanoUsdPerMillion",
|
|
508
|
+
"imageInNanoUsdPerMillion",
|
|
509
|
+
"imageOutNanoUsdPerMillion",
|
|
510
|
+
"tierThresholdTokens",
|
|
511
|
+
"tierInputNanoUsdPerMillion",
|
|
512
|
+
"tierOutputNanoUsdPerMillion",
|
|
513
|
+
"unitRates",
|
|
514
|
+
"currency",
|
|
515
|
+
"effectiveFrom",
|
|
516
|
+
"effectiveTo",
|
|
517
|
+
"source"
|
|
518
|
+
];
|
|
519
|
+
var PRICE_COLUMNS_SQL = client.Prisma.raw(PRICE_COLUMNS.map((column) => `"${column}"`).join(", "));
|
|
520
|
+
function priceValues(input, serviceTier, effectiveFrom) {
|
|
521
|
+
const unitRates = input.unitRates === void 0 ? client.Prisma.sql`NULL` : client.Prisma.sql`${serializeUnitRates(input.unitRates)}::jsonb`;
|
|
522
|
+
return [
|
|
523
|
+
client.Prisma.sql`${crypto.randomUUID()}`,
|
|
524
|
+
client.Prisma.sql`${input.provider}`,
|
|
525
|
+
client.Prisma.sql`${input.model}`,
|
|
526
|
+
client.Prisma.sql`${input.operation}`,
|
|
527
|
+
client.Prisma.sql`${serviceTier}`,
|
|
528
|
+
client.Prisma.sql`${input.inputNanoUsdPerMillion ?? 0n}`,
|
|
529
|
+
client.Prisma.sql`${input.outputNanoUsdPerMillion ?? 0n}`,
|
|
530
|
+
client.Prisma.sql`${input.cacheReadNanoUsdPerMillion ?? 0n}`,
|
|
531
|
+
client.Prisma.sql`${input.cacheWrite5mNanoUsdPerMillion ?? 0n}`,
|
|
532
|
+
client.Prisma.sql`${input.cacheWrite1hNanoUsdPerMillion ?? 0n}`,
|
|
533
|
+
client.Prisma.sql`${input.reasoningNanoUsdPerMillion ?? 0n}`,
|
|
534
|
+
client.Prisma.sql`${input.audioInNanoUsdPerMillion ?? 0n}`,
|
|
535
|
+
client.Prisma.sql`${input.audioOutNanoUsdPerMillion ?? 0n}`,
|
|
536
|
+
client.Prisma.sql`${input.imageInNanoUsdPerMillion ?? 0n}`,
|
|
537
|
+
client.Prisma.sql`${input.imageOutNanoUsdPerMillion ?? 0n}`,
|
|
538
|
+
client.Prisma.sql`${input.tierThresholdTokens ?? null}`,
|
|
539
|
+
client.Prisma.sql`${input.tierInputNanoUsdPerMillion ?? null}`,
|
|
540
|
+
client.Prisma.sql`${input.tierOutputNanoUsdPerMillion ?? null}`,
|
|
541
|
+
unitRates,
|
|
542
|
+
client.Prisma.sql`${"USD"}`,
|
|
543
|
+
client.Prisma.sql`${effectiveFrom}`,
|
|
544
|
+
client.Prisma.sql`${null}`,
|
|
545
|
+
client.Prisma.sql`${input.source ?? "snapshot"}`
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
function statusAssignments(from, to, patch, prevHash, hash) {
|
|
549
|
+
const parts = [client.Prisma.sql`"status" = ${to}`, client.Prisma.sql`"updatedAt" = CURRENT_TIMESTAMP`];
|
|
550
|
+
if (patch !== void 0) {
|
|
551
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
552
|
+
if (!isLegalTransitionPatchKey(from, to, key)) {
|
|
553
|
+
throw new nestAiTokens.AiTokensException("AI_TOKENS_IDEMPOTENCY_CONFLICT", void 0, {
|
|
554
|
+
reason: `${from} \u2192 ${to} may not patch "${key}"`
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
parts.push(client.Prisma.sql`${client.Prisma.raw(`"${key}"`)} = ${value}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (hash !== null) {
|
|
561
|
+
parts.push(client.Prisma.sql`"prevHash" = ${prevHash}`, client.Prisma.sql`"hash" = ${hash}`);
|
|
562
|
+
}
|
|
563
|
+
return client.Prisma.join(parts, ", ");
|
|
564
|
+
}
|
|
565
|
+
function buildWhere(filter) {
|
|
566
|
+
const parts = [client.Prisma.sql`"tenantId" = ${filter.tenantId}`];
|
|
567
|
+
if (filter.scope !== void 0) parts.push(client.Prisma.sql`"scopeType" = ${filter.scope.type} AND "scopeId" = ${filter.scope.id}`);
|
|
568
|
+
if (filter.beneficiary !== void 0) {
|
|
569
|
+
parts.push(client.Prisma.sql`"beneficiaryType" = ${filter.beneficiary.type} AND "beneficiaryId" = ${filter.beneficiary.id}`);
|
|
570
|
+
}
|
|
571
|
+
if (filter.feature !== void 0) parts.push(client.Prisma.sql`"feature" = ${filter.feature}`);
|
|
572
|
+
if (filter.features !== void 0) parts.push(client.Prisma.sql`"feature" = ANY(${filter.features}::text[])`);
|
|
573
|
+
if (filter.provider !== void 0) parts.push(client.Prisma.sql`"provider" = ${filter.provider}`);
|
|
574
|
+
if (filter.model !== void 0) parts.push(client.Prisma.sql`"model" = ${filter.model}`);
|
|
575
|
+
if (filter.operation !== void 0) parts.push(client.Prisma.sql`"operation" = ${filter.operation}`);
|
|
576
|
+
if (filter.serviceTier !== void 0) parts.push(client.Prisma.sql`"serviceTier" = ${filter.serviceTier}`);
|
|
577
|
+
if (filter.tags !== void 0) parts.push(client.Prisma.sql`"tags" @> ${filter.tags}::text[]`);
|
|
578
|
+
if (filter.isSystemCost !== void 0) parts.push(client.Prisma.sql`"isSystemCost" = ${filter.isSystemCost}`);
|
|
579
|
+
if (filter.systemCostCategory !== void 0) parts.push(client.Prisma.sql`"systemCostCategory" = ${filter.systemCostCategory}`);
|
|
580
|
+
if (filter.status !== void 0) parts.push(client.Prisma.sql`"status" = ANY(${filter.status}::text[])`);
|
|
581
|
+
if (filter.enforcedOnly === true) parts.push(client.Prisma.sql`"enforced" = true`);
|
|
582
|
+
if (filter.from !== void 0) parts.push(client.Prisma.sql`"occurredAt" >= ${filter.from}`);
|
|
583
|
+
if (filter.to !== void 0) parts.push(client.Prisma.sql`"occurredAt" <= ${filter.to}`);
|
|
584
|
+
return client.Prisma.join(parts, " AND ");
|
|
585
|
+
}
|
|
586
|
+
var WalletMissingError = class extends Error {
|
|
587
|
+
isWalletMissing = true;
|
|
588
|
+
constructor() {
|
|
589
|
+
super("wallet does not exist");
|
|
590
|
+
this.name = "WalletMissingError";
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
function isWalletMissing(error) {
|
|
594
|
+
if (typeof error !== "object" || error === null) return false;
|
|
595
|
+
return error.isWalletMissing === true;
|
|
596
|
+
}
|
|
597
|
+
function mapWalletRow(row) {
|
|
598
|
+
return {
|
|
599
|
+
id: row.id,
|
|
600
|
+
tenantId: row.tenantId,
|
|
601
|
+
ownerType: row.ownerType,
|
|
602
|
+
ownerId: row.ownerId,
|
|
603
|
+
balanceNanoUsd: row.balanceNanoUsd,
|
|
604
|
+
createdAt: row.createdAt,
|
|
605
|
+
updatedAt: row.updatedAt
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function mapWalletEntryRow(row) {
|
|
609
|
+
return {
|
|
610
|
+
id: row.id,
|
|
611
|
+
walletId: row.walletId,
|
|
612
|
+
type: row.type,
|
|
613
|
+
amountNanoUsd: row.amountNanoUsd,
|
|
614
|
+
priority: row.priority,
|
|
615
|
+
effectiveAt: row.effectiveAt,
|
|
616
|
+
...row.expiresAt !== null ? { expiresAt: row.expiresAt } : {},
|
|
617
|
+
...row.usageRecordId !== null ? { usageRecordId: row.usageRecordId } : {},
|
|
618
|
+
idempotencyKey: row.idempotencyKey,
|
|
619
|
+
...row.reason !== null ? { reason: row.reason } : {},
|
|
620
|
+
createdAt: row.createdAt
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function mapOpenGrantRow(row) {
|
|
624
|
+
return { ...mapWalletEntryRow(row), remainingNanoUsd: row.remaining };
|
|
625
|
+
}
|
|
626
|
+
function walletAutoCreatable(entry) {
|
|
627
|
+
return entry.type === "grant" || entry.type === "adjustment" && entry.amountNanoUsd > 0n;
|
|
628
|
+
}
|
|
629
|
+
function walletBalanceDelta(entry) {
|
|
630
|
+
if (entry.type !== "grant") return entry.amountNanoUsd;
|
|
631
|
+
const spendable = entry.effectiveAt <= entry.createdAt && (entry.expiresAt === void 0 || entry.expiresAt > entry.createdAt);
|
|
632
|
+
return spendable ? entry.amountNanoUsd : 0n;
|
|
633
|
+
}
|
|
634
|
+
function walletEntryMatches(row, entry) {
|
|
635
|
+
return row.type === entry.type && row.amountNanoUsd === entry.amountNanoUsd && row.priority === entry.priority && (row.expiresAt?.getTime() ?? null) === (entry.expiresAt?.getTime() ?? null) && (row.usageRecordId ?? null) === (entry.usageRecordId ?? null) && (row.reason ?? null) === (entry.reason ?? null);
|
|
636
|
+
}
|
|
637
|
+
function walletEntryWhere(walletId, filter) {
|
|
638
|
+
const parts = [client.Prisma.sql`"walletId" = ${walletId}`];
|
|
639
|
+
if (filter.type !== void 0) parts.push(client.Prisma.sql`"type" = ${filter.type}`);
|
|
640
|
+
if (filter.from !== void 0) parts.push(client.Prisma.sql`"createdAt" >= ${filter.from}`);
|
|
641
|
+
if (filter.to !== void 0) parts.push(client.Prisma.sql`"createdAt" <= ${filter.to}`);
|
|
642
|
+
return client.Prisma.join(parts, " AND ");
|
|
643
|
+
}
|
|
644
|
+
function spendableBalanceSql(now) {
|
|
645
|
+
return client.Prisma.sql`(
|
|
646
|
+
SELECT COALESCE(SUM(CASE WHEN e."type" = 'grant'
|
|
647
|
+
THEN (CASE WHEN e."effectiveAt" <= ${now} THEN e."amountNanoUsd" ELSE 0 END)
|
|
648
|
+
ELSE e."amountNanoUsd" END), 0)
|
|
649
|
+
FROM "ai_wallet_entries" e WHERE e."walletId" = w."id"
|
|
650
|
+
) - (
|
|
651
|
+
SELECT COALESCE(SUM(g."amountNanoUsd" - COALESCE((SELECT SUM(a."amountNanoUsd") FROM "ai_wallet_debit_allocations" a WHERE a."grantEntryId" = g."id"), 0)), 0)
|
|
652
|
+
FROM "ai_wallet_entries" g
|
|
653
|
+
WHERE g."walletId" = w."id" AND g."type" = 'grant' AND g."effectiveAt" <= ${now}
|
|
654
|
+
AND g."expiresAt" IS NOT NULL AND g."expiresAt" <= ${now}
|
|
655
|
+
)`;
|
|
656
|
+
}
|
|
657
|
+
function serializeWindow(window) {
|
|
658
|
+
return typeof window === "object" ? `custom:${window.customSeconds.toString()}` : window;
|
|
659
|
+
}
|
|
660
|
+
function parseWindow(value) {
|
|
661
|
+
if (value.startsWith("custom:")) return { customSeconds: Number(value.slice("custom:".length)) };
|
|
662
|
+
return value;
|
|
663
|
+
}
|
|
664
|
+
function mapBudgetRow(row) {
|
|
665
|
+
return {
|
|
666
|
+
id: row.id,
|
|
667
|
+
tenantId: row.tenantId,
|
|
668
|
+
scope: { type: row.scopeType, id: row.scopeId },
|
|
669
|
+
...row.features.length > 0 ? { features: row.features } : {},
|
|
670
|
+
...row.limitNanoUsd !== null ? { limitNanoUsd: row.limitNanoUsd } : {},
|
|
671
|
+
...row.limitTokens !== null ? { limitTokens: Number(row.limitTokens) } : {},
|
|
672
|
+
...row.limitCount !== null ? { limitCount: row.limitCount } : {},
|
|
673
|
+
window: parseWindow(row.window),
|
|
674
|
+
...row.anchorAt !== null ? { anchorAt: row.anchorAt } : {},
|
|
675
|
+
...row.expiresAt !== null ? { expiresAt: row.expiresAt } : {},
|
|
676
|
+
softThresholds: row.softThresholds,
|
|
677
|
+
policy: row.policy,
|
|
678
|
+
createdAt: row.createdAt
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function mapBudgetWindowRow(row) {
|
|
682
|
+
return { spentNanoUsd: row.spentNanoUsd, spentTokens: Number(row.spentTokens), spentCount: row.spentCount };
|
|
683
|
+
}
|
|
684
|
+
var BUDGET_COLUMNS = [
|
|
685
|
+
"id",
|
|
686
|
+
"tenantId",
|
|
687
|
+
"scopeType",
|
|
688
|
+
"scopeId",
|
|
689
|
+
"features",
|
|
690
|
+
"limitNanoUsd",
|
|
691
|
+
"limitTokens",
|
|
692
|
+
"limitCount",
|
|
693
|
+
"window",
|
|
694
|
+
"anchorAt",
|
|
695
|
+
"expiresAt",
|
|
696
|
+
"softThresholds",
|
|
697
|
+
"policy"
|
|
698
|
+
];
|
|
699
|
+
var BUDGET_COLUMNS_SQL = client.Prisma.raw(BUDGET_COLUMNS.map((column) => `"${column}"`).join(", "));
|
|
700
|
+
function tokenLimitSql(limit) {
|
|
701
|
+
return limit === void 0 ? null : BigInt(limit);
|
|
702
|
+
}
|
|
703
|
+
function budgetValues(id, input) {
|
|
704
|
+
return [
|
|
705
|
+
client.Prisma.sql`${id}`,
|
|
706
|
+
client.Prisma.sql`${input.tenantId}`,
|
|
707
|
+
client.Prisma.sql`${input.scope.type}`,
|
|
708
|
+
client.Prisma.sql`${input.scope.id}`,
|
|
709
|
+
client.Prisma.sql`${input.features ?? []}::text[]`,
|
|
710
|
+
client.Prisma.sql`${input.limitNanoUsd ?? null}`,
|
|
711
|
+
client.Prisma.sql`${tokenLimitSql(input.limitTokens)}`,
|
|
712
|
+
client.Prisma.sql`${input.limitCount ?? null}`,
|
|
713
|
+
client.Prisma.sql`${serializeWindow(input.window)}`,
|
|
714
|
+
client.Prisma.sql`${input.anchorAt ?? null}`,
|
|
715
|
+
client.Prisma.sql`${input.expiresAt ?? null}`,
|
|
716
|
+
client.Prisma.sql`${JSON.stringify(input.softThresholds)}::jsonb`,
|
|
717
|
+
client.Prisma.sql`${input.policy}`
|
|
718
|
+
];
|
|
719
|
+
}
|
|
720
|
+
function budgetUpdateAssignments(input) {
|
|
721
|
+
return client.Prisma.join(
|
|
722
|
+
[
|
|
723
|
+
client.Prisma.sql`"scopeType" = ${input.scope.type}`,
|
|
724
|
+
client.Prisma.sql`"scopeId" = ${input.scope.id}`,
|
|
725
|
+
client.Prisma.sql`"features" = ${input.features ?? []}::text[]`,
|
|
726
|
+
client.Prisma.sql`"limitNanoUsd" = ${input.limitNanoUsd ?? null}`,
|
|
727
|
+
client.Prisma.sql`"limitTokens" = ${tokenLimitSql(input.limitTokens)}`,
|
|
728
|
+
client.Prisma.sql`"limitCount" = ${input.limitCount ?? null}`,
|
|
729
|
+
client.Prisma.sql`"window" = ${serializeWindow(input.window)}`,
|
|
730
|
+
client.Prisma.sql`"anchorAt" = ${input.anchorAt ?? null}`,
|
|
731
|
+
client.Prisma.sql`"expiresAt" = ${input.expiresAt ?? null}`,
|
|
732
|
+
client.Prisma.sql`"softThresholds" = ${JSON.stringify(input.softThresholds)}::jsonb`,
|
|
733
|
+
client.Prisma.sql`"policy" = ${input.policy}`,
|
|
734
|
+
client.Prisma.sql`"updatedAt" = CURRENT_TIMESTAMP`
|
|
735
|
+
],
|
|
736
|
+
", "
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/prisma/index.ts
|
|
741
|
+
var PrismaAiTokensStore = class {
|
|
742
|
+
/**
|
|
743
|
+
* @param prisma The host's Prisma client (talks to PostgreSQL).
|
|
744
|
+
* @param options Optional wallet grant burn order (must match the module's `wallets.burnOrder`; default `'expiry'`).
|
|
745
|
+
*/
|
|
746
|
+
constructor(prisma, options = {}) {
|
|
747
|
+
this.prisma = prisma;
|
|
748
|
+
this.burnOrder = options.burnOrder ?? "expiry";
|
|
749
|
+
}
|
|
750
|
+
prisma;
|
|
751
|
+
burnOrder;
|
|
752
|
+
async append(record, payloadHash, hashChain) {
|
|
753
|
+
try {
|
|
754
|
+
if (hashChain === true && record.status === "posted") {
|
|
755
|
+
return await this.prisma.$transaction((tx) => this.insertChained(tx, record, payloadHash));
|
|
756
|
+
}
|
|
757
|
+
return await this.insert(this.prisma, crypto.randomUUID(), record, payloadHash, null, null);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (isUniqueViolation(error)) return this.replayOrConflict(record, payloadHash);
|
|
760
|
+
throw storeError(error);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
transition(id, from, to, patch, hashChain) {
|
|
764
|
+
if (hashChain === true && to === "posted") return this.settleChained(id, from, patch);
|
|
765
|
+
return this.updateStatus(this.prisma, id, from, to, patch, null, null);
|
|
766
|
+
}
|
|
767
|
+
async findByIdempotencyKey(tenantId, key) {
|
|
768
|
+
const rows = await this.prisma.$queryRaw(
|
|
769
|
+
client.Prisma.sql`SELECT * FROM "ai_usage_records" WHERE "tenantId" = ${tenantId} AND "idempotencyKey" = ${key} LIMIT 1`
|
|
770
|
+
);
|
|
771
|
+
return rows[0] === void 0 ? null : mapUsageRow(rows[0]);
|
|
772
|
+
}
|
|
773
|
+
async findById(id) {
|
|
774
|
+
const rows = await this.prisma.$queryRaw(
|
|
775
|
+
client.Prisma.sql`SELECT * FROM "ai_usage_records" WHERE "id" = ${id} LIMIT 1`
|
|
776
|
+
);
|
|
777
|
+
return rows[0] === void 0 ? null : mapUsageRow(rows[0]);
|
|
778
|
+
}
|
|
779
|
+
async findExpiredHolds(olderThan, limit) {
|
|
780
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
781
|
+
SELECT * FROM "ai_usage_records"
|
|
782
|
+
WHERE "status" = 'pending' AND "createdAt" < ${olderThan}
|
|
783
|
+
ORDER BY "createdAt" ASC LIMIT ${limit}
|
|
784
|
+
`);
|
|
785
|
+
return rows.map(mapUsageRow);
|
|
786
|
+
}
|
|
787
|
+
async query(filter) {
|
|
788
|
+
const limit = filter.limit === void 0 ? client.Prisma.empty : client.Prisma.sql`LIMIT ${filter.limit}`;
|
|
789
|
+
const offset = filter.offset === void 0 ? client.Prisma.empty : client.Prisma.sql`OFFSET ${filter.offset}`;
|
|
790
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
791
|
+
SELECT * FROM "ai_usage_records" WHERE ${buildWhere(filter)}
|
|
792
|
+
ORDER BY "createdAt" ASC ${limit} ${offset}
|
|
793
|
+
`);
|
|
794
|
+
return rows.map(mapUsageRow);
|
|
795
|
+
}
|
|
796
|
+
async sumCost(filter) {
|
|
797
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
798
|
+
SELECT
|
|
799
|
+
COALESCE(SUM("rawCostNanoUsd"), 0)::bigint AS "raw",
|
|
800
|
+
COALESCE(SUM("billedCostNanoUsd"), 0)::bigint AS "billed",
|
|
801
|
+
COALESCE(SUM("surchargeNanoUsd"), 0)::bigint AS "surcharge",
|
|
802
|
+
COALESCE(SUM("totalTokens"), 0)::bigint AS "tokens",
|
|
803
|
+
COUNT(*)::int AS "records"
|
|
804
|
+
FROM "ai_usage_records" WHERE ${buildWhere(filter)}
|
|
805
|
+
`);
|
|
806
|
+
const row = rows[0] ?? { raw: 0n, billed: 0n, surcharge: 0n, tokens: 0n, records: 0 };
|
|
807
|
+
return {
|
|
808
|
+
rawCostNanoUsd: row.raw,
|
|
809
|
+
billedCostNanoUsd: row.billed,
|
|
810
|
+
surchargeNanoUsd: row.surcharge,
|
|
811
|
+
totalTokens: Number(row.tokens),
|
|
812
|
+
records: row.records
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
lastHash(tenantId) {
|
|
816
|
+
return this.lastHashWithin(this.prisma, tenantId);
|
|
817
|
+
}
|
|
818
|
+
async resolveRate(provider, model, operation, serviceTier, at) {
|
|
819
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
820
|
+
SELECT * FROM "ai_model_prices"
|
|
821
|
+
WHERE "provider" = ${provider} AND "model" = ${model} AND "operation" = ${operation}
|
|
822
|
+
AND "serviceTier" = ${serviceTier} AND "effectiveFrom" <= ${at}
|
|
823
|
+
AND ("effectiveTo" IS NULL OR "effectiveTo" >= ${at})
|
|
824
|
+
ORDER BY "effectiveFrom" DESC LIMIT 1
|
|
825
|
+
`);
|
|
826
|
+
return rows[0] === void 0 ? null : mapPriceRow(rows[0]);
|
|
827
|
+
}
|
|
828
|
+
async upsertPrice(input) {
|
|
829
|
+
const serviceTier = input.serviceTier ?? "standard";
|
|
830
|
+
const effectiveFrom = input.effectiveFrom ?? /* @__PURE__ */ new Date();
|
|
831
|
+
const key = `${input.provider}|${input.model}|${input.operation}|${serviceTier}`;
|
|
832
|
+
return this.prisma.$transaction(async (tx) => {
|
|
833
|
+
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${key})::bigint)`;
|
|
834
|
+
await tx.$executeRaw(client.Prisma.sql`
|
|
835
|
+
UPDATE "ai_model_prices" SET "effectiveTo" = ${effectiveFrom}
|
|
836
|
+
WHERE "provider" = ${input.provider} AND "model" = ${input.model} AND "operation" = ${input.operation}
|
|
837
|
+
AND "serviceTier" = ${serviceTier} AND "effectiveTo" IS NULL
|
|
838
|
+
`);
|
|
839
|
+
const rows = await tx.$queryRaw(client.Prisma.sql`
|
|
840
|
+
INSERT INTO "ai_model_prices" (${PRICE_COLUMNS_SQL}) VALUES (${client.Prisma.join(priceValues(input, serviceTier, effectiveFrom))})
|
|
841
|
+
RETURNING *
|
|
842
|
+
`);
|
|
843
|
+
return mapPriceRow(firstOrThrow(rows));
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
async getPriceHistory(provider, model, operation, serviceTier) {
|
|
847
|
+
const tier = serviceTier === void 0 ? client.Prisma.empty : client.Prisma.sql`AND "serviceTier" = ${serviceTier}`;
|
|
848
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
849
|
+
SELECT * FROM "ai_model_prices"
|
|
850
|
+
WHERE "provider" = ${provider} AND "model" = ${model} AND "operation" = ${operation} ${tier}
|
|
851
|
+
ORDER BY "effectiveFrom" DESC
|
|
852
|
+
`);
|
|
853
|
+
return rows.map(mapPriceRow);
|
|
854
|
+
}
|
|
855
|
+
async listModels(provider) {
|
|
856
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
857
|
+
SELECT DISTINCT "model", "operation", "serviceTier" FROM "ai_model_prices" WHERE "provider" = ${provider}
|
|
858
|
+
`);
|
|
859
|
+
return rows.map((row) => ({ model: row.model, operation: row.operation, serviceTier: row.serviceTier }));
|
|
860
|
+
}
|
|
861
|
+
/** Advisory seed lock (§6.4): the first caller wins; concurrent boots seed exactly once. */
|
|
862
|
+
async acquireSeedLock(key) {
|
|
863
|
+
const rows = await this.prisma.$queryRaw(
|
|
864
|
+
client.Prisma.sql`SELECT pg_try_advisory_lock(hashtext(${key})::bigint) AS "acquired"`
|
|
865
|
+
);
|
|
866
|
+
return rows[0]?.acquired ?? false;
|
|
867
|
+
}
|
|
868
|
+
/** Insert one usage row and return it; shared by the plain and chained paths. */
|
|
869
|
+
async insert(executor, id, record, payloadHash, prevHash, hash) {
|
|
870
|
+
const values = client.Prisma.join(usageValues(id, record, payloadHash, prevHash, hash));
|
|
871
|
+
const rows = await executor.$queryRaw(
|
|
872
|
+
client.Prisma.sql`INSERT INTO "ai_usage_records" (${USAGE_COLUMNS_SQL}) VALUES (${values}) RETURNING *`
|
|
873
|
+
);
|
|
874
|
+
return mapUsageRow(firstOrThrow(rows));
|
|
875
|
+
}
|
|
876
|
+
/** Insert a `posted` row under the per-tenant chain lock (§8.6). */
|
|
877
|
+
async insertChained(tx, record, payloadHash) {
|
|
878
|
+
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${record.tenantId})::bigint)`;
|
|
879
|
+
const prevHash = await this.lastHashWithin(tx, record.tenantId);
|
|
880
|
+
const id = crypto.randomUUID();
|
|
881
|
+
const hash = chainHash(prevHash, { ...record, id });
|
|
882
|
+
return this.insert(tx, id, record, payloadHash, prevHash, hash);
|
|
883
|
+
}
|
|
884
|
+
/** Settle a hold `pending → posted`, computing its chain hash under the lock (§8.6). */
|
|
885
|
+
settleChained(id, from, patch) {
|
|
886
|
+
return this.prisma.$transaction(async (tx) => {
|
|
887
|
+
const rows = await tx.$queryRaw(
|
|
888
|
+
client.Prisma.sql`SELECT * FROM "ai_usage_records" WHERE "id" = ${id} AND "status" = ${from} FOR UPDATE`
|
|
889
|
+
);
|
|
890
|
+
if (rows[0] === void 0) return null;
|
|
891
|
+
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${rows[0].tenantId})::bigint)`;
|
|
892
|
+
const prevHash = await this.lastHashWithin(tx, rows[0].tenantId);
|
|
893
|
+
const settled = { ...mapUsageRow(rows[0]), ...patch};
|
|
894
|
+
return this.updateStatus(tx, id, from, "posted", patch, prevHash, chainHash(prevHash, settled));
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
/** Atomic status transition via a conditional `UPDATE`; 0 rows → `null` (§8.3). */
|
|
898
|
+
async updateStatus(executor, id, from, to, patch, prevHash, hash) {
|
|
899
|
+
const assignments = statusAssignments(from, to, patch, prevHash, hash);
|
|
900
|
+
const rows = await executor.$queryRaw(
|
|
901
|
+
client.Prisma.sql`UPDATE "ai_usage_records" SET ${assignments} WHERE "id" = ${id} AND "status" = ${from} RETURNING *`
|
|
902
|
+
);
|
|
903
|
+
return rows[0] === void 0 ? null : mapUsageRow(rows[0]);
|
|
904
|
+
}
|
|
905
|
+
/** The tenant's last chain hash (most recently created settled row). */
|
|
906
|
+
async lastHashWithin(executor, tenantId) {
|
|
907
|
+
const rows = await executor.$queryRaw(client.Prisma.sql`
|
|
908
|
+
SELECT "hash" FROM "ai_usage_records"
|
|
909
|
+
WHERE "tenantId" = ${tenantId} AND "hash" IS NOT NULL
|
|
910
|
+
ORDER BY "createdAt" DESC, "id" DESC LIMIT 1
|
|
911
|
+
`);
|
|
912
|
+
return rows[0]?.hash ?? null;
|
|
913
|
+
}
|
|
914
|
+
async getWallet(ref) {
|
|
915
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
916
|
+
SELECT * FROM "ai_wallets"
|
|
917
|
+
WHERE "tenantId" = ${ref.tenantId} AND "ownerType" = ${ref.ownerType} AND "ownerId" = ${ref.ownerId} LIMIT 1
|
|
918
|
+
`);
|
|
919
|
+
return rows[0] === void 0 ? null : mapWalletRow(rows[0]);
|
|
920
|
+
}
|
|
921
|
+
async appendEntry(ref, entry, allocations = []) {
|
|
922
|
+
try {
|
|
923
|
+
return await this.prisma.$transaction(async (tx) => {
|
|
924
|
+
const wallet = await this.ensureWallet(tx, ref, walletAutoCreatable(entry));
|
|
925
|
+
if (wallet === null) throw new WalletMissingError();
|
|
926
|
+
const stored = await this.insertWalletEntry(tx, wallet.id, entry);
|
|
927
|
+
await this.insertAllocations(tx, stored.id, allocations);
|
|
928
|
+
await this.applyBalanceDelta(tx, wallet.id, walletBalanceDelta(stored));
|
|
929
|
+
return stored;
|
|
930
|
+
});
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (isUniqueViolation(error)) return this.replayOrConflictWallet(ref, entry);
|
|
933
|
+
if (isWalletMissing(error)) throw error;
|
|
934
|
+
throw storeError(error);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async conditionalDebit(ref, entry, overdraftNanoUsd) {
|
|
938
|
+
try {
|
|
939
|
+
return await this.prisma.$transaction(async (tx) => {
|
|
940
|
+
const wallet = await this.getWalletWithin(tx, ref) ?? null;
|
|
941
|
+
if (wallet === null) return null;
|
|
942
|
+
const replay = await this.walletReplay(tx, wallet.id, ref, entry);
|
|
943
|
+
if (replay !== void 0) return replay;
|
|
944
|
+
await this.sweepExpiredGrants(tx, wallet.id);
|
|
945
|
+
const cost = -entry.amountNanoUsd;
|
|
946
|
+
const reserved = await tx.$queryRaw(client.Prisma.sql`
|
|
947
|
+
UPDATE "ai_wallets" SET "balanceNanoUsd" = "balanceNanoUsd" - ${cost}, "updatedAt" = CURRENT_TIMESTAMP
|
|
948
|
+
WHERE "id" = ${wallet.id} AND "balanceNanoUsd" - ${cost} >= ${-overdraftNanoUsd} RETURNING *
|
|
949
|
+
`);
|
|
950
|
+
if (reserved[0] === void 0) return null;
|
|
951
|
+
const debit = await this.insertWalletEntry(tx, wallet.id, entry);
|
|
952
|
+
await this.insertAllocations(tx, debit.id, await this.burnDown(tx, wallet.id, cost));
|
|
953
|
+
return debit;
|
|
954
|
+
});
|
|
955
|
+
} catch (error) {
|
|
956
|
+
if (isLedgerIdempotencyConflict(error)) throw error;
|
|
957
|
+
if (isUniqueViolation(error)) return this.replayOrConflictWallet(ref, entry);
|
|
958
|
+
throw storeError(error);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async openGrants(ref, order) {
|
|
962
|
+
const wallet = await this.getWallet(ref);
|
|
963
|
+
if (wallet === null) return [];
|
|
964
|
+
const rows = await this.prisma.$queryRaw(this.openGrantsSql(wallet.id, order));
|
|
965
|
+
return rows.map(mapOpenGrantRow);
|
|
966
|
+
}
|
|
967
|
+
async listEntries(ref, filter = {}) {
|
|
968
|
+
const wallet = await this.getWallet(ref);
|
|
969
|
+
if (wallet === null) return { entries: [], total: 0 };
|
|
970
|
+
const where = walletEntryWhere(wallet.id, filter);
|
|
971
|
+
const limit = filter.limit === void 0 ? client.Prisma.empty : client.Prisma.sql`LIMIT ${filter.limit}`;
|
|
972
|
+
const offset = filter.offset === void 0 ? client.Prisma.empty : client.Prisma.sql`OFFSET ${filter.offset}`;
|
|
973
|
+
const [entries, counts] = await Promise.all([
|
|
974
|
+
this.prisma.$queryRaw(
|
|
975
|
+
client.Prisma.sql`SELECT * FROM "ai_wallet_entries" WHERE ${where} ORDER BY "createdAt" ASC ${limit} ${offset}`
|
|
976
|
+
),
|
|
977
|
+
this.prisma.$queryRaw(client.Prisma.sql`SELECT COUNT(*)::bigint AS "total" FROM "ai_wallet_entries" WHERE ${where}`)
|
|
978
|
+
]);
|
|
979
|
+
return { entries: entries.map(mapWalletEntryRow), total: Number(counts[0]?.total ?? 0n) };
|
|
980
|
+
}
|
|
981
|
+
async reconcile(ref) {
|
|
982
|
+
const wallet = await this.getWallet(ref);
|
|
983
|
+
if (wallet === null) throw new nestAiTokens.AiTokensException("AI_TOKENS_INSUFFICIENT_CREDITS", void 0, { reason: "wallet does not exist" });
|
|
984
|
+
const now = /* @__PURE__ */ new Date();
|
|
985
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
986
|
+
UPDATE "ai_wallets" w SET "balanceNanoUsd" = ${spendableBalanceSql(now)}, "updatedAt" = CURRENT_TIMESTAMP
|
|
987
|
+
WHERE w."id" = ${wallet.id} RETURNING *
|
|
988
|
+
`);
|
|
989
|
+
return mapWalletRow(firstOrThrow(rows));
|
|
990
|
+
}
|
|
991
|
+
async upsert(input) {
|
|
992
|
+
const id = input.id ?? crypto.randomUUID();
|
|
993
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
994
|
+
INSERT INTO "ai_budgets" (${BUDGET_COLUMNS_SQL}) VALUES (${client.Prisma.join(budgetValues(id, input))})
|
|
995
|
+
ON CONFLICT ("id") DO UPDATE SET ${budgetUpdateAssignments(input)} RETURNING *
|
|
996
|
+
`);
|
|
997
|
+
return mapBudgetRow(firstOrThrow(rows));
|
|
998
|
+
}
|
|
999
|
+
async remove(budgetId) {
|
|
1000
|
+
await this.prisma.$transaction([
|
|
1001
|
+
this.prisma.$executeRaw`DELETE FROM "ai_budget_windows" WHERE "budgetId" = ${budgetId}`,
|
|
1002
|
+
this.prisma.$executeRaw`DELETE FROM "ai_budgets" WHERE "id" = ${budgetId}`
|
|
1003
|
+
]);
|
|
1004
|
+
}
|
|
1005
|
+
async findBudgetById(budgetId) {
|
|
1006
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`SELECT * FROM "ai_budgets" WHERE "id" = ${budgetId} LIMIT 1`);
|
|
1007
|
+
return rows[0] === void 0 ? null : mapBudgetRow(rows[0]);
|
|
1008
|
+
}
|
|
1009
|
+
async findMatching(tenantId, scope) {
|
|
1010
|
+
const now = /* @__PURE__ */ new Date();
|
|
1011
|
+
const rows = await this.prisma.$queryRaw(client.Prisma.sql`
|
|
1012
|
+
SELECT * FROM "ai_budgets"
|
|
1013
|
+
WHERE "tenantId" = ${tenantId}
|
|
1014
|
+
AND ("scopeType" = 'tenant' OR ("scopeType" = ${scope.type} AND "scopeId" = ${scope.id}))
|
|
1015
|
+
AND ("expiresAt" IS NULL OR "expiresAt" > ${now})
|
|
1016
|
+
`);
|
|
1017
|
+
return rows.map(mapBudgetRow);
|
|
1018
|
+
}
|
|
1019
|
+
async conditionalConsume(budgetId, windowStart, delta, limits) {
|
|
1020
|
+
return this.prisma.$transaction(async (tx) => {
|
|
1021
|
+
await this.ensureWindow(tx, budgetId, windowStart);
|
|
1022
|
+
const rows = await tx.$queryRaw(client.Prisma.sql`
|
|
1023
|
+
UPDATE "ai_budget_windows"
|
|
1024
|
+
SET "spentNanoUsd" = "spentNanoUsd" + ${delta.nanoUsd},
|
|
1025
|
+
"spentTokens" = "spentTokens" + ${BigInt(delta.tokens)},
|
|
1026
|
+
"spentCount" = "spentCount" + ${delta.count},
|
|
1027
|
+
"updatedAt" = CURRENT_TIMESTAMP
|
|
1028
|
+
WHERE "budgetId" = ${budgetId} AND "windowStart" = ${windowStart}
|
|
1029
|
+
AND (${limits.nanoUsd ?? null}::bigint IS NULL OR "spentNanoUsd" + ${delta.nanoUsd} <= ${limits.nanoUsd ?? null})
|
|
1030
|
+
AND (${limits.tokens === void 0 ? null : BigInt(limits.tokens)}::bigint IS NULL OR "spentTokens" + ${BigInt(delta.tokens)} <= ${limits.tokens === void 0 ? null : BigInt(limits.tokens)})
|
|
1031
|
+
AND (${limits.count ?? null}::int IS NULL OR "spentCount" + ${delta.count} <= ${limits.count ?? null})
|
|
1032
|
+
RETURNING "id"
|
|
1033
|
+
`);
|
|
1034
|
+
return rows[0] !== void 0;
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
async adjustWindow(budgetId, windowStart, delta) {
|
|
1038
|
+
await this.prisma.$transaction(async (tx) => {
|
|
1039
|
+
await this.ensureWindow(tx, budgetId, windowStart);
|
|
1040
|
+
await tx.$executeRaw(client.Prisma.sql`
|
|
1041
|
+
UPDATE "ai_budget_windows"
|
|
1042
|
+
SET "spentNanoUsd" = GREATEST(0, "spentNanoUsd" + ${delta.nanoUsd}),
|
|
1043
|
+
"spentTokens" = GREATEST(0, "spentTokens" + ${BigInt(delta.tokens)}),
|
|
1044
|
+
"spentCount" = GREATEST(0, "spentCount" + ${delta.count}),
|
|
1045
|
+
"updatedAt" = CURRENT_TIMESTAMP
|
|
1046
|
+
WHERE "budgetId" = ${budgetId} AND "windowStart" = ${windowStart}
|
|
1047
|
+
`);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
async getWindow(budgetId, windowStart) {
|
|
1051
|
+
const rows = await this.prisma.$queryRaw(
|
|
1052
|
+
client.Prisma.sql`SELECT * FROM "ai_budget_windows" WHERE "budgetId" = ${budgetId} AND "windowStart" = ${windowStart} LIMIT 1`
|
|
1053
|
+
);
|
|
1054
|
+
return rows[0] === void 0 ? null : mapBudgetWindowRow(rows[0]);
|
|
1055
|
+
}
|
|
1056
|
+
async setWindowStart(budgetId, windowStart) {
|
|
1057
|
+
await this.prisma.$executeRaw(client.Prisma.sql`
|
|
1058
|
+
INSERT INTO "ai_budget_windows" ("id", "budgetId", "windowStart", "spentNanoUsd", "spentTokens", "spentCount", "updatedAt")
|
|
1059
|
+
VALUES (${crypto.randomUUID()}, ${budgetId}, ${windowStart}, 0, 0, 0, CURRENT_TIMESTAMP)
|
|
1060
|
+
ON CONFLICT ("budgetId", "windowStart") DO UPDATE SET "spentNanoUsd" = 0, "spentTokens" = 0, "spentCount" = 0, "updatedAt" = CURRENT_TIMESTAMP
|
|
1061
|
+
`);
|
|
1062
|
+
}
|
|
1063
|
+
/** Load the wallet within a transaction (existence + id for the conditional debit). */
|
|
1064
|
+
async getWalletWithin(tx, ref) {
|
|
1065
|
+
const rows = await tx.$queryRaw(client.Prisma.sql`
|
|
1066
|
+
SELECT * FROM "ai_wallets"
|
|
1067
|
+
WHERE "tenantId" = ${ref.tenantId} AND "ownerType" = ${ref.ownerType} AND "ownerId" = ${ref.ownerId} LIMIT 1
|
|
1068
|
+
`);
|
|
1069
|
+
return rows[0];
|
|
1070
|
+
}
|
|
1071
|
+
/** Fetch or (optionally) create the wallet for a ref within a transaction. */
|
|
1072
|
+
async ensureWallet(tx, ref, autoCreate) {
|
|
1073
|
+
const existing = await this.getWalletWithin(tx, ref);
|
|
1074
|
+
if (existing !== void 0) return existing;
|
|
1075
|
+
if (!autoCreate) return null;
|
|
1076
|
+
const rows = await tx.$queryRaw(client.Prisma.sql`
|
|
1077
|
+
INSERT INTO "ai_wallets" ("id", "tenantId", "ownerType", "ownerId", "balanceNanoUsd", "createdAt", "updatedAt")
|
|
1078
|
+
VALUES (${crypto.randomUUID()}, ${ref.tenantId}, ${ref.ownerType}, ${ref.ownerId}, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
1079
|
+
ON CONFLICT ("tenantId", "ownerType", "ownerId") DO UPDATE SET "updatedAt" = "ai_wallets"."updatedAt" RETURNING *
|
|
1080
|
+
`);
|
|
1081
|
+
return firstOrThrow(rows);
|
|
1082
|
+
}
|
|
1083
|
+
/** Insert a wallet entry, returning the stored row. */
|
|
1084
|
+
async insertWalletEntry(tx, walletId, entry) {
|
|
1085
|
+
const rows = await tx.$queryRaw(client.Prisma.sql`
|
|
1086
|
+
INSERT INTO "ai_wallet_entries" ("id", "walletId", "type", "amountNanoUsd", "priority", "effectiveAt", "expiresAt", "usageRecordId", "idempotencyKey", "reason", "createdAt")
|
|
1087
|
+
VALUES (${crypto.randomUUID()}, ${walletId}, ${entry.type}, ${entry.amountNanoUsd}, ${entry.priority}, ${entry.effectiveAt}, ${entry.expiresAt ?? null}, ${entry.usageRecordId ?? null}, ${entry.idempotencyKey}, ${entry.reason ?? null}, CURRENT_TIMESTAMP)
|
|
1088
|
+
RETURNING *
|
|
1089
|
+
`);
|
|
1090
|
+
return mapWalletEntryRow(firstOrThrow(rows));
|
|
1091
|
+
}
|
|
1092
|
+
/** Insert the debit→grant allocation trail. */
|
|
1093
|
+
async insertAllocations(tx, debitEntryId, allocations) {
|
|
1094
|
+
for (const allocation of allocations) {
|
|
1095
|
+
await tx.$executeRaw(client.Prisma.sql`
|
|
1096
|
+
INSERT INTO "ai_wallet_debit_allocations" ("id", "debitEntryId", "grantEntryId", "amountNanoUsd")
|
|
1097
|
+
VALUES (${crypto.randomUUID()}, ${debitEntryId}, ${allocation.grantEntryId}, ${allocation.amountNanoUsd})
|
|
1098
|
+
`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/** Apply a signed delta to the materialized balance. */
|
|
1102
|
+
async applyBalanceDelta(tx, walletId, delta) {
|
|
1103
|
+
if (delta === 0n) return;
|
|
1104
|
+
await tx.$executeRaw(client.Prisma.sql`
|
|
1105
|
+
UPDATE "ai_wallets" SET "balanceNanoUsd" = "balanceNanoUsd" + ${delta}, "updatedAt" = CURRENT_TIMESTAMP WHERE "id" = ${walletId}
|
|
1106
|
+
`);
|
|
1107
|
+
}
|
|
1108
|
+
/** Write an `expiry` entry (with its allocation) for each expired grant with a remainder. */
|
|
1109
|
+
async sweepExpiredGrants(tx, walletId) {
|
|
1110
|
+
const now = /* @__PURE__ */ new Date();
|
|
1111
|
+
const grants = await tx.$queryRaw(client.Prisma.sql`
|
|
1112
|
+
SELECT g."id", g."priority", (g."amountNanoUsd" - COALESCE((SELECT SUM(a."amountNanoUsd") FROM "ai_wallet_debit_allocations" a WHERE a."grantEntryId" = g."id"), 0))::bigint AS "remaining"
|
|
1113
|
+
FROM "ai_wallet_entries" g
|
|
1114
|
+
WHERE g."walletId" = ${walletId} AND g."type" = 'grant'
|
|
1115
|
+
AND g."effectiveAt" <= g."createdAt" AND (g."expiresAt" IS NULL OR g."expiresAt" > g."createdAt")
|
|
1116
|
+
AND g."expiresAt" IS NOT NULL AND g."expiresAt" <= ${now}
|
|
1117
|
+
`);
|
|
1118
|
+
for (const grant of grants) {
|
|
1119
|
+
if (grant.remaining <= 0n) continue;
|
|
1120
|
+
const expiry = await this.insertWalletEntry(tx, walletId, {
|
|
1121
|
+
walletId,
|
|
1122
|
+
type: "expiry",
|
|
1123
|
+
amountNanoUsd: -grant.remaining,
|
|
1124
|
+
priority: grant.priority,
|
|
1125
|
+
effectiveAt: now,
|
|
1126
|
+
idempotencyKey: `expiry:${grant.id}`,
|
|
1127
|
+
reason: "grant expired"
|
|
1128
|
+
});
|
|
1129
|
+
await this.insertAllocations(tx, expiry.id, [{ grantEntryId: grant.id, amountNanoUsd: grant.remaining }]);
|
|
1130
|
+
await this.applyBalanceDelta(tx, walletId, -grant.remaining);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/** Greedily allocate a debit of `cost` across the open grants in burn order. */
|
|
1134
|
+
async burnDown(tx, walletId, cost) {
|
|
1135
|
+
const grants = await tx.$queryRaw(this.openGrantsSql(walletId, this.burnOrder));
|
|
1136
|
+
const trail = [];
|
|
1137
|
+
let remaining = cost;
|
|
1138
|
+
for (const grant of grants) {
|
|
1139
|
+
if (remaining <= 0n) break;
|
|
1140
|
+
const take = grant.remaining < remaining ? grant.remaining : remaining;
|
|
1141
|
+
trail.push({ grantEntryId: grant.id, amountNanoUsd: take });
|
|
1142
|
+
remaining -= take;
|
|
1143
|
+
}
|
|
1144
|
+
return trail;
|
|
1145
|
+
}
|
|
1146
|
+
/** The open-grants query (remaining > 0, effective, not expired) ordered per burn order. */
|
|
1147
|
+
openGrantsSql(walletId, order) {
|
|
1148
|
+
const now = /* @__PURE__ */ new Date();
|
|
1149
|
+
const orderBy = order === "priority" ? client.Prisma.sql`g."priority" ASC, g."createdAt" ASC` : order === "fifo" ? client.Prisma.sql`g."createdAt" ASC` : client.Prisma.sql`g."expiresAt" ASC NULLS LAST, g."createdAt" ASC`;
|
|
1150
|
+
return client.Prisma.sql`
|
|
1151
|
+
SELECT g.*, (g."amountNanoUsd" - COALESCE((SELECT SUM(a."amountNanoUsd") FROM "ai_wallet_debit_allocations" a WHERE a."grantEntryId" = g."id"), 0))::bigint AS "remaining"
|
|
1152
|
+
FROM "ai_wallet_entries" g
|
|
1153
|
+
WHERE g."walletId" = ${walletId} AND g."type" = 'grant'
|
|
1154
|
+
AND g."effectiveAt" <= ${now} AND (g."expiresAt" IS NULL OR g."expiresAt" > ${now})
|
|
1155
|
+
AND (g."amountNanoUsd" - COALESCE((SELECT SUM(a."amountNanoUsd") FROM "ai_wallet_debit_allocations" a WHERE a."grantEntryId" = g."id"), 0)) > 0
|
|
1156
|
+
ORDER BY ${orderBy}
|
|
1157
|
+
`;
|
|
1158
|
+
}
|
|
1159
|
+
/** Return the existing entry on a matching replay, throw the conflict, or `undefined` when new. */
|
|
1160
|
+
async walletReplay(tx, walletId, ref, entry) {
|
|
1161
|
+
const rows = await tx.$queryRaw(
|
|
1162
|
+
client.Prisma.sql`SELECT * FROM "ai_wallet_entries" WHERE "walletId" = ${walletId} AND "idempotencyKey" = ${entry.idempotencyKey} LIMIT 1`
|
|
1163
|
+
);
|
|
1164
|
+
if (rows[0] === void 0) return void 0;
|
|
1165
|
+
if (walletEntryMatches(rows[0], entry)) return mapWalletEntryRow(rows[0]);
|
|
1166
|
+
throw new LedgerIdempotencyConflict(ref.tenantId, entry.idempotencyKey);
|
|
1167
|
+
}
|
|
1168
|
+
/** Ensure a (zeroed) window row exists for first-touch consumption. */
|
|
1169
|
+
async ensureWindow(tx, budgetId, windowStart) {
|
|
1170
|
+
await tx.$executeRaw(client.Prisma.sql`
|
|
1171
|
+
INSERT INTO "ai_budget_windows" ("id", "budgetId", "windowStart", "spentNanoUsd", "spentTokens", "spentCount", "updatedAt")
|
|
1172
|
+
VALUES (${crypto.randomUUID()}, ${budgetId}, ${windowStart}, 0, 0, 0, CURRENT_TIMESTAMP)
|
|
1173
|
+
ON CONFLICT ("budgetId", "windowStart") DO NOTHING
|
|
1174
|
+
`);
|
|
1175
|
+
}
|
|
1176
|
+
/** Map the P2002 replay-or-conflict path for a wallet entry (§15.2). */
|
|
1177
|
+
async replayOrConflictWallet(ref, entry) {
|
|
1178
|
+
const wallet = await this.getWallet(ref);
|
|
1179
|
+
if (wallet !== null) {
|
|
1180
|
+
const rows = await this.prisma.$queryRaw(
|
|
1181
|
+
client.Prisma.sql`SELECT * FROM "ai_wallet_entries" WHERE "walletId" = ${wallet.id} AND "idempotencyKey" = ${entry.idempotencyKey} LIMIT 1`
|
|
1182
|
+
);
|
|
1183
|
+
if (rows[0] !== void 0 && walletEntryMatches(rows[0], entry)) return mapWalletEntryRow(rows[0]);
|
|
1184
|
+
}
|
|
1185
|
+
throw new LedgerIdempotencyConflict(ref.tenantId, entry.idempotencyKey);
|
|
1186
|
+
}
|
|
1187
|
+
/** Return the existing record on a matching payload replay, else raise the conflict (§15.2). */
|
|
1188
|
+
async replayOrConflict(record, payloadHash) {
|
|
1189
|
+
const rows = await this.prisma.$queryRaw(
|
|
1190
|
+
client.Prisma.sql`SELECT * FROM "ai_usage_records" WHERE "tenantId" = ${record.tenantId} AND "idempotencyKey" = ${record.idempotencyKey} LIMIT 1`
|
|
1191
|
+
);
|
|
1192
|
+
if (rows[0]?.payloadHash === payloadHash) return mapUsageRow(rows[0]);
|
|
1193
|
+
throw new LedgerIdempotencyConflict(record.tenantId, record.idempotencyKey);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
exports.PrismaAiTokensStore = PrismaAiTokensStore;
|