@hachej/boring-core 0.1.70 → 0.1.72
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/dist/{PostgresModelBudgetStore-CFbEEdQL.d.ts → PostgresModelBudgetStore-DWwh7ElQ.d.ts} +93 -11
- package/dist/app/front/index.d.ts +6 -2
- package/dist/app/front/index.js +13 -6
- package/dist/app/server/index.d.ts +3 -3
- package/dist/app/server/index.js +2 -2
- package/dist/{chunk-CIRY3ZLU.js → chunk-DSENO5O4.js} +3 -1
- package/dist/{chunk-EFM5IWTK.js → chunk-JBEKMYST.js} +486 -214
- package/dist/front/index.d.ts +1 -1
- package/dist/{authHook-BUSGTx_o.d.ts → loadConfig-C3iHuZNw.d.ts} +11 -11
- package/dist/server/db/index.d.ts +4 -4
- package/dist/server/db/index.js +5 -1
- package/dist/server/index.d.ts +56 -56
- package/dist/server/index.js +6 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/{types-DVgeIw1d.d.ts → types-DDnayJjI.d.ts} +1 -1
- package/dist/{connection-D9s7Td0I.d.ts → types-Dm11Zm56.d.ts} +8 -8
- package/drizzle/0017_user_budget_reservations.sql +15 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +7 -7
|
@@ -13,13 +13,13 @@ function createDatabase(config) {
|
|
|
13
13
|
if (!config.databaseUrl) {
|
|
14
14
|
throw new Error("databaseUrl is required to create a database connection");
|
|
15
15
|
}
|
|
16
|
-
const
|
|
16
|
+
const sql10 = postgres(config.databaseUrl, {
|
|
17
17
|
max: 10,
|
|
18
18
|
idle_timeout: 20,
|
|
19
19
|
connect_timeout: 10
|
|
20
20
|
});
|
|
21
|
-
const db = drizzle(
|
|
22
|
-
return { db, sql:
|
|
21
|
+
const db = drizzle(sql10);
|
|
22
|
+
return { db, sql: sql10 };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// src/server/db/migrate.ts
|
|
@@ -553,6 +553,7 @@ var schema_exports = {};
|
|
|
553
553
|
__export(schema_exports, {
|
|
554
554
|
accounts: () => accounts,
|
|
555
555
|
accountsRelations: () => accountsRelations,
|
|
556
|
+
budgetReservations: () => budgetReservations,
|
|
556
557
|
creditGrants: () => creditGrants,
|
|
557
558
|
creditPurchases: () => creditPurchases,
|
|
558
559
|
idempotencyKeys: () => idempotencyKeys,
|
|
@@ -968,16 +969,17 @@ var usageReservations = pgTable2(
|
|
|
968
969
|
)
|
|
969
970
|
]
|
|
970
971
|
);
|
|
971
|
-
var
|
|
972
|
-
"
|
|
972
|
+
var budgetReservations = pgTable2(
|
|
973
|
+
"boring_budget_reservations",
|
|
973
974
|
{
|
|
974
975
|
id: uuid2("id").default(sql3`gen_random_uuid()`).primaryKey(),
|
|
975
976
|
userId: text2("user_id").notNull(),
|
|
976
977
|
workspaceId: text2("workspace_id"),
|
|
977
978
|
sessionId: text2("session_id"),
|
|
978
979
|
runId: text2("run_id").notNull(),
|
|
979
|
-
|
|
980
|
-
|
|
980
|
+
scope: text2("scope").notNull().default("model"),
|
|
981
|
+
provider: text2("provider"),
|
|
982
|
+
model: text2("model"),
|
|
981
983
|
period: text2("period").notNull(),
|
|
982
984
|
amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
|
|
983
985
|
status: text2("status").notNull().default("active"),
|
|
@@ -985,16 +987,23 @@ var modelBudgetReservations = pgTable2(
|
|
|
985
987
|
expiresAt: timestamp2("expires_at").notNull()
|
|
986
988
|
},
|
|
987
989
|
(table) => [
|
|
988
|
-
uniqueIndex("
|
|
989
|
-
index2("
|
|
990
|
-
index2("
|
|
991
|
-
|
|
990
|
+
uniqueIndex("boring_budget_reservations_active_user_run_idx").on(table.scope, table.userId, table.runId).where(sql3`${table.status} = 'active'`),
|
|
991
|
+
index2("boring_budget_reservations_budget_idx").on(table.scope, table.userId, table.provider, table.model, table.period, table.status),
|
|
992
|
+
index2("boring_budget_reservations_user_budget_idx").on(table.scope, table.userId, table.period, table.status),
|
|
993
|
+
index2("boring_budget_reservations_stale_idx").on(table.status, table.expiresAt),
|
|
994
|
+
check("boring_budget_reservations_amount_check", sql3`${table.amountMicros} > 0`),
|
|
995
|
+
check("boring_budget_reservations_scope_check", sql3`${table.scope} IN ('model', 'user')`),
|
|
992
996
|
check(
|
|
993
|
-
"
|
|
997
|
+
"boring_budget_reservations_scope_shape_check",
|
|
998
|
+
sql3`(${table.scope} = 'model' AND ${table.provider} IS NOT NULL AND length(btrim(${table.provider})) > 0 AND ${table.model} IS NOT NULL AND length(btrim(${table.model})) > 0) OR (${table.scope} = 'user' AND ${table.provider} IS NULL AND ${table.model} IS NULL)`
|
|
999
|
+
),
|
|
1000
|
+
check(
|
|
1001
|
+
"boring_budget_reservations_status_check",
|
|
994
1002
|
sql3`${table.status} IN ('active', 'settled', 'released', 'expired')`
|
|
995
1003
|
)
|
|
996
1004
|
]
|
|
997
1005
|
);
|
|
1006
|
+
var modelBudgetReservations = budgetReservations;
|
|
998
1007
|
var usageLedger = pgTable2(
|
|
999
1008
|
"boring_usage_ledger",
|
|
1000
1009
|
{
|
|
@@ -2289,14 +2298,33 @@ var PostgresMeteringStore = class {
|
|
|
2289
2298
|
providerCostMicros: usageLedger.providerCostMicros,
|
|
2290
2299
|
billedCostMicros: usageLedger.billedCostMicros,
|
|
2291
2300
|
stopReason: usageLedger.stopReason,
|
|
2292
|
-
reservationId: sql6`${usageLedger.metadata}->>'reservationId'
|
|
2301
|
+
reservationId: sql6`${usageLedger.metadata}->>'reservationId'`,
|
|
2302
|
+
modelBudgetReservationId: sql6`${usageLedger.metadata}->>'modelBudgetReservationId'`,
|
|
2303
|
+
userBudgetReservationId: sql6`${usageLedger.metadata}->>'userBudgetReservationId'`
|
|
2293
2304
|
}).from(usageLedger).where(eq3(usageLedger.id, input.usageId)).limit(1);
|
|
2294
2305
|
const e = existing[0];
|
|
2306
|
+
if (!e) throw new Error(`usage ledger id collision for ${input.usageId}: conflicting row disappeared before idempotency check`);
|
|
2295
2307
|
const incomingReservationId = input.metadata?.reservationId ?? null;
|
|
2296
|
-
const
|
|
2308
|
+
const incomingModelBudgetReservationId = input.metadata?.modelBudgetReservationId ?? null;
|
|
2309
|
+
const incomingUserBudgetReservationId = input.metadata?.userBudgetReservationId ?? null;
|
|
2310
|
+
const legacyMetadataCanBeBackfilled = (existing2, incoming) => existing2 === null && incoming !== null;
|
|
2311
|
+
const retryMetadataIsAbsent = (existing2, incoming) => existing2 !== null && incoming === null;
|
|
2312
|
+
const metadataFieldIsIdempotent = (existing2, incoming) => existing2 === incoming || legacyMetadataCanBeBackfilled(existing2, incoming) || retryMetadataIsAbsent(existing2, incoming);
|
|
2313
|
+
const exactBudgetMetadataMatch = e.modelBudgetReservationId === incomingModelBudgetReservationId && e.userBudgetReservationId === incomingUserBudgetReservationId;
|
|
2314
|
+
const backfillableBudgetMetadata = !exactBudgetMetadataMatch && (legacyMetadataCanBeBackfilled(e.modelBudgetReservationId, incomingModelBudgetReservationId) || legacyMetadataCanBeBackfilled(e.userBudgetReservationId, incomingUserBudgetReservationId)) && metadataFieldIsIdempotent(e.modelBudgetReservationId, incomingModelBudgetReservationId) && metadataFieldIsIdempotent(e.userBudgetReservationId, incomingUserBudgetReservationId);
|
|
2315
|
+
const budgetMetadataIsIdempotent = exactBudgetMetadataMatch || backfillableBudgetMetadata || (retryMetadataIsAbsent(e.modelBudgetReservationId, incomingModelBudgetReservationId) || retryMetadataIsAbsent(e.userBudgetReservationId, incomingUserBudgetReservationId)) && metadataFieldIsIdempotent(e.modelBudgetReservationId, incomingModelBudgetReservationId) && metadataFieldIsIdempotent(e.userBudgetReservationId, incomingUserBudgetReservationId);
|
|
2316
|
+
const matches = e && e.userId === input.userId && e.runId === (input.runId ?? null) && e.messageId === (input.messageId ?? null) && e.source === (input.source ?? "") && e.provider === (input.provider ?? null) && e.model === (input.model ?? null) && e.inputTokens === (input.inputTokens ?? 0) && e.outputTokens === (input.outputTokens ?? 0) && e.cacheReadTokens === (input.cacheReadTokens ?? 0) && e.cacheWriteTokens === (input.cacheWriteTokens ?? 0) && e.providerCostMicros === (input.providerCostMicros ?? 0) && e.billedCostMicros === input.billedCostMicros && e.stopReason === (input.stopReason ?? null) && e.reservationId === incomingReservationId && budgetMetadataIsIdempotent;
|
|
2297
2317
|
if (!matches) {
|
|
2298
2318
|
throw new Error(`usage ledger id collision for ${input.usageId}: existing row does not match this usage (refusing to silently drop the debit or corrupt the audit trail)`);
|
|
2299
2319
|
}
|
|
2320
|
+
if (!exactBudgetMetadataMatch && backfillableBudgetMetadata) {
|
|
2321
|
+
const metadataPatch = {};
|
|
2322
|
+
if (legacyMetadataCanBeBackfilled(e.modelBudgetReservationId, incomingModelBudgetReservationId)) metadataPatch.modelBudgetReservationId = incomingModelBudgetReservationId;
|
|
2323
|
+
if (legacyMetadataCanBeBackfilled(e.userBudgetReservationId, incomingUserBudgetReservationId)) metadataPatch.userBudgetReservationId = incomingUserBudgetReservationId;
|
|
2324
|
+
await tx.update(usageLedger).set({
|
|
2325
|
+
metadata: sql6`COALESCE(${usageLedger.metadata}, '{}'::jsonb) || ${JSON.stringify(metadataPatch)}::jsonb`
|
|
2326
|
+
}).where(eq3(usageLedger.id, input.usageId));
|
|
2327
|
+
}
|
|
2300
2328
|
return { inserted: false };
|
|
2301
2329
|
});
|
|
2302
2330
|
}
|
|
@@ -2452,8 +2480,157 @@ function describeUsage(source) {
|
|
|
2452
2480
|
return { kind: "usage", description: "Agent usage" };
|
|
2453
2481
|
}
|
|
2454
2482
|
|
|
2455
|
-
// src/server/db/stores/
|
|
2456
|
-
import { and as and3, eq as eq4, inArray as
|
|
2483
|
+
// src/server/db/stores/PostgresBudgetReservationStore.ts
|
|
2484
|
+
import { and as and3, eq as eq4, inArray as inArray3, lt as lt2, or as or2, sql as sql9 } from "drizzle-orm";
|
|
2485
|
+
|
|
2486
|
+
// src/server/db/stores/BudgetReservationSupport.ts
|
|
2487
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2488
|
+
function assertPositiveSafeInteger(name, value) {
|
|
2489
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} must be a positive safe integer`);
|
|
2490
|
+
}
|
|
2491
|
+
function monthPeriodUtc(now) {
|
|
2492
|
+
const year = now.getUTCFullYear();
|
|
2493
|
+
const month = now.getUTCMonth();
|
|
2494
|
+
return {
|
|
2495
|
+
period: `${year}-${String(month + 1).padStart(2, "0")}`,
|
|
2496
|
+
start: new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)),
|
|
2497
|
+
end: new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0))
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function chunkIds(ids, size = 1e3) {
|
|
2501
|
+
const chunks = [];
|
|
2502
|
+
for (let index3 = 0; index3 < ids.length; index3 += size) chunks.push(ids.slice(index3, index3 + size));
|
|
2503
|
+
return chunks;
|
|
2504
|
+
}
|
|
2505
|
+
async function lockBudgetTargets(executor, targets, keyForTarget) {
|
|
2506
|
+
const keys = Array.from(new Set(targets.map(keyForTarget))).sort();
|
|
2507
|
+
for (const key of keys) await executor.execute(sql7`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// src/server/db/stores/BudgetSpendAttribution.ts
|
|
2511
|
+
import { inArray as inArray2, sql as sql8 } from "drizzle-orm";
|
|
2512
|
+
var budgetSpendAttributionStrategies = {
|
|
2513
|
+
model: { usageMicros: modelUsageMicros },
|
|
2514
|
+
user: { usageMicros: userUsageMicros }
|
|
2515
|
+
};
|
|
2516
|
+
async function computeBudgetSpend(tx, input, period, start, end, eligibleLegacySources, totals) {
|
|
2517
|
+
const strategy = budgetSpendAttributionStrategies[input.scope];
|
|
2518
|
+
return totals(input, await strategy.usageMicros(tx, input, period, start, end, eligibleLegacySources));
|
|
2519
|
+
}
|
|
2520
|
+
async function modelUsageMicros(tx, input, period, start, end) {
|
|
2521
|
+
const periodStartIso = start.toISOString();
|
|
2522
|
+
const periodEndIso = end.toISOString();
|
|
2523
|
+
const metadataExpr = sql8`${usageLedger.metadata}->>'modelBudgetReservationId'`;
|
|
2524
|
+
const usageRows = await tx.execute(sql8`
|
|
2525
|
+
SELECT COALESCE(SUM(${usageLedger.billedCostMicros}), 0)::text AS total
|
|
2526
|
+
FROM ${usageLedger}
|
|
2527
|
+
WHERE ${usageLedger.userId} = ${input.userId}
|
|
2528
|
+
AND ${usageLedger.provider} = ${input.provider}
|
|
2529
|
+
AND ${usageLedger.model} = ${input.model}
|
|
2530
|
+
AND (
|
|
2531
|
+
EXISTS (
|
|
2532
|
+
SELECT 1 FROM ${budgetReservations} r
|
|
2533
|
+
WHERE r.scope = 'model'
|
|
2534
|
+
AND r.user_id = ${usageLedger.userId}
|
|
2535
|
+
AND r.provider = ${usageLedger.provider}
|
|
2536
|
+
AND r.model = ${usageLedger.model}
|
|
2537
|
+
AND r.run_id = ${usageLedger.runId}
|
|
2538
|
+
AND r.period = ${period}
|
|
2539
|
+
AND r.status NOT IN ('active', 'settled')
|
|
2540
|
+
AND (
|
|
2541
|
+
${metadataExpr} = r.id::text
|
|
2542
|
+
OR (
|
|
2543
|
+
${metadataExpr} IS NULL
|
|
2544
|
+
AND r.created_at <= ${usageLedger.createdAt}
|
|
2545
|
+
AND NOT EXISTS (
|
|
2546
|
+
SELECT 1 FROM ${budgetReservations} newer
|
|
2547
|
+
WHERE newer.scope = 'model'
|
|
2548
|
+
AND newer.user_id = r.user_id
|
|
2549
|
+
AND newer.provider = r.provider
|
|
2550
|
+
AND newer.model = r.model
|
|
2551
|
+
AND newer.run_id = r.run_id
|
|
2552
|
+
AND newer.created_at <= ${usageLedger.createdAt}
|
|
2553
|
+
AND (newer.created_at > r.created_at OR (newer.created_at = r.created_at AND newer.id > r.id))
|
|
2554
|
+
)
|
|
2555
|
+
)
|
|
2556
|
+
)
|
|
2557
|
+
)
|
|
2558
|
+
OR (
|
|
2559
|
+
${metadataExpr} IS NULL
|
|
2560
|
+
AND ${usageLedger.createdAt} >= ${periodStartIso}::timestamp
|
|
2561
|
+
AND ${usageLedger.createdAt} < ${periodEndIso}::timestamp
|
|
2562
|
+
AND NOT EXISTS (
|
|
2563
|
+
SELECT 1 FROM ${budgetReservations} r
|
|
2564
|
+
WHERE r.scope = 'model'
|
|
2565
|
+
AND r.user_id = ${usageLedger.userId}
|
|
2566
|
+
AND r.provider = ${usageLedger.provider}
|
|
2567
|
+
AND r.model = ${usageLedger.model}
|
|
2568
|
+
AND r.run_id = ${usageLedger.runId}
|
|
2569
|
+
AND r.created_at <= ${usageLedger.createdAt}
|
|
2570
|
+
)
|
|
2571
|
+
)
|
|
2572
|
+
)
|
|
2573
|
+
`);
|
|
2574
|
+
return Number(usageRows[0]?.total ?? 0);
|
|
2575
|
+
}
|
|
2576
|
+
async function userUsageMicros(tx, input, period, start, end, eligibleLegacySources) {
|
|
2577
|
+
const periodStartIso = start.toISOString();
|
|
2578
|
+
const periodEndIso = end.toISOString();
|
|
2579
|
+
const metadataExpr = sql8`${usageLedger.metadata}->>'userBudgetReservationId'`;
|
|
2580
|
+
const legacySourcePredicate = inArray2(usageLedger.source, [...eligibleLegacySources]);
|
|
2581
|
+
const usageRows = await tx.execute(sql8`
|
|
2582
|
+
SELECT COALESCE(SUM(${usageLedger.billedCostMicros}), 0)::text AS total
|
|
2583
|
+
FROM ${usageLedger}
|
|
2584
|
+
WHERE ${usageLedger.userId} = ${input.userId}
|
|
2585
|
+
AND (
|
|
2586
|
+
EXISTS (
|
|
2587
|
+
SELECT 1 FROM ${budgetReservations} r
|
|
2588
|
+
WHERE r.scope = 'user'
|
|
2589
|
+
AND r.id::text = ${metadataExpr}
|
|
2590
|
+
AND r.user_id = ${usageLedger.userId}
|
|
2591
|
+
AND r.period = ${period}
|
|
2592
|
+
AND r.status NOT IN ('active', 'settled')
|
|
2593
|
+
)
|
|
2594
|
+
OR (
|
|
2595
|
+
${legacySourcePredicate}
|
|
2596
|
+
AND ${metadataExpr} IS NULL
|
|
2597
|
+
AND EXISTS (
|
|
2598
|
+
SELECT 1 FROM ${budgetReservations} r
|
|
2599
|
+
WHERE r.scope = 'user'
|
|
2600
|
+
AND r.user_id = ${usageLedger.userId}
|
|
2601
|
+
AND r.run_id = ${usageLedger.runId}
|
|
2602
|
+
AND r.period = ${period}
|
|
2603
|
+
AND r.status NOT IN ('active', 'settled')
|
|
2604
|
+
AND r.created_at <= ${usageLedger.createdAt}
|
|
2605
|
+
AND NOT EXISTS (
|
|
2606
|
+
SELECT 1 FROM ${budgetReservations} newer
|
|
2607
|
+
WHERE newer.scope = 'user'
|
|
2608
|
+
AND newer.user_id = r.user_id
|
|
2609
|
+
AND newer.run_id = r.run_id
|
|
2610
|
+
AND newer.created_at <= ${usageLedger.createdAt}
|
|
2611
|
+
AND (newer.created_at > r.created_at OR (newer.created_at = r.created_at AND newer.id > r.id))
|
|
2612
|
+
)
|
|
2613
|
+
)
|
|
2614
|
+
)
|
|
2615
|
+
OR (
|
|
2616
|
+
${legacySourcePredicate}
|
|
2617
|
+
AND ${metadataExpr} IS NULL
|
|
2618
|
+
AND ${usageLedger.createdAt} >= ${periodStartIso}::timestamp
|
|
2619
|
+
AND ${usageLedger.createdAt} < ${periodEndIso}::timestamp
|
|
2620
|
+
AND NOT EXISTS (
|
|
2621
|
+
SELECT 1 FROM ${budgetReservations} r
|
|
2622
|
+
WHERE r.scope = 'user'
|
|
2623
|
+
AND r.user_id = ${usageLedger.userId}
|
|
2624
|
+
AND r.run_id = ${usageLedger.runId}
|
|
2625
|
+
AND r.created_at <= ${usageLedger.createdAt}
|
|
2626
|
+
)
|
|
2627
|
+
)
|
|
2628
|
+
)
|
|
2629
|
+
`);
|
|
2630
|
+
return Number(usageRows[0]?.total ?? 0);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/server/db/stores/PostgresBudgetReservationStore.ts
|
|
2457
2634
|
var ModelBudgetExceededError = class extends Error {
|
|
2458
2635
|
constructor(usedMicros, heldMicros, budgetMicros, requestedMicros) {
|
|
2459
2636
|
super("Budget reached for this model.");
|
|
@@ -2470,221 +2647,210 @@ var ModelBudgetExceededError = class extends Error {
|
|
|
2470
2647
|
statusCode = 402;
|
|
2471
2648
|
code = "MODEL_BUDGET_EXCEEDED";
|
|
2472
2649
|
};
|
|
2473
|
-
var
|
|
2474
|
-
|
|
2475
|
-
|
|
2650
|
+
var UserBudgetExceededError = class extends Error {
|
|
2651
|
+
constructor(usedMicros, heldMicros, budgetMicros, requestedMicros) {
|
|
2652
|
+
super("Budget reached for this user.");
|
|
2653
|
+
this.usedMicros = usedMicros;
|
|
2654
|
+
this.heldMicros = heldMicros;
|
|
2655
|
+
this.budgetMicros = budgetMicros;
|
|
2656
|
+
this.requestedMicros = requestedMicros;
|
|
2657
|
+
this.name = "UserBudgetExceededError";
|
|
2658
|
+
}
|
|
2659
|
+
usedMicros;
|
|
2660
|
+
heldMicros;
|
|
2661
|
+
budgetMicros;
|
|
2662
|
+
requestedMicros;
|
|
2663
|
+
statusCode = 402;
|
|
2664
|
+
code = "MODEL_BUDGET_EXCEEDED";
|
|
2665
|
+
};
|
|
2666
|
+
function toBudgetReservationRow(row) {
|
|
2667
|
+
if (row.scope !== "model" && row.scope !== "user") throw new Error(`invalid budget reservation scope: ${row.scope}`);
|
|
2668
|
+
return { id: row.id, scope: row.scope, userId: row.userId, provider: row.provider ?? null, model: row.model ?? null, period: row.period };
|
|
2476
2669
|
}
|
|
2477
|
-
function
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2670
|
+
function requireModelBudgetInput(input) {
|
|
2671
|
+
if (input.scope !== "model") throw new Error("expected model budget input");
|
|
2672
|
+
return input;
|
|
2673
|
+
}
|
|
2674
|
+
var budgetScopePolicies = {
|
|
2675
|
+
model: {
|
|
2676
|
+
metadataKey: "modelBudgetReservationId",
|
|
2677
|
+
lockKey: (target) => `model-budget:${target.userId}:${target.provider ?? ""}:${target.model ?? ""}:${target.period}`,
|
|
2678
|
+
assertInput: (input) => {
|
|
2679
|
+
const modelInput = requireModelBudgetInput(input);
|
|
2680
|
+
if (!modelInput.provider.trim() || !modelInput.model.trim()) throw new Error("reserve requires provider/model");
|
|
2681
|
+
},
|
|
2682
|
+
sameReservation: (existing, input, period) => {
|
|
2683
|
+
const modelInput = requireModelBudgetInput(input);
|
|
2684
|
+
return existing.period === period && existing.provider === modelInput.provider && existing.model === modelInput.model;
|
|
2685
|
+
},
|
|
2686
|
+
exceededError: ModelBudgetExceededError,
|
|
2687
|
+
totalFilter: (input, period) => {
|
|
2688
|
+
const modelInput = requireModelBudgetInput(input);
|
|
2689
|
+
return sql9`scope = 'model' AND user_id = ${modelInput.userId} AND provider = ${modelInput.provider} AND model = ${modelInput.model} AND period = ${period}`;
|
|
2690
|
+
},
|
|
2691
|
+
scopeBudgetWhere: (input) => {
|
|
2692
|
+
const modelInput = requireModelBudgetInput(input);
|
|
2693
|
+
return [eq4(budgetReservations.provider, modelInput.provider), eq4(budgetReservations.model, modelInput.model)];
|
|
2694
|
+
}
|
|
2695
|
+
},
|
|
2696
|
+
user: {
|
|
2697
|
+
metadataKey: "userBudgetReservationId",
|
|
2698
|
+
lockKey: (target) => `user-budget:${target.userId}:${target.period}`,
|
|
2699
|
+
assertInput: () => {
|
|
2700
|
+
},
|
|
2701
|
+
sameReservation: (existing, _input, period) => existing.period === period,
|
|
2702
|
+
exceededError: UserBudgetExceededError,
|
|
2703
|
+
totalFilter: (input, period) => sql9`scope = 'user' AND user_id = ${input.userId} AND period = ${period}`,
|
|
2704
|
+
scopeBudgetWhere: () => []
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
function scopePolicy(scope) {
|
|
2708
|
+
return budgetScopePolicies[scope];
|
|
2709
|
+
}
|
|
2710
|
+
function budgetLockKey(target) {
|
|
2711
|
+
return scopePolicy(target.scope).lockKey(target);
|
|
2485
2712
|
}
|
|
2486
2713
|
function requireFinishKey(input) {
|
|
2487
|
-
if (input.reservationId) return eq4(
|
|
2714
|
+
if (input.reservationId) return and3(eq4(budgetReservations.id, input.reservationId), eq4(budgetReservations.scope, input.scope));
|
|
2488
2715
|
if (!input.runId || !input.userId) throw new Error("finish requires reservationId or runId+userId");
|
|
2489
|
-
return and3(eq4(
|
|
2716
|
+
return and3(eq4(budgetReservations.runId, input.runId), eq4(budgetReservations.userId, input.userId), eq4(budgetReservations.scope, input.scope));
|
|
2490
2717
|
}
|
|
2491
|
-
function
|
|
2492
|
-
|
|
2493
|
-
for (let index3 = 0; index3 < ids.length; index3 += UPDATE_ID_BATCH_SIZE) {
|
|
2494
|
-
chunks.push(ids.slice(index3, index3 + UPDATE_ID_BATCH_SIZE));
|
|
2495
|
-
}
|
|
2496
|
-
return chunks;
|
|
2718
|
+
function metadataKey(scope) {
|
|
2719
|
+
return scopePolicy(scope).metadataKey;
|
|
2497
2720
|
}
|
|
2498
|
-
function
|
|
2499
|
-
|
|
2721
|
+
function admissionFromResults(results) {
|
|
2722
|
+
const user = results.find((result) => result.scope === "user");
|
|
2723
|
+
const model = results.find((result) => result.scope === "model");
|
|
2724
|
+
if (!model) throw new Error("budget admission requires a model reservation");
|
|
2725
|
+
const handles = user ? [user, model] : [model];
|
|
2726
|
+
return { user, model };
|
|
2500
2727
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
for (const key of keys) {
|
|
2504
|
-
await executor.execute(sql7`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
|
|
2505
|
-
}
|
|
2728
|
+
function normalizeTarget(input, period) {
|
|
2729
|
+
return { scope: input.scope, userId: input.userId, provider: input.provider ?? null, model: input.model ?? null, period };
|
|
2506
2730
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2731
|
+
function assertReserveInput(input) {
|
|
2732
|
+
if (!input.userId) throw new Error("reserve requires userId");
|
|
2733
|
+
scopePolicy(input.scope).assertInput(input);
|
|
2734
|
+
assertPositiveSafeInteger("budgetMicros", input.budgetMicros);
|
|
2735
|
+
assertPositiveSafeInteger("holdMicros", input.holdMicros);
|
|
2736
|
+
if (!Number.isSafeInteger(input.ttlSeconds) || input.ttlSeconds <= 0) throw new Error("ttlSeconds must be a positive safe integer");
|
|
2737
|
+
}
|
|
2738
|
+
var PostgresBudgetReservationStore = class {
|
|
2739
|
+
constructor(db, options = {}) {
|
|
2509
2740
|
this.db = db;
|
|
2741
|
+
this.options = options;
|
|
2510
2742
|
}
|
|
2511
2743
|
db;
|
|
2744
|
+
options;
|
|
2512
2745
|
static monthPeriodUtc(now = /* @__PURE__ */ new Date()) {
|
|
2513
2746
|
return monthPeriodUtc(now).period;
|
|
2514
2747
|
}
|
|
2515
2748
|
async sweepExpired(now = /* @__PURE__ */ new Date()) {
|
|
2516
2749
|
return this.db.transaction(async (tx) => {
|
|
2517
2750
|
const expired = await tx.select({
|
|
2518
|
-
id:
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2751
|
+
id: budgetReservations.id,
|
|
2752
|
+
scope: budgetReservations.scope,
|
|
2753
|
+
userId: budgetReservations.userId,
|
|
2754
|
+
provider: budgetReservations.provider,
|
|
2755
|
+
model: budgetReservations.model,
|
|
2756
|
+
period: budgetReservations.period
|
|
2757
|
+
}).from(budgetReservations).where(and3(eq4(budgetReservations.status, "active"), lt2(budgetReservations.expiresAt, now)));
|
|
2524
2758
|
if (expired.length === 0) return 0;
|
|
2525
|
-
await
|
|
2759
|
+
await lockBudgetTargets(tx, expired.map(toBudgetReservationRow), budgetLockKey);
|
|
2526
2760
|
let count = 0;
|
|
2527
2761
|
for (const ids of chunkIds(expired.map((row) => row.id))) {
|
|
2528
|
-
const rows = await tx.update(
|
|
2529
|
-
|
|
2530
|
-
eq4(
|
|
2531
|
-
lt2(
|
|
2532
|
-
)).returning({ id:
|
|
2762
|
+
const rows = await tx.update(budgetReservations).set({ status: "expired" }).where(and3(
|
|
2763
|
+
inArray3(budgetReservations.id, ids),
|
|
2764
|
+
eq4(budgetReservations.status, "active"),
|
|
2765
|
+
lt2(budgetReservations.expiresAt, now)
|
|
2766
|
+
)).returning({ id: budgetReservations.id });
|
|
2533
2767
|
count += rows.length;
|
|
2534
2768
|
}
|
|
2535
2769
|
return count;
|
|
2536
2770
|
});
|
|
2537
2771
|
}
|
|
2538
2772
|
async reserve(input) {
|
|
2539
|
-
|
|
2540
|
-
if (!input.provider || !input.model) throw new Error("reserve requires provider/model");
|
|
2541
|
-
assertPositiveSafeInteger("budgetMicros", input.budgetMicros);
|
|
2542
|
-
assertPositiveSafeInteger("holdMicros", input.holdMicros);
|
|
2543
|
-
if (!Number.isSafeInteger(input.ttlSeconds) || input.ttlSeconds <= 0) throw new Error("ttlSeconds must be a positive safe integer");
|
|
2773
|
+
assertReserveInput(input);
|
|
2544
2774
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
2775
|
+
return this.db.transaction((tx) => this.reserveInTransaction(tx, input, now));
|
|
2776
|
+
}
|
|
2777
|
+
async reserveAdmission(input) {
|
|
2778
|
+
return admissionFromResults(await this.reserveMany(input.user ? [input.user, input.model] : [input.model]));
|
|
2779
|
+
}
|
|
2780
|
+
async reserveMany(inputs) {
|
|
2781
|
+
const now = inputs[0]?.now ?? /* @__PURE__ */ new Date();
|
|
2782
|
+
const period = monthPeriodUtc(now).period;
|
|
2783
|
+
for (const input of inputs) {
|
|
2784
|
+
assertReserveInput(input);
|
|
2785
|
+
if (input.now && input.now.getTime() !== now.getTime()) throw new Error("reserveMany requires all item clocks to match");
|
|
2786
|
+
}
|
|
2787
|
+
return this.db.transaction(async (tx) => {
|
|
2788
|
+
await lockBudgetTargets(tx, inputs.map((input) => normalizeTarget(input, period)), budgetLockKey);
|
|
2789
|
+
const results = [];
|
|
2790
|
+
for (const input of inputs) results.push(await this.reserveInTransaction(tx, { ...input, now }, now));
|
|
2791
|
+
return results;
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
async reserveInTransaction(tx, input, now) {
|
|
2545
2795
|
const period = monthPeriodUtc(now);
|
|
2546
2796
|
const expiresAt = new Date(now.getTime() + input.ttlSeconds * 1e3);
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
)
|
|
2797
|
+
const expired = await tx.select({
|
|
2798
|
+
id: budgetReservations.id,
|
|
2799
|
+
scope: budgetReservations.scope,
|
|
2800
|
+
userId: budgetReservations.userId,
|
|
2801
|
+
provider: budgetReservations.provider,
|
|
2802
|
+
model: budgetReservations.model,
|
|
2803
|
+
period: budgetReservations.period
|
|
2804
|
+
}).from(budgetReservations).where(and3(
|
|
2805
|
+
eq4(budgetReservations.status, "active"),
|
|
2806
|
+
lt2(budgetReservations.expiresAt, now),
|
|
2807
|
+
or2(
|
|
2808
|
+
this.scopeBudgetWhere(input, period.period),
|
|
2809
|
+
and3(eq4(budgetReservations.scope, input.scope), eq4(budgetReservations.userId, input.userId), eq4(budgetReservations.runId, input.runId))
|
|
2810
|
+
)
|
|
2811
|
+
));
|
|
2812
|
+
await lockBudgetTargets(tx, [normalizeTarget(input, period.period), ...expired.map(toBudgetReservationRow)], budgetLockKey);
|
|
2813
|
+
for (const ids of chunkIds(expired.map((row) => row.id))) {
|
|
2814
|
+
await tx.update(budgetReservations).set({ status: "expired" }).where(and3(
|
|
2815
|
+
inArray3(budgetReservations.id, ids),
|
|
2816
|
+
eq4(budgetReservations.status, "active"),
|
|
2817
|
+
lt2(budgetReservations.expiresAt, now)
|
|
2569
2818
|
));
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
AND ${usageLedger.model} = ${input.model}
|
|
2606
|
-
AND (
|
|
2607
|
-
(
|
|
2608
|
-
EXISTS (
|
|
2609
|
-
SELECT 1 FROM ${modelBudgetReservations} r
|
|
2610
|
-
WHERE r.user_id = ${usageLedger.userId}
|
|
2611
|
-
AND r.provider = ${usageLedger.provider}
|
|
2612
|
-
AND r.model = ${usageLedger.model}
|
|
2613
|
-
AND r.run_id = ${usageLedger.runId}
|
|
2614
|
-
AND r.period = ${period.period}
|
|
2615
|
-
AND r.status NOT IN ('active', 'settled')
|
|
2616
|
-
AND (
|
|
2617
|
-
${usageLedger.metadata}->>'modelBudgetReservationId' = r.id::text
|
|
2618
|
-
OR (
|
|
2619
|
-
${usageLedger.metadata}->>'modelBudgetReservationId' IS NULL
|
|
2620
|
-
AND r.created_at <= ${usageLedger.createdAt}
|
|
2621
|
-
AND NOT EXISTS (
|
|
2622
|
-
SELECT 1 FROM ${modelBudgetReservations} newer
|
|
2623
|
-
WHERE newer.user_id = r.user_id
|
|
2624
|
-
AND newer.provider = r.provider
|
|
2625
|
-
AND newer.model = r.model
|
|
2626
|
-
AND newer.run_id = r.run_id
|
|
2627
|
-
AND newer.created_at <= ${usageLedger.createdAt}
|
|
2628
|
-
AND (
|
|
2629
|
-
newer.created_at > r.created_at
|
|
2630
|
-
OR (newer.created_at = r.created_at AND newer.id > r.id)
|
|
2631
|
-
)
|
|
2632
|
-
)
|
|
2633
|
-
)
|
|
2634
|
-
)
|
|
2635
|
-
)
|
|
2636
|
-
)
|
|
2637
|
-
OR (
|
|
2638
|
-
${usageLedger.createdAt} >= ${periodStartIso}::timestamp
|
|
2639
|
-
AND ${usageLedger.createdAt} < ${periodEndIso}::timestamp
|
|
2640
|
-
AND ${usageLedger.metadata}->>'modelBudgetReservationId' IS NULL
|
|
2641
|
-
AND NOT EXISTS (
|
|
2642
|
-
SELECT 1 FROM ${modelBudgetReservations} r
|
|
2643
|
-
WHERE r.user_id = ${usageLedger.userId}
|
|
2644
|
-
AND r.provider = ${usageLedger.provider}
|
|
2645
|
-
AND r.model = ${usageLedger.model}
|
|
2646
|
-
AND r.run_id = ${usageLedger.runId}
|
|
2647
|
-
AND r.created_at <= ${usageLedger.createdAt}
|
|
2648
|
-
)
|
|
2649
|
-
)
|
|
2650
|
-
)
|
|
2651
|
-
`);
|
|
2652
|
-
const heldRows = await tx.execute(sql7`
|
|
2653
|
-
SELECT COALESCE(SUM(amount_micros), 0)::text AS total
|
|
2654
|
-
FROM ${modelBudgetReservations}
|
|
2655
|
-
WHERE status = 'active'
|
|
2656
|
-
AND user_id = ${input.userId}
|
|
2657
|
-
AND provider = ${input.provider}
|
|
2658
|
-
AND model = ${input.model}
|
|
2659
|
-
AND period = ${period.period}
|
|
2660
|
-
`);
|
|
2661
|
-
const settledFallbackRows = await tx.execute(sql7`
|
|
2662
|
-
SELECT COALESCE(SUM(amount_micros), 0)::text AS total
|
|
2663
|
-
FROM ${modelBudgetReservations}
|
|
2664
|
-
WHERE status = 'settled'
|
|
2665
|
-
AND user_id = ${input.userId}
|
|
2666
|
-
AND provider = ${input.provider}
|
|
2667
|
-
AND model = ${input.model}
|
|
2668
|
-
AND period = ${period.period}
|
|
2669
|
-
`);
|
|
2670
|
-
const usedMicros = Number(usageRows[0]?.total ?? 0) + Number(settledFallbackRows[0]?.total ?? 0);
|
|
2671
|
-
const heldMicros = Number(heldRows[0]?.total ?? 0);
|
|
2672
|
-
if (usedMicros + heldMicros + input.holdMicros > input.budgetMicros) {
|
|
2673
|
-
throw new ModelBudgetExceededError(usedMicros, heldMicros, input.budgetMicros, input.holdMicros);
|
|
2674
|
-
}
|
|
2675
|
-
const inserted = await tx.insert(modelBudgetReservations).values({
|
|
2676
|
-
userId: input.userId,
|
|
2677
|
-
workspaceId: input.workspaceId ?? null,
|
|
2678
|
-
sessionId: input.sessionId ?? null,
|
|
2679
|
-
runId: input.runId,
|
|
2680
|
-
provider: input.provider,
|
|
2681
|
-
model: input.model,
|
|
2682
|
-
period: period.period,
|
|
2683
|
-
amountMicros: input.holdMicros,
|
|
2684
|
-
expiresAt
|
|
2685
|
-
}).returning({ id: modelBudgetReservations.id });
|
|
2686
|
-
return { reservationId: inserted[0].id, created: true, period: period.period };
|
|
2687
|
-
});
|
|
2819
|
+
}
|
|
2820
|
+
const existing = await tx.select({
|
|
2821
|
+
id: budgetReservations.id,
|
|
2822
|
+
provider: budgetReservations.provider,
|
|
2823
|
+
model: budgetReservations.model,
|
|
2824
|
+
period: budgetReservations.period
|
|
2825
|
+
}).from(budgetReservations).where(and3(
|
|
2826
|
+
eq4(budgetReservations.status, "active"),
|
|
2827
|
+
eq4(budgetReservations.scope, input.scope),
|
|
2828
|
+
eq4(budgetReservations.userId, input.userId),
|
|
2829
|
+
eq4(budgetReservations.runId, input.runId)
|
|
2830
|
+
)).limit(1);
|
|
2831
|
+
const prior = existing[0];
|
|
2832
|
+
if (prior) {
|
|
2833
|
+
if (!scopePolicy(input.scope).sameReservation(prior, input, period.period)) throw new Error(`active ${input.scope} budget reservation for run ${input.runId} has conflicting target`);
|
|
2834
|
+
return { scope: input.scope, reservationId: prior.id, created: false, period: period.period };
|
|
2835
|
+
}
|
|
2836
|
+
const { usedMicros, heldMicros } = await this.spend(tx, input, period.period, period.start, period.end);
|
|
2837
|
+
if (usedMicros + heldMicros + input.holdMicros > input.budgetMicros) {
|
|
2838
|
+
const ErrorCtor = scopePolicy(input.scope).exceededError;
|
|
2839
|
+
throw new ErrorCtor(usedMicros, heldMicros, input.budgetMicros, input.holdMicros);
|
|
2840
|
+
}
|
|
2841
|
+
const inserted = await tx.insert(budgetReservations).values({
|
|
2842
|
+
userId: input.userId,
|
|
2843
|
+
workspaceId: input.workspaceId ?? null,
|
|
2844
|
+
sessionId: input.sessionId ?? null,
|
|
2845
|
+
runId: input.runId,
|
|
2846
|
+
scope: input.scope,
|
|
2847
|
+
provider: input.provider ?? null,
|
|
2848
|
+
model: input.model ?? null,
|
|
2849
|
+
period: period.period,
|
|
2850
|
+
amountMicros: input.holdMicros,
|
|
2851
|
+
expiresAt
|
|
2852
|
+
}).returning({ id: budgetReservations.id });
|
|
2853
|
+
return { scope: input.scope, reservationId: inserted[0].id, created: true, period: period.period };
|
|
2688
2854
|
}
|
|
2689
2855
|
async settle(input) {
|
|
2690
2856
|
await this.finish(input, "settled");
|
|
@@ -2692,23 +2858,126 @@ var PostgresModelBudgetStore = class {
|
|
|
2692
2858
|
async release(input) {
|
|
2693
2859
|
await this.finish(input, "released");
|
|
2694
2860
|
}
|
|
2695
|
-
|
|
2861
|
+
metadataForAdmission(admission) {
|
|
2862
|
+
return Object.fromEntries(this.handlesForAdmission(admission).map((result) => [metadataKey(result.scope), result.reservationId]));
|
|
2863
|
+
}
|
|
2864
|
+
async releaseCreated(admission) {
|
|
2865
|
+
const created = this.handlesForAdmission(admission).filter((handle) => handle.created).map((handle) => ({ scope: handle.scope, reservationId: handle.reservationId }));
|
|
2866
|
+
if (created.length > 0) await this.finishMany(created, "released");
|
|
2867
|
+
}
|
|
2868
|
+
async finishAdmission(admission, status) {
|
|
2869
|
+
await this.finishMany(this.handlesForAdmission(admission).map((handle) => ({ scope: handle.scope, reservationId: handle.reservationId })), status);
|
|
2870
|
+
}
|
|
2871
|
+
handlesForAdmission(admission) {
|
|
2872
|
+
return admission.user ? [admission.user, admission.model] : [admission.model];
|
|
2873
|
+
}
|
|
2874
|
+
async finishRun(input, status) {
|
|
2875
|
+
await this.finishMany(["model", "user"].map((scope) => ({ scope, runId: input.runId, userId: input.userId })), status);
|
|
2876
|
+
}
|
|
2877
|
+
async finishMany(inputs, status) {
|
|
2696
2878
|
await this.db.transaction(async (tx) => {
|
|
2697
|
-
const active =
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2879
|
+
const active = [];
|
|
2880
|
+
for (const input of inputs) {
|
|
2881
|
+
const scope = input.scope;
|
|
2882
|
+
const rows = await tx.select({
|
|
2883
|
+
id: budgetReservations.id,
|
|
2884
|
+
scope: budgetReservations.scope,
|
|
2885
|
+
userId: budgetReservations.userId,
|
|
2886
|
+
provider: budgetReservations.provider,
|
|
2887
|
+
model: budgetReservations.model,
|
|
2888
|
+
period: budgetReservations.period
|
|
2889
|
+
}).from(budgetReservations).where(and3(requireFinishKey({ ...input, scope }), eq4(budgetReservations.status, "active")));
|
|
2890
|
+
active.push(...rows.map(toBudgetReservationRow));
|
|
2891
|
+
}
|
|
2704
2892
|
if (active.length === 0) return;
|
|
2705
|
-
await
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2893
|
+
await lockBudgetTargets(tx, active, budgetLockKey);
|
|
2894
|
+
for (const ids of chunkIds(active.map((row) => row.id))) {
|
|
2895
|
+
await tx.update(budgetReservations).set({ status }).where(and3(
|
|
2896
|
+
inArray3(budgetReservations.id, ids),
|
|
2897
|
+
eq4(budgetReservations.status, "active")
|
|
2898
|
+
));
|
|
2899
|
+
}
|
|
2710
2900
|
});
|
|
2711
2901
|
}
|
|
2902
|
+
scopeBudgetWhere(input, period) {
|
|
2903
|
+
const base = [
|
|
2904
|
+
eq4(budgetReservations.scope, input.scope),
|
|
2905
|
+
eq4(budgetReservations.status, "active"),
|
|
2906
|
+
eq4(budgetReservations.userId, input.userId),
|
|
2907
|
+
eq4(budgetReservations.period, period)
|
|
2908
|
+
];
|
|
2909
|
+
base.push(...scopePolicy(input.scope).scopeBudgetWhere(input));
|
|
2910
|
+
return and3(...base);
|
|
2911
|
+
}
|
|
2912
|
+
async reservationTotal(tx, input, period, status) {
|
|
2913
|
+
const scopeFilter = scopePolicy(input.scope).totalFilter(input, period);
|
|
2914
|
+
const rows = await tx.execute(sql9`
|
|
2915
|
+
SELECT COALESCE(SUM(amount_micros), 0)::text AS total
|
|
2916
|
+
FROM ${budgetReservations}
|
|
2917
|
+
WHERE status = ${status} AND ${scopeFilter}
|
|
2918
|
+
`);
|
|
2919
|
+
return Number(rows[0]?.total ?? 0);
|
|
2920
|
+
}
|
|
2921
|
+
async spend(tx, input, period, start, end) {
|
|
2922
|
+
return computeBudgetSpend(
|
|
2923
|
+
tx,
|
|
2924
|
+
input,
|
|
2925
|
+
period,
|
|
2926
|
+
start,
|
|
2927
|
+
end,
|
|
2928
|
+
this.options.eligibleLegacySources ?? [],
|
|
2929
|
+
(budgetInput, usageMicros) => this.spendTotals(tx, budgetInput, period, usageMicros)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
async spendTotals(tx, input, period, usageMicros) {
|
|
2933
|
+
const heldMicros = await this.reservationTotal(tx, input, period, "active");
|
|
2934
|
+
const settledFallbackMicros = await this.reservationTotal(tx, input, period, "settled");
|
|
2935
|
+
return { usedMicros: usageMicros + settledFallbackMicros, heldMicros };
|
|
2936
|
+
}
|
|
2937
|
+
async finish(input, status) {
|
|
2938
|
+
await this.db.transaction((tx) => this.finishInTransaction(tx, input, status));
|
|
2939
|
+
}
|
|
2940
|
+
async finishInTransaction(tx, input, status) {
|
|
2941
|
+
const scope = input.scope;
|
|
2942
|
+
const active = await tx.select({
|
|
2943
|
+
id: budgetReservations.id,
|
|
2944
|
+
scope: budgetReservations.scope,
|
|
2945
|
+
userId: budgetReservations.userId,
|
|
2946
|
+
provider: budgetReservations.provider,
|
|
2947
|
+
model: budgetReservations.model,
|
|
2948
|
+
period: budgetReservations.period
|
|
2949
|
+
}).from(budgetReservations).where(and3(requireFinishKey({ ...input, scope }), eq4(budgetReservations.status, "active")));
|
|
2950
|
+
if (active.length === 0) return;
|
|
2951
|
+
await lockBudgetTargets(tx, active.map(toBudgetReservationRow), budgetLockKey);
|
|
2952
|
+
await tx.update(budgetReservations).set({ status }).where(and3(
|
|
2953
|
+
inArray3(budgetReservations.id, active.map((row) => row.id)),
|
|
2954
|
+
eq4(budgetReservations.status, "active")
|
|
2955
|
+
));
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2959
|
+
// src/server/db/stores/PostgresModelBudgetStore.ts
|
|
2960
|
+
var PostgresModelBudgetStore = class {
|
|
2961
|
+
store;
|
|
2962
|
+
constructor(db) {
|
|
2963
|
+
this.store = new PostgresBudgetReservationStore(db);
|
|
2964
|
+
}
|
|
2965
|
+
static monthPeriodUtc(now = /* @__PURE__ */ new Date()) {
|
|
2966
|
+
return PostgresBudgetReservationStore.monthPeriodUtc(now);
|
|
2967
|
+
}
|
|
2968
|
+
sweepExpired(now = /* @__PURE__ */ new Date()) {
|
|
2969
|
+
return this.store.sweepExpired(now);
|
|
2970
|
+
}
|
|
2971
|
+
async reserve(input) {
|
|
2972
|
+
const { scope: _scope, ...result } = await this.store.reserve({ ...input, scope: "model" });
|
|
2973
|
+
return result;
|
|
2974
|
+
}
|
|
2975
|
+
settle(input) {
|
|
2976
|
+
return this.store.settle({ ...input, scope: "model" });
|
|
2977
|
+
}
|
|
2978
|
+
release(input) {
|
|
2979
|
+
return this.store.release({ ...input, scope: "model" });
|
|
2980
|
+
}
|
|
2712
2981
|
};
|
|
2713
2982
|
|
|
2714
2983
|
export {
|
|
@@ -2723,6 +2992,7 @@ export {
|
|
|
2723
2992
|
creditGrants,
|
|
2724
2993
|
creditPurchases,
|
|
2725
2994
|
usageReservations,
|
|
2995
|
+
modelBudgetReservations,
|
|
2726
2996
|
usageLedger,
|
|
2727
2997
|
telemetryEvents,
|
|
2728
2998
|
schema_exports,
|
|
@@ -2735,5 +3005,7 @@ export {
|
|
|
2735
3005
|
InsufficientCreditError,
|
|
2736
3006
|
PostgresMeteringStore,
|
|
2737
3007
|
ModelBudgetExceededError,
|
|
3008
|
+
UserBudgetExceededError,
|
|
3009
|
+
PostgresBudgetReservationStore,
|
|
2738
3010
|
PostgresModelBudgetStore
|
|
2739
3011
|
};
|