usage-meter-test 0.1.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/README.md +86 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/math.d.ts +7 -0
- package/dist/math.js +37 -0
- package/dist/pricing.d.ts +32 -0
- package/dist/pricing.js +57 -0
- package/dist/providers/inMemoryProvider.d.ts +66 -0
- package/dist/providers/inMemoryProvider.js +198 -0
- package/dist/providers/prismaProvider.d.ts +36 -0
- package/dist/providers/prismaProvider.js +323 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.js +1 -0
- package/dist/usageMeter.d.ts +18 -0
- package/dist/usageMeter.js +320 -0
- package/package.json +25 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
function toAccount(row) {
|
|
2
|
+
return {
|
|
3
|
+
userId: row.userId,
|
|
4
|
+
balance: BigInt(row.balance),
|
|
5
|
+
frozen: BigInt(row.frozen),
|
|
6
|
+
updatedAt: new Date(row.updatedAt),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function toHold(row) {
|
|
10
|
+
const base = {
|
|
11
|
+
userId: row.userId,
|
|
12
|
+
requestId: row.requestId,
|
|
13
|
+
idempotencyKey: row.idempotencyKey,
|
|
14
|
+
amount: BigInt(row.amount),
|
|
15
|
+
status: row.status,
|
|
16
|
+
expiresAt: new Date(row.expiresAt),
|
|
17
|
+
createdAt: new Date(row.createdAt),
|
|
18
|
+
updatedAt: new Date(row.updatedAt),
|
|
19
|
+
};
|
|
20
|
+
if (row.capturedIdempotencyKey != null)
|
|
21
|
+
base.capturedIdempotencyKey = row.capturedIdempotencyKey;
|
|
22
|
+
return base;
|
|
23
|
+
}
|
|
24
|
+
function toLedger(row) {
|
|
25
|
+
const base = {
|
|
26
|
+
userId: row.userId,
|
|
27
|
+
type: row.type,
|
|
28
|
+
amount: BigInt(row.amount),
|
|
29
|
+
idempotencyKey: row.idempotencyKey,
|
|
30
|
+
createdAt: new Date(row.createdAt),
|
|
31
|
+
};
|
|
32
|
+
if (row.referenceId != null)
|
|
33
|
+
base.referenceId = row.referenceId;
|
|
34
|
+
if (row.meta != null)
|
|
35
|
+
base.meta = row.meta;
|
|
36
|
+
return base;
|
|
37
|
+
}
|
|
38
|
+
function toPolicy(row) {
|
|
39
|
+
return {
|
|
40
|
+
policyId: row.id,
|
|
41
|
+
period: row.period,
|
|
42
|
+
limitCredits: BigInt(row.limitCredits),
|
|
43
|
+
limitRequests: row.limitRequests ?? undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function toBucket(row) {
|
|
47
|
+
return {
|
|
48
|
+
userId: row.userId,
|
|
49
|
+
policyId: row.policyId,
|
|
50
|
+
periodStart: new Date(row.periodStart),
|
|
51
|
+
periodEnd: new Date(row.periodEnd),
|
|
52
|
+
usedCredits: BigInt(row.usedCredits),
|
|
53
|
+
reservedCredits: BigInt(row.reservedCredits),
|
|
54
|
+
usedRequests: row.usedRequests,
|
|
55
|
+
reservedRequests: row.reservedRequests,
|
|
56
|
+
updatedAt: new Date(row.updatedAt),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function computePeriod(period, now) {
|
|
60
|
+
const d = new Date(now);
|
|
61
|
+
if (period === "daily") {
|
|
62
|
+
const start = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));
|
|
63
|
+
const end = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1, 0, 0, 0, 0));
|
|
64
|
+
return { start, end };
|
|
65
|
+
}
|
|
66
|
+
const start = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1, 0, 0, 0, 0));
|
|
67
|
+
const end = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1, 0, 0, 0, 0));
|
|
68
|
+
return { start, end };
|
|
69
|
+
}
|
|
70
|
+
async function resolvePlanKeyForUserId(userId, tx, options) {
|
|
71
|
+
if (options?.resolvePlanKey)
|
|
72
|
+
return options.resolvePlanKey(userId, tx);
|
|
73
|
+
const userDelegate = tx.user;
|
|
74
|
+
if (!userDelegate?.findUnique)
|
|
75
|
+
return "default";
|
|
76
|
+
const isNumericId = /^[0-9]+$/.test(userId);
|
|
77
|
+
const row = isNumericId
|
|
78
|
+
? await userDelegate.findUnique({ where: { id: Number(userId) } })
|
|
79
|
+
: await userDelegate.findUnique({ where: { uuid: userId } });
|
|
80
|
+
const planKey = row?.planKey;
|
|
81
|
+
return typeof planKey === "string" && planKey.trim() ? planKey.trim() : "default";
|
|
82
|
+
}
|
|
83
|
+
function isRetryableTransactionError(err) {
|
|
84
|
+
const anyErr = err;
|
|
85
|
+
// Prisma errors commonly expose `code` (e.g., P2034 for deadlock / write conflict).
|
|
86
|
+
// Different DBs map to different codes; keep this conservative.
|
|
87
|
+
return anyErr?.code === "P2034";
|
|
88
|
+
}
|
|
89
|
+
export function createPrismaPersistence(prisma, options) {
|
|
90
|
+
const clock = options?.now ?? (() => new Date());
|
|
91
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
92
|
+
const isolationLevel = options?.isolationLevel ?? "Serializable";
|
|
93
|
+
return {
|
|
94
|
+
now: () => clock(),
|
|
95
|
+
withTransaction: async (fn) => {
|
|
96
|
+
let attempt = 0;
|
|
97
|
+
// eslint-disable-next-line no-constant-condition
|
|
98
|
+
while (true) {
|
|
99
|
+
attempt += 1;
|
|
100
|
+
try {
|
|
101
|
+
return await prisma.$transaction(async (clientTx) => {
|
|
102
|
+
const accounts = {
|
|
103
|
+
getOrCreate: async (userId) => {
|
|
104
|
+
const row = await clientTx.creditAccount.upsert({
|
|
105
|
+
where: { userId },
|
|
106
|
+
create: { userId, balance: 0n, frozen: 0n },
|
|
107
|
+
update: {},
|
|
108
|
+
});
|
|
109
|
+
return toAccount(row);
|
|
110
|
+
},
|
|
111
|
+
update: async (userId, patch) => {
|
|
112
|
+
// Atomic compare-and-set using `version` to avoid lost updates under concurrency.
|
|
113
|
+
// Requires `CreditAccount.version` in the Prisma schema.
|
|
114
|
+
const maxCasRetries = 5;
|
|
115
|
+
for (let i = 0; i < maxCasRetries; i += 1) {
|
|
116
|
+
const current = await clientTx.creditAccount.findUnique({ where: { userId } });
|
|
117
|
+
if (!current) {
|
|
118
|
+
const created = await clientTx.creditAccount.create({
|
|
119
|
+
data: {
|
|
120
|
+
userId,
|
|
121
|
+
balance: patch.balance ?? 0n,
|
|
122
|
+
frozen: patch.frozen ?? 0n,
|
|
123
|
+
version: 0,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return toAccount(created);
|
|
127
|
+
}
|
|
128
|
+
const nextBalance = patch.balance ?? current.balance;
|
|
129
|
+
const nextFrozen = patch.frozen ?? current.frozen;
|
|
130
|
+
const res = await clientTx.creditAccount.updateMany({
|
|
131
|
+
where: { userId, version: current.version },
|
|
132
|
+
data: { balance: nextBalance, frozen: nextFrozen, version: { increment: 1 } },
|
|
133
|
+
});
|
|
134
|
+
if (res.count === 1) {
|
|
135
|
+
const updated = await clientTx.creditAccount.findUnique({ where: { userId } });
|
|
136
|
+
if (!updated)
|
|
137
|
+
throw new Error("CreditAccount disappeared after update");
|
|
138
|
+
return toAccount(updated);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new Error("CreditAccount CAS update failed after retries");
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const holds = {
|
|
145
|
+
getByRequestId: async (userId, requestId) => {
|
|
146
|
+
const row = await clientTx.creditHold.findUnique({
|
|
147
|
+
where: { userId_requestId: { userId, requestId } },
|
|
148
|
+
});
|
|
149
|
+
return row ? toHold(row) : null;
|
|
150
|
+
},
|
|
151
|
+
create: async (hold) => {
|
|
152
|
+
const row = await clientTx.creditHold.create({
|
|
153
|
+
data: {
|
|
154
|
+
userId: hold.userId,
|
|
155
|
+
requestId: hold.requestId,
|
|
156
|
+
idempotencyKey: hold.idempotencyKey,
|
|
157
|
+
amount: hold.amount,
|
|
158
|
+
status: hold.status,
|
|
159
|
+
expiresAt: hold.expiresAt,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
return toHold(row);
|
|
163
|
+
},
|
|
164
|
+
update: async (userId, requestId, patch) => {
|
|
165
|
+
const row = await clientTx.creditHold.update({
|
|
166
|
+
where: { userId_requestId: { userId, requestId } },
|
|
167
|
+
data: {
|
|
168
|
+
...(patch.status ? { status: patch.status } : {}),
|
|
169
|
+
...(patch.expiresAt ? { expiresAt: patch.expiresAt } : {}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
return toHold(row);
|
|
173
|
+
},
|
|
174
|
+
lockCapture: async (userId, requestId, capturedIdempotencyKey, now) => {
|
|
175
|
+
await clientTx.creditHold.updateMany({
|
|
176
|
+
where: {
|
|
177
|
+
userId,
|
|
178
|
+
requestId,
|
|
179
|
+
status: "held",
|
|
180
|
+
expiresAt: { gt: now },
|
|
181
|
+
capturedIdempotencyKey: null,
|
|
182
|
+
},
|
|
183
|
+
data: { capturedIdempotencyKey },
|
|
184
|
+
});
|
|
185
|
+
const row = await clientTx.creditHold.findUnique({ where: { userId_requestId: { userId, requestId } } });
|
|
186
|
+
if (!row)
|
|
187
|
+
throw new Error("Hold not found");
|
|
188
|
+
return toHold(row);
|
|
189
|
+
},
|
|
190
|
+
finalizeCapture: async (userId, requestId, capturedIdempotencyKey) => {
|
|
191
|
+
await clientTx.creditHold.updateMany({
|
|
192
|
+
where: {
|
|
193
|
+
userId,
|
|
194
|
+
requestId,
|
|
195
|
+
capturedIdempotencyKey,
|
|
196
|
+
},
|
|
197
|
+
data: { status: "captured" },
|
|
198
|
+
});
|
|
199
|
+
const row = await clientTx.creditHold.findUnique({ where: { userId_requestId: { userId, requestId } } });
|
|
200
|
+
if (!row)
|
|
201
|
+
throw new Error("Hold not found");
|
|
202
|
+
return toHold(row);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const ledger = {
|
|
206
|
+
getByIdempotencyKey: async (userId, idempotencyKey) => {
|
|
207
|
+
const row = await clientTx.creditLedger.findUnique({
|
|
208
|
+
where: { userId_idempotencyKey: { userId, idempotencyKey } },
|
|
209
|
+
});
|
|
210
|
+
return row ? toLedger(row) : null;
|
|
211
|
+
},
|
|
212
|
+
append: async (entry) => {
|
|
213
|
+
try {
|
|
214
|
+
const row = await clientTx.creditLedger.create({
|
|
215
|
+
data: {
|
|
216
|
+
userId: entry.userId,
|
|
217
|
+
idempotencyKey: entry.idempotencyKey,
|
|
218
|
+
type: entry.type,
|
|
219
|
+
amount: entry.amount,
|
|
220
|
+
referenceId: entry.referenceId ?? null,
|
|
221
|
+
meta: entry.meta ?? null,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
return toLedger(row);
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
// If a concurrent request created the same idempotency key, read it back.
|
|
228
|
+
const existing = await clientTx.creditLedger.findUnique({
|
|
229
|
+
where: { userId_idempotencyKey: { userId: entry.userId, idempotencyKey: entry.idempotencyKey } },
|
|
230
|
+
});
|
|
231
|
+
if (existing)
|
|
232
|
+
return toLedger(existing);
|
|
233
|
+
throw e;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const quota = {
|
|
238
|
+
getActivePolicy: async (userId) => {
|
|
239
|
+
const planKey = await resolvePlanKeyForUserId(userId, clientTx, options);
|
|
240
|
+
const row = await clientTx.planQuotaPolicy.findFirst({
|
|
241
|
+
where: { planKey, active: true },
|
|
242
|
+
orderBy: { updatedAt: "desc" },
|
|
243
|
+
});
|
|
244
|
+
return row ? toPolicy(row) : null;
|
|
245
|
+
},
|
|
246
|
+
getOrCreateBucket: async (userId, policy, now) => {
|
|
247
|
+
const { start, end } = computePeriod(policy.period, now);
|
|
248
|
+
const row = await clientTx.quotaBucket.upsert({
|
|
249
|
+
where: { userId_policyId_periodStart: { userId, policyId: policy.policyId, periodStart: start } },
|
|
250
|
+
create: {
|
|
251
|
+
userId,
|
|
252
|
+
policyId: policy.policyId,
|
|
253
|
+
periodStart: start,
|
|
254
|
+
periodEnd: end,
|
|
255
|
+
usedCredits: 0n,
|
|
256
|
+
reservedCredits: 0n,
|
|
257
|
+
usedRequests: 0,
|
|
258
|
+
reservedRequests: 0,
|
|
259
|
+
},
|
|
260
|
+
update: {},
|
|
261
|
+
});
|
|
262
|
+
return toBucket(row);
|
|
263
|
+
},
|
|
264
|
+
updateBucket: async (userId, policyId, periodStart, patch) => {
|
|
265
|
+
// Atomic compare-and-set using `version`.
|
|
266
|
+
const maxCasRetries = 5;
|
|
267
|
+
for (let i = 0; i < maxCasRetries; i += 1) {
|
|
268
|
+
const current = await clientTx.quotaBucket.findUnique({
|
|
269
|
+
where: { userId_policyId_periodStart: { userId, policyId, periodStart } },
|
|
270
|
+
});
|
|
271
|
+
if (!current)
|
|
272
|
+
throw new Error("Quota bucket not found");
|
|
273
|
+
const nextUsedCredits = patch.usedCredits ?? current.usedCredits;
|
|
274
|
+
const nextReservedCredits = patch.reservedCredits ?? current.reservedCredits;
|
|
275
|
+
const nextUsedRequests = patch.usedRequests ?? current.usedRequests;
|
|
276
|
+
const nextReservedRequests = patch.reservedRequests ?? current.reservedRequests;
|
|
277
|
+
const res = await clientTx.quotaBucket.updateMany({
|
|
278
|
+
where: { userId, policyId, periodStart, version: current.version },
|
|
279
|
+
data: {
|
|
280
|
+
usedCredits: nextUsedCredits,
|
|
281
|
+
reservedCredits: nextReservedCredits,
|
|
282
|
+
usedRequests: nextUsedRequests,
|
|
283
|
+
reservedRequests: nextReservedRequests,
|
|
284
|
+
version: { increment: 1 },
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
if (res.count === 1) {
|
|
288
|
+
const updated = await clientTx.quotaBucket.findUnique({
|
|
289
|
+
where: { userId_policyId_periodStart: { userId, policyId, periodStart } },
|
|
290
|
+
});
|
|
291
|
+
if (!updated)
|
|
292
|
+
throw new Error("Quota bucket disappeared after update");
|
|
293
|
+
return toBucket(updated);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
throw new Error("QuotaBucket CAS update failed after retries");
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
const usage = {
|
|
300
|
+
recordUsage: async (row) => {
|
|
301
|
+
// Optional: integrate with your app's logging/audit table.
|
|
302
|
+
void row;
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
const meteringTx = {
|
|
306
|
+
accounts,
|
|
307
|
+
holds,
|
|
308
|
+
ledger,
|
|
309
|
+
quota,
|
|
310
|
+
usage,
|
|
311
|
+
};
|
|
312
|
+
return fn(meteringTx);
|
|
313
|
+
}, { isolationLevel });
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (attempt <= maxRetries && isRetryableTransactionError(err))
|
|
317
|
+
continue;
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export type UserId = string;
|
|
2
|
+
export type RequestId = string;
|
|
3
|
+
export type Credits = bigint;
|
|
4
|
+
export type IdempotencyKey = string;
|
|
5
|
+
export type Bps = bigint;
|
|
6
|
+
export type Period = "daily" | "monthly";
|
|
7
|
+
export type LedgerEntryType = "grant" | "purchase" | "charge" | "refund" | "adjust" | "expire";
|
|
8
|
+
export type HoldStatus = "held" | "captured" | "released" | "expired";
|
|
9
|
+
export interface CreditAccount {
|
|
10
|
+
userId: UserId;
|
|
11
|
+
balance: Credits;
|
|
12
|
+
frozen: Credits;
|
|
13
|
+
updatedAt: Date;
|
|
14
|
+
}
|
|
15
|
+
export interface CreditHold {
|
|
16
|
+
userId: UserId;
|
|
17
|
+
requestId: RequestId;
|
|
18
|
+
idempotencyKey: IdempotencyKey;
|
|
19
|
+
capturedIdempotencyKey?: IdempotencyKey;
|
|
20
|
+
amount: Credits;
|
|
21
|
+
status: HoldStatus;
|
|
22
|
+
expiresAt: Date;
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
updatedAt: Date;
|
|
25
|
+
}
|
|
26
|
+
export interface LedgerEntry {
|
|
27
|
+
userId: UserId;
|
|
28
|
+
type: LedgerEntryType;
|
|
29
|
+
amount: Credits;
|
|
30
|
+
referenceId?: string;
|
|
31
|
+
idempotencyKey: IdempotencyKey;
|
|
32
|
+
meta?: Record<string, unknown>;
|
|
33
|
+
createdAt: Date;
|
|
34
|
+
}
|
|
35
|
+
export interface QuotaPolicy {
|
|
36
|
+
policyId: string;
|
|
37
|
+
period: Period;
|
|
38
|
+
limitCredits: Credits;
|
|
39
|
+
limitRequests?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface QuotaBucket {
|
|
42
|
+
userId: UserId;
|
|
43
|
+
policyId: string;
|
|
44
|
+
periodStart: Date;
|
|
45
|
+
periodEnd: Date;
|
|
46
|
+
usedCredits: Credits;
|
|
47
|
+
reservedCredits: Credits;
|
|
48
|
+
usedRequests: number;
|
|
49
|
+
reservedRequests: number;
|
|
50
|
+
updatedAt: Date;
|
|
51
|
+
}
|
|
52
|
+
export interface UsageTokens {
|
|
53
|
+
inputTokens: number;
|
|
54
|
+
outputTokens: number;
|
|
55
|
+
}
|
|
56
|
+
export interface ToolUsage {
|
|
57
|
+
tool: string;
|
|
58
|
+
units: number;
|
|
59
|
+
}
|
|
60
|
+
export interface UsageFacts {
|
|
61
|
+
requestId: RequestId;
|
|
62
|
+
model: string;
|
|
63
|
+
tokens: UsageTokens;
|
|
64
|
+
tools?: ToolUsage[];
|
|
65
|
+
meta?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
export interface EstimateRequest {
|
|
68
|
+
userId: UserId;
|
|
69
|
+
requestId: RequestId;
|
|
70
|
+
model: string;
|
|
71
|
+
tokens: UsageTokens;
|
|
72
|
+
maxOutputTokens: number;
|
|
73
|
+
tools?: ToolUsage[];
|
|
74
|
+
riskBps?: Bps;
|
|
75
|
+
}
|
|
76
|
+
export interface EstimateResult {
|
|
77
|
+
estimatedCost: Credits;
|
|
78
|
+
maxPossibleCost: Credits;
|
|
79
|
+
breakdown: PriceBreakdown;
|
|
80
|
+
}
|
|
81
|
+
export interface HoldRequest {
|
|
82
|
+
userId: UserId;
|
|
83
|
+
requestId: RequestId;
|
|
84
|
+
amount: Credits;
|
|
85
|
+
idempotencyKey: IdempotencyKey;
|
|
86
|
+
expiresAt: Date;
|
|
87
|
+
meta?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
export interface HoldResult {
|
|
90
|
+
hold: CreditHold;
|
|
91
|
+
account: CreditAccount;
|
|
92
|
+
quotaBucket?: QuotaBucket;
|
|
93
|
+
}
|
|
94
|
+
export interface CaptureRequest {
|
|
95
|
+
userId: UserId;
|
|
96
|
+
requestId: RequestId;
|
|
97
|
+
actualCost: Credits;
|
|
98
|
+
idempotencyKey: IdempotencyKey;
|
|
99
|
+
usage: UsageFacts;
|
|
100
|
+
}
|
|
101
|
+
export interface CaptureResult {
|
|
102
|
+
charged: Credits;
|
|
103
|
+
hold: CreditHold;
|
|
104
|
+
account: CreditAccount;
|
|
105
|
+
ledgerEntry: LedgerEntry;
|
|
106
|
+
quotaBucket?: QuotaBucket;
|
|
107
|
+
}
|
|
108
|
+
export interface ReleaseRequest {
|
|
109
|
+
userId: UserId;
|
|
110
|
+
requestId: RequestId;
|
|
111
|
+
reason: string;
|
|
112
|
+
}
|
|
113
|
+
export interface ReleaseResult {
|
|
114
|
+
hold: CreditHold;
|
|
115
|
+
account: CreditAccount;
|
|
116
|
+
quotaBucket?: QuotaBucket;
|
|
117
|
+
}
|
|
118
|
+
export interface PriceBreakdown {
|
|
119
|
+
model: string;
|
|
120
|
+
inputCost: Credits;
|
|
121
|
+
outputCost: Credits;
|
|
122
|
+
toolCost: Credits;
|
|
123
|
+
minCostApplied: boolean;
|
|
124
|
+
modelMultiplierBps: Bps;
|
|
125
|
+
riskMultiplierBps: Bps;
|
|
126
|
+
totalBeforeMultipliers: Credits;
|
|
127
|
+
totalAfterMultipliers: Credits;
|
|
128
|
+
totalFinal: Credits;
|
|
129
|
+
}
|
|
130
|
+
export interface MeteringTransaction {
|
|
131
|
+
accounts: CreditAccountStore;
|
|
132
|
+
holds: CreditHoldStore;
|
|
133
|
+
ledger: CreditLedgerStore;
|
|
134
|
+
quota?: QuotaStore;
|
|
135
|
+
usage?: UsageStore;
|
|
136
|
+
}
|
|
137
|
+
export interface MeteringPersistence {
|
|
138
|
+
withTransaction<T>(fn: (tx: MeteringTransaction) => Promise<T>): Promise<T>;
|
|
139
|
+
now(): Date;
|
|
140
|
+
}
|
|
141
|
+
export interface CreditAccountStore {
|
|
142
|
+
getOrCreate(userId: UserId): Promise<CreditAccount>;
|
|
143
|
+
update(userId: UserId, patch: Partial<Pick<CreditAccount, "balance" | "frozen">>): Promise<CreditAccount>;
|
|
144
|
+
}
|
|
145
|
+
export interface CreditHoldStore {
|
|
146
|
+
getByRequestId(userId: UserId, requestId: RequestId): Promise<CreditHold | null>;
|
|
147
|
+
create(hold: CreditHold): Promise<CreditHold>;
|
|
148
|
+
update(userId: UserId, requestId: RequestId, patch: Partial<Pick<CreditHold, "status" | "expiresAt">>): Promise<CreditHold>;
|
|
149
|
+
lockCapture(userId: UserId, requestId: RequestId, capturedIdempotencyKey: IdempotencyKey, now: Date): Promise<CreditHold>;
|
|
150
|
+
finalizeCapture(userId: UserId, requestId: RequestId, capturedIdempotencyKey: IdempotencyKey): Promise<CreditHold>;
|
|
151
|
+
}
|
|
152
|
+
export interface CreditLedgerStore {
|
|
153
|
+
getByIdempotencyKey(userId: UserId, idempotencyKey: IdempotencyKey): Promise<LedgerEntry | null>;
|
|
154
|
+
append(entry: LedgerEntry): Promise<LedgerEntry>;
|
|
155
|
+
}
|
|
156
|
+
export interface QuotaStore {
|
|
157
|
+
getActivePolicy(userId: UserId): Promise<QuotaPolicy | null>;
|
|
158
|
+
getOrCreateBucket(userId: UserId, policy: QuotaPolicy, now: Date): Promise<QuotaBucket>;
|
|
159
|
+
updateBucket(userId: UserId, policyId: string, periodStart: Date, patch: Partial<Pick<QuotaBucket, "usedCredits" | "reservedCredits" | "usedRequests" | "reservedRequests">>): Promise<QuotaBucket>;
|
|
160
|
+
}
|
|
161
|
+
export interface UsageStore {
|
|
162
|
+
recordUsage(result: {
|
|
163
|
+
userId: UserId;
|
|
164
|
+
usage: UsageFacts;
|
|
165
|
+
estimatedCost: Credits;
|
|
166
|
+
actualCost?: Credits;
|
|
167
|
+
}): Promise<void>;
|
|
168
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type PricingConfig } from "./pricing.js";
|
|
2
|
+
import type { CaptureRequest, CaptureResult, EstimateRequest, EstimateResult, HoldRequest, HoldResult, MeteringPersistence, ReleaseRequest, ReleaseResult } from "./types.js";
|
|
3
|
+
export interface UsageMeterOptions {
|
|
4
|
+
pricing: PricingConfig;
|
|
5
|
+
}
|
|
6
|
+
export declare class UsageMeter {
|
|
7
|
+
readonly pricing: PricingConfig;
|
|
8
|
+
readonly persistence: MeteringPersistence;
|
|
9
|
+
constructor(persistence: MeteringPersistence, options: UsageMeterOptions);
|
|
10
|
+
estimate(req: EstimateRequest): EstimateResult;
|
|
11
|
+
hold(req: HoldRequest): Promise<HoldResult>;
|
|
12
|
+
capture(req: CaptureRequest): Promise<CaptureResult>;
|
|
13
|
+
release(req: ReleaseRequest): Promise<ReleaseResult>;
|
|
14
|
+
private tryGetBucket;
|
|
15
|
+
private reserveQuota;
|
|
16
|
+
private unreserveQuota;
|
|
17
|
+
private settleQuota;
|
|
18
|
+
}
|