omnigateway 0.10.3 → 0.11.1
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 +19 -2
- package/bin/omni.js +621 -333
- package/gateway.js +715 -395
- package/package.json +2 -2
- package/public/assets/{Chip-BeKVtewW.js → Chip-BBNaA-sL.js} +1 -1
- package/public/assets/{CopyValue-NTHhiCuT.js → CopyValue-DQMqneTT.js} +1 -1
- package/public/assets/{Lamp-C2Q3t5Mn.js → Lamp-CosPf-gw.js} +7 -7
- package/public/assets/Rack-BGrdPQRN.js +476 -0
- package/public/assets/RequestTable-B5pOnwDe.js +11 -0
- package/public/assets/SummaryDeck-DAvCcP_N.js +15 -0
- package/public/assets/{Toggle-DwKz1aVq.js → Toggle-ZS3zIMNF.js} +1 -1
- package/public/assets/{TokenBreakdown-DM-1HvOr.js → TokenBreakdown-VV2U0Fgv.js} +1 -1
- package/public/assets/{WindowChart-BsonO5Py.js → WindowChart-eqPzFE06.js} +2 -2
- package/public/assets/_app.accounts-wJdiS7Ue.js +59 -0
- package/public/assets/{_app.console-BQJMLRv4.js → _app.console-DPuEIJl7.js} +2 -2
- package/public/assets/{_app.database-BIiajZm6.js → _app.database-BWxxqrNy.js} +4 -4
- package/public/assets/_app.index-C_jGPDHc.js +67 -0
- package/public/assets/_app.keys-vLOZaX9s.js +92 -0
- package/public/assets/{_app.logs-AyVq3lLb.js → _app.logs-4fRTXz5H.js} +3 -3
- package/public/assets/{_app.models-CwlkGPrx.js → _app.models-BFWk7FBf.js} +4 -4
- package/public/assets/{_app.plugins._pluginId-BhvEnawY.js → _app.plugins._pluginId-B2R5TEp9.js} +1 -1
- package/public/assets/_app.settings-BKUhzFsw.js +88 -0
- package/public/assets/_app.usage-Dme4K70C.js +76 -0
- package/public/assets/{chevron-right-S84SGCvk.js → chevron-right-CL2vJUko.js} +1 -1
- package/public/assets/client-DIfYMjPd.js +39 -0
- package/public/assets/index-teScS3wF.js +13 -0
- package/public/assets/{login-CitmheGz.js → login-CCLJgPoH.js} +2 -2
- package/public/assets/plus-C3pkG04C.js +1 -0
- package/public/assets/{preload-helper-BC9bKUg1.js → preload-helper-Dv9CFok1.js} +1 -1
- package/public/assets/{shared-C47BfV_u.js → shared-5YRtk6PC.js} +12 -12
- package/public/assets/{trash-2-DmtElUHr.js → trash-2-CLOg_lJ7.js} +1 -1
- package/public/index.html +4 -4
- package/public/assets/Rack-sUGRoZUx.js +0 -476
- package/public/assets/RequestTable-C2RLARWy.js +0 -11
- package/public/assets/SummaryDeck-BHXcb_Ji.js +0 -15
- package/public/assets/_app.accounts-DUYxItf0.js +0 -59
- package/public/assets/_app.index-DagK0Pqp.js +0 -62
- package/public/assets/_app.keys-DLAG_bmt.js +0 -82
- package/public/assets/_app.settings-BKA-1YLs.js +0 -88
- package/public/assets/_app.usage-C7JEWKZM.js +0 -76
- package/public/assets/client-DCvpw0LS.js +0 -39
- package/public/assets/index-pZbadEun.js +0 -13
- package/public/assets/plus-E7qLz9CF.js +0 -1
package/bin/omni.js
CHANGED
|
@@ -75,7 +75,10 @@ function note(ctx, writer, message) {
|
|
|
75
75
|
function formatTime(at) {
|
|
76
76
|
if (at === null || !Number.isFinite(at))
|
|
77
77
|
return "\u2014";
|
|
78
|
-
|
|
78
|
+
const date = new Date(at);
|
|
79
|
+
if (Number.isNaN(date.getTime()))
|
|
80
|
+
return "out of range";
|
|
81
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
79
82
|
}
|
|
80
83
|
function formatAge(from, now) {
|
|
81
84
|
if (from === null || !Number.isFinite(from))
|
|
@@ -19238,13 +19241,13 @@ function prepareArtifact(input2) {
|
|
|
19238
19241
|
error: error61.value
|
|
19239
19242
|
};
|
|
19240
19243
|
const json2 = JSON.stringify(bounded);
|
|
19241
|
-
const size =
|
|
19244
|
+
const size = Buffer.byteLength(json2);
|
|
19242
19245
|
if (size <= MAX_ARTIFACT_BYTES)
|
|
19243
19246
|
return { artifact: bounded, json: json2 };
|
|
19244
19247
|
const marker = omission(size);
|
|
19245
19248
|
const omitted = omitBodies(bounded, marker);
|
|
19246
19249
|
const omittedJson = JSON.stringify(omitted);
|
|
19247
|
-
if (
|
|
19250
|
+
if (Buffer.byteLength(omittedJson) <= MAX_ARTIFACT_BYTES) {
|
|
19248
19251
|
return { artifact: omitted, json: omittedJson };
|
|
19249
19252
|
}
|
|
19250
19253
|
const stripped = { ...omitted, error: marker };
|
|
@@ -19504,6 +19507,261 @@ function estimateCachedInputTokens(request) {
|
|
|
19504
19507
|
}
|
|
19505
19508
|
return cached2;
|
|
19506
19509
|
}
|
|
19510
|
+
// packages/store/src/sqlite/rollup.ts
|
|
19511
|
+
var HOUR_MS = 3600000;
|
|
19512
|
+
function hourOf(at) {
|
|
19513
|
+
return Math.floor(at / HOUR_MS);
|
|
19514
|
+
}
|
|
19515
|
+
var DAY_MS = 86400000;
|
|
19516
|
+
function hostDayOffsetMinutes() {
|
|
19517
|
+
return -new Date().getTimezoneOffset() || 0;
|
|
19518
|
+
}
|
|
19519
|
+
function startOfDay(at, offsetMinutes) {
|
|
19520
|
+
const offsetMs = offsetMinutes * 60000;
|
|
19521
|
+
return Math.floor((at + offsetMs) / DAY_MS) * DAY_MS - offsetMs;
|
|
19522
|
+
}
|
|
19523
|
+
var UPSERT = `
|
|
19524
|
+
INSERT INTO usage_daily
|
|
19525
|
+
(day, provider, credential_id, requested_model, resolved_model, api_key_id,
|
|
19526
|
+
requests, errors, input_tokens, output_tokens, cache_read_tokens,
|
|
19527
|
+
cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
|
|
19528
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
19529
|
+
ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
|
|
19530
|
+
DO UPDATE SET
|
|
19531
|
+
requests = requests + excluded.requests,
|
|
19532
|
+
errors = errors + excluded.errors,
|
|
19533
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
19534
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
19535
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
19536
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
19537
|
+
rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
|
|
19538
|
+
rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
|
|
19539
|
+
cost_usd = cost_usd + excluded.cost_usd,
|
|
19540
|
+
duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
|
|
19541
|
+
function keyOf(log, dayOffsetMinutes) {
|
|
19542
|
+
return [
|
|
19543
|
+
startOfDay(log.at, dayOffsetMinutes),
|
|
19544
|
+
log.resolvedProvider ?? "",
|
|
19545
|
+
log.credentialId ?? "",
|
|
19546
|
+
log.requestedModel,
|
|
19547
|
+
log.resolvedModel ?? "",
|
|
19548
|
+
log.apiKeyId ?? ""
|
|
19549
|
+
];
|
|
19550
|
+
}
|
|
19551
|
+
function countersOf(log) {
|
|
19552
|
+
return {
|
|
19553
|
+
requests: 1,
|
|
19554
|
+
errors: log.status >= 400 ? 1 : 0,
|
|
19555
|
+
inputTokens: log.inputTokens,
|
|
19556
|
+
outputTokens: log.outputTokens,
|
|
19557
|
+
cacheReadTokens: log.cacheReadTokens,
|
|
19558
|
+
cacheWriteTokens: log.cacheWriteTokens,
|
|
19559
|
+
rtkSavedTokens: log.rtkEstimatedTokensSaved,
|
|
19560
|
+
rtkAppliedRequests: log.rtkApplied ? 1 : 0,
|
|
19561
|
+
costUsd: log.costUsd,
|
|
19562
|
+
durationMsSum: log.durationMs
|
|
19563
|
+
};
|
|
19564
|
+
}
|
|
19565
|
+
var BACKFILL_UPSERT = `
|
|
19566
|
+
INSERT INTO usage_daily
|
|
19567
|
+
(day, provider, credential_id, requested_model, resolved_model, api_key_id,
|
|
19568
|
+
requests, errors, input_tokens, output_tokens, cache_read_tokens,
|
|
19569
|
+
cache_write_tokens, cost_usd, duration_ms_sum)
|
|
19570
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
19571
|
+
ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
|
|
19572
|
+
DO UPDATE SET
|
|
19573
|
+
requests = requests + excluded.requests,
|
|
19574
|
+
errors = errors + excluded.errors,
|
|
19575
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
19576
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
19577
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
19578
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
19579
|
+
cost_usd = cost_usd + excluded.cost_usd,
|
|
19580
|
+
duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
|
|
19581
|
+
function upsertPreRtk(db, key, c) {
|
|
19582
|
+
db.run(BACKFILL_UPSERT, [
|
|
19583
|
+
...key,
|
|
19584
|
+
c.requests,
|
|
19585
|
+
c.errors,
|
|
19586
|
+
c.inputTokens,
|
|
19587
|
+
c.outputTokens,
|
|
19588
|
+
c.cacheReadTokens,
|
|
19589
|
+
c.cacheWriteTokens,
|
|
19590
|
+
c.costUsd,
|
|
19591
|
+
c.durationMsSum
|
|
19592
|
+
]);
|
|
19593
|
+
}
|
|
19594
|
+
function upsert(db, key, c) {
|
|
19595
|
+
db.run(UPSERT, [
|
|
19596
|
+
...key,
|
|
19597
|
+
c.requests,
|
|
19598
|
+
c.errors,
|
|
19599
|
+
c.inputTokens,
|
|
19600
|
+
c.outputTokens,
|
|
19601
|
+
c.cacheReadTokens,
|
|
19602
|
+
c.cacheWriteTokens,
|
|
19603
|
+
c.rtkSavedTokens,
|
|
19604
|
+
c.rtkAppliedRequests,
|
|
19605
|
+
c.costUsd,
|
|
19606
|
+
c.durationMsSum
|
|
19607
|
+
]);
|
|
19608
|
+
}
|
|
19609
|
+
function rollupLog(db, log, dayOffsetMinutes) {
|
|
19610
|
+
upsert(db, keyOf(log, dayOffsetMinutes), countersOf(log));
|
|
19611
|
+
}
|
|
19612
|
+
function backfillDaily(db, dayOffsetMinutes) {
|
|
19613
|
+
const groups = new Map;
|
|
19614
|
+
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
19615
|
+
status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
19616
|
+
duration_ms, cost_usd
|
|
19617
|
+
FROM request_logs`).all()) {
|
|
19618
|
+
const key = [
|
|
19619
|
+
startOfDay(row.at, dayOffsetMinutes),
|
|
19620
|
+
row.resolved_provider ?? "",
|
|
19621
|
+
row.credential_id ?? "",
|
|
19622
|
+
row.requested_model,
|
|
19623
|
+
row.resolved_model ?? "",
|
|
19624
|
+
row.api_key_id ?? ""
|
|
19625
|
+
];
|
|
19626
|
+
const id = key.join("\x00");
|
|
19627
|
+
const seen = groups.get(id);
|
|
19628
|
+
const counters = seen?.counters ?? {
|
|
19629
|
+
requests: 0,
|
|
19630
|
+
errors: 0,
|
|
19631
|
+
inputTokens: 0,
|
|
19632
|
+
outputTokens: 0,
|
|
19633
|
+
cacheReadTokens: 0,
|
|
19634
|
+
cacheWriteTokens: 0,
|
|
19635
|
+
rtkSavedTokens: 0,
|
|
19636
|
+
rtkAppliedRequests: 0,
|
|
19637
|
+
costUsd: 0,
|
|
19638
|
+
durationMsSum: 0
|
|
19639
|
+
};
|
|
19640
|
+
counters.requests += 1;
|
|
19641
|
+
counters.errors += row.status >= 400 ? 1 : 0;
|
|
19642
|
+
counters.inputTokens += row.input_tokens;
|
|
19643
|
+
counters.outputTokens += row.output_tokens;
|
|
19644
|
+
counters.cacheReadTokens += row.cache_read_tokens;
|
|
19645
|
+
counters.cacheWriteTokens += row.cache_write_tokens;
|
|
19646
|
+
counters.costUsd += row.cost_usd;
|
|
19647
|
+
counters.durationMsSum += row.duration_ms;
|
|
19648
|
+
if (seen === undefined)
|
|
19649
|
+
groups.set(id, { key, counters });
|
|
19650
|
+
}
|
|
19651
|
+
for (const group of groups.values())
|
|
19652
|
+
upsertPreRtk(db, group.key, group.counters);
|
|
19653
|
+
return groups.size;
|
|
19654
|
+
}
|
|
19655
|
+
var HOURLY_UPSERT = `
|
|
19656
|
+
INSERT INTO usage_rollup
|
|
19657
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
19658
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
19659
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
19660
|
+
ON CONFLICT (api_key_id, hour)
|
|
19661
|
+
DO UPDATE SET
|
|
19662
|
+
requests = requests + excluded.requests,
|
|
19663
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
19664
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
19665
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
19666
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
19667
|
+
cost_usd = cost_usd + excluded.cost_usd`;
|
|
19668
|
+
function rollupHour(db, log) {
|
|
19669
|
+
const apiKeyId = log.apiKeyId;
|
|
19670
|
+
if (apiKeyId === null)
|
|
19671
|
+
return;
|
|
19672
|
+
db.run(HOURLY_UPSERT, [
|
|
19673
|
+
apiKeyId,
|
|
19674
|
+
hourOf(log.at),
|
|
19675
|
+
1,
|
|
19676
|
+
log.inputTokens,
|
|
19677
|
+
log.outputTokens,
|
|
19678
|
+
log.cacheReadTokens,
|
|
19679
|
+
log.cacheWriteTokens,
|
|
19680
|
+
log.costUsd
|
|
19681
|
+
]);
|
|
19682
|
+
}
|
|
19683
|
+
var REBUILD = `
|
|
19684
|
+
INSERT INTO usage_rollup
|
|
19685
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
19686
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
19687
|
+
SELECT api_key_id,
|
|
19688
|
+
CAST(at / 3600000 AS INTEGER),
|
|
19689
|
+
COUNT(*),
|
|
19690
|
+
COALESCE(SUM(input_tokens), 0),
|
|
19691
|
+
COALESCE(SUM(output_tokens), 0),
|
|
19692
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
19693
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
19694
|
+
COALESCE(SUM(cost_usd), 0)
|
|
19695
|
+
FROM request_logs
|
|
19696
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
19697
|
+
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
19698
|
+
function rebuildRollup(db) {
|
|
19699
|
+
db.run("DELETE FROM usage_rollup");
|
|
19700
|
+
db.run(REBUILD);
|
|
19701
|
+
}
|
|
19702
|
+
function auditRollup(db) {
|
|
19703
|
+
const row = db.query(`WITH truth AS (
|
|
19704
|
+
SELECT api_key_id AS k,
|
|
19705
|
+
CAST(at / 3600000 AS INTEGER) AS h,
|
|
19706
|
+
COUNT(*) AS requests,
|
|
19707
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
19708
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
19709
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
19710
|
+
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
19711
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
19712
|
+
FROM request_logs
|
|
19713
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
19714
|
+
GROUP BY k, h
|
|
19715
|
+
)
|
|
19716
|
+
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
19717
|
+
(SELECT COUNT(*)
|
|
19718
|
+
FROM truth
|
|
19719
|
+
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
19720
|
+
WHERE r.api_key_id IS NULL
|
|
19721
|
+
OR r.requests <> truth.requests
|
|
19722
|
+
OR r.input_tokens <> truth.input_tokens
|
|
19723
|
+
OR r.output_tokens <> truth.output_tokens
|
|
19724
|
+
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
19725
|
+
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
19726
|
+
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
19727
|
+
) AS mismatched,
|
|
19728
|
+
(SELECT COUNT(*)
|
|
19729
|
+
FROM usage_rollup r
|
|
19730
|
+
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
19731
|
+
WHERE truth.k IS NULL
|
|
19732
|
+
) AS orphans`).get();
|
|
19733
|
+
const buckets = row?.buckets ?? 0;
|
|
19734
|
+
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
19735
|
+
return { buckets, mismatched, ok: mismatched === 0 };
|
|
19736
|
+
}
|
|
19737
|
+
function backfillRtkUsage(db, dayOffsetMinutes) {
|
|
19738
|
+
const groups = new Map;
|
|
19739
|
+
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
19740
|
+
rtk_applied, rtk_estimated_tokens_saved
|
|
19741
|
+
FROM request_logs
|
|
19742
|
+
WHERE state = 'done'`).all()) {
|
|
19743
|
+
const key = [
|
|
19744
|
+
startOfDay(row.at, dayOffsetMinutes),
|
|
19745
|
+
row.resolved_provider ?? "",
|
|
19746
|
+
row.credential_id ?? "",
|
|
19747
|
+
row.requested_model,
|
|
19748
|
+
row.resolved_model ?? "",
|
|
19749
|
+
row.api_key_id ?? ""
|
|
19750
|
+
];
|
|
19751
|
+
const id = key.join("\x00");
|
|
19752
|
+
const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
|
|
19753
|
+
group.saved += row.rtk_estimated_tokens_saved;
|
|
19754
|
+
group.applied += row.rtk_applied === 1 ? 1 : 0;
|
|
19755
|
+
groups.set(id, group);
|
|
19756
|
+
}
|
|
19757
|
+
for (const { key, saved, applied } of groups.values()) {
|
|
19758
|
+
db.run(`UPDATE usage_daily
|
|
19759
|
+
SET rtk_saved_tokens = ?, rtk_applied_requests = ?
|
|
19760
|
+
WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
|
|
19761
|
+
AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
|
|
19762
|
+
}
|
|
19763
|
+
}
|
|
19764
|
+
|
|
19507
19765
|
// packages/store/src/postgres/db.ts
|
|
19508
19766
|
var {SQL } = globalThis.Bun;
|
|
19509
19767
|
|
|
@@ -19809,11 +20067,95 @@ CREATE TRIGGER api_keys_config_version
|
|
|
19809
20067
|
FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
|
|
19810
20068
|
`;
|
|
19811
20069
|
|
|
20070
|
+
// packages/store/src/postgres/migrations/002_health_measurements.sql
|
|
20071
|
+
var _002_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before applying to a fleet.
|
|
20072
|
+
--
|
|
20073
|
+
-- This migration drops two columns that every gateway before it writes on
|
|
20074
|
+
-- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
|
|
20075
|
+
-- The first new replica to boot applies it under the migration lock; from that
|
|
20076
|
+
-- moment every OLD replica still running throws on every successful request:
|
|
20077
|
+
-- the upstream call has completed and been billed, the client has received its
|
|
20078
|
+
-- content frames, and then the connection aborts with no terminal frame. The
|
|
20079
|
+
-- \`RollingUpdate\` in \`k8s/deployment.yaml\` does not protect against this \u2014 the
|
|
20080
|
+
-- "surviving" replica is the one that breaks.
|
|
20081
|
+
--
|
|
20082
|
+
-- Required: scale to ONE replica before deploying this release, or accept that
|
|
20083
|
+
-- every old replica fails every successful request until it is replaced.
|
|
20084
|
+
-- Rollback is unsafe: an old image cannot re-add the columns, and running it
|
|
20085
|
+
-- against this schema fails the same way. The only way back is a snapshot
|
|
20086
|
+
-- restore taken before the deploy. Deliberate: kept as a drop, not
|
|
20087
|
+
-- expand/contract, on the operator's decision \u2014 see the write-path spec.
|
|
20088
|
+
--
|
|
20089
|
+
-- \`credential_health\` keeps decisions, not measurements.
|
|
20090
|
+
--
|
|
20091
|
+
-- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
|
|
20092
|
+
-- and a success that changes nothing else now writes nothing. Both live in the
|
|
20093
|
+
-- gateway's process-local \`loadRegistry\` instead. The console's "last used"
|
|
20094
|
+
-- reads \`request_logs\` through \`usage.lastUsedByCredential\`, served by
|
|
20095
|
+
-- \`idx_request_logs_cred\`.
|
|
20096
|
+
ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
|
|
20097
|
+
ALTER TABLE credential_health DROP COLUMN last_used_at;
|
|
20098
|
+
|
|
20099
|
+
-- The statement-level trigger bumped \`config_version\` \u2014 one row, locked until
|
|
20100
|
+
-- commit, by every writer on every replica \u2014 on every health write, and every
|
|
20101
|
+
-- bump makes every replica rebuild its routing snapshot. Only a change to a
|
|
20102
|
+
-- column routing *decides* on needs that; a bare \`consecutive_failures\`
|
|
20103
|
+
-- increment is patched into the held snapshot from the \`healthSaved\` change.
|
|
20104
|
+
--
|
|
20105
|
+
-- The INSERT arm must stay \`FOR EACH ROW\`. The write is an upsert, and a
|
|
20106
|
+
-- statement-level INSERT trigger fires on an upsert whichever path each row
|
|
20107
|
+
-- took, which would bump on every write and make the WHEN clause below moot.
|
|
20108
|
+
-- Row-level \`AFTER INSERT\` fires only for rows actually inserted.
|
|
20109
|
+
DROP TRIGGER credential_health_config_version ON credential_health;
|
|
20110
|
+
|
|
20111
|
+
CREATE TRIGGER credential_health_config_version_upd
|
|
20112
|
+
AFTER UPDATE ON credential_health
|
|
20113
|
+
FOR EACH ROW
|
|
20114
|
+
WHEN (OLD.breaker_state IS DISTINCT FROM NEW.breaker_state
|
|
20115
|
+
OR OLD.rate_limited_until IS DISTINCT FROM NEW.rate_limited_until
|
|
20116
|
+
OR OLD.opened_at IS DISTINCT FROM NEW.opened_at)
|
|
20117
|
+
EXECUTE FUNCTION bump_config_version();
|
|
20118
|
+
|
|
20119
|
+
CREATE TRIGGER credential_health_config_version_ins
|
|
20120
|
+
AFTER INSERT ON credential_health
|
|
20121
|
+
FOR EACH ROW EXECUTE FUNCTION bump_config_version();
|
|
20122
|
+
|
|
20123
|
+
-- DELETE needs no WHEN, so it stays statement-level as in 001: a credential
|
|
20124
|
+
-- with M model rows bumps once, not M times.
|
|
20125
|
+
CREATE TRIGGER credential_health_config_version_del
|
|
20126
|
+
AFTER DELETE ON credential_health
|
|
20127
|
+
FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
|
|
20128
|
+
`;
|
|
20129
|
+
|
|
20130
|
+
// packages/store/src/postgres/migrations/003_key_expiry.sql
|
|
20131
|
+
var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of the same name. \`BIGINT\`, like
|
|
20132
|
+
-- every other epoch-ms column in this schema, and read back through \`numOrNull\`.
|
|
20133
|
+
--
|
|
20134
|
+
-- Nullable rather than \`NOT NULL DEFAULT\`: the column is a deadline, and every
|
|
20135
|
+
-- finite default is a date on which somebody's client stops working. NULL is
|
|
20136
|
+
-- the only value that says nothing was decided, and every reader maps it to
|
|
20137
|
+
-- "never expires".
|
|
20138
|
+
--
|
|
20139
|
+
-- No backfill, deliberately. Every key on every existing installation was
|
|
20140
|
+
-- minted on the promise that it works until revoked, so adding the column and
|
|
20141
|
+
-- stopping is what keeps that promise. The absence of an \`UPDATE\` here is the
|
|
20142
|
+
-- substance of the migration, not an omission.
|
|
20143
|
+
--
|
|
20144
|
+
-- Safe to apply under a rolling deploy, unlike migration 2: this adds a
|
|
20145
|
+
-- nullable column and drops nothing, so a replica running the previous image
|
|
20146
|
+
-- keeps serving \u2014 \`SELECT *\` gains a field its mapper ignores.
|
|
20147
|
+
ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
|
|
20148
|
+
`;
|
|
20149
|
+
|
|
19812
20150
|
// packages/store/src/postgres/db.ts
|
|
19813
20151
|
var num = (v) => Number(v);
|
|
19814
20152
|
var numOrNull = (v) => v === null ? null : Number(v);
|
|
19815
20153
|
var MIGRATION_LOCK = 7140641;
|
|
19816
|
-
var MIGRATIONS = [
|
|
20154
|
+
var MIGRATIONS = [
|
|
20155
|
+
{ id: 1, sql: _001_init_default },
|
|
20156
|
+
{ id: 2, sql: _002_health_measurements_default },
|
|
20157
|
+
{ id: 3, sql: _003_key_expiry_default }
|
|
20158
|
+
];
|
|
19817
20159
|
async function openPg(url2) {
|
|
19818
20160
|
const sql = new SQL({ url: url2 });
|
|
19819
20161
|
const conn = await sql.reserve();
|
|
@@ -20010,6 +20352,9 @@ function resolvePin(target, accounts) {
|
|
|
20010
20352
|
return;
|
|
20011
20353
|
return accounts.find((account) => servesTarget(target, account));
|
|
20012
20354
|
}
|
|
20355
|
+
var keyUsable = (key, now) => key.revokedAt === null && (key.expiresAt === null || key.expiresAt > now);
|
|
20356
|
+
var credentialPastExpiry = (credential, now) => credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now;
|
|
20357
|
+
var credentialExpired = (credential, now) => credentialPastExpiry(credential, now) && !credential.hasRefreshToken;
|
|
20013
20358
|
var NODE_GRACE_MS = 60000;
|
|
20014
20359
|
var DEFAULT_SETTINGS = {
|
|
20015
20360
|
weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
|
|
@@ -20137,31 +20482,25 @@ function toHealth(r) {
|
|
|
20137
20482
|
breakerState: r.breaker_state,
|
|
20138
20483
|
consecutiveFailures: r.consecutive_failures,
|
|
20139
20484
|
openedAt: numOrNull(r.opened_at),
|
|
20140
|
-
rateLimitedUntil: numOrNull(r.rate_limited_until)
|
|
20141
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
20142
|
-
lastUsedAt: numOrNull(r.last_used_at)
|
|
20485
|
+
rateLimitedUntil: numOrNull(r.rate_limited_until)
|
|
20143
20486
|
};
|
|
20144
20487
|
}
|
|
20145
20488
|
var UPSERT_HEALTH = `INSERT INTO credential_health
|
|
20146
20489
|
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
20147
|
-
rate_limited_until
|
|
20148
|
-
VALUES ($1,$2,$3,$4,$5,$6
|
|
20490
|
+
rate_limited_until)
|
|
20491
|
+
VALUES ($1,$2,$3,$4,$5,$6)
|
|
20149
20492
|
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
20150
20493
|
breaker_state = EXCLUDED.breaker_state,
|
|
20151
20494
|
consecutive_failures = EXCLUDED.consecutive_failures,
|
|
20152
20495
|
opened_at = EXCLUDED.opened_at,
|
|
20153
|
-
rate_limited_until = EXCLUDED.rate_limited_until
|
|
20154
|
-
ewma_ttft_ms = EXCLUDED.ewma_ttft_ms,
|
|
20155
|
-
last_used_at = EXCLUDED.last_used_at`;
|
|
20496
|
+
rate_limited_until = EXCLUDED.rate_limited_until`;
|
|
20156
20497
|
var healthValues = (r) => [
|
|
20157
20498
|
r.credentialId,
|
|
20158
20499
|
r.model,
|
|
20159
20500
|
r.breakerState,
|
|
20160
20501
|
r.consecutiveFailures,
|
|
20161
20502
|
r.openedAt,
|
|
20162
|
-
r.rateLimitedUntil
|
|
20163
|
-
r.ewmaTtftMs,
|
|
20164
|
-
r.lastUsedAt
|
|
20503
|
+
r.rateLimitedUntil
|
|
20165
20504
|
];
|
|
20166
20505
|
function createCredentialRepo(sql, key, emit2) {
|
|
20167
20506
|
const open2 = async (v) => v === null ? null : decrypt(key, v);
|
|
@@ -20354,13 +20693,6 @@ function createCredentialRepo(sql, key, emit2) {
|
|
|
20354
20693
|
const rows = await sql.unsafe("SELECT * FROM credential_health");
|
|
20355
20694
|
return rows.map(toHealth);
|
|
20356
20695
|
},
|
|
20357
|
-
async saveHealth(rows) {
|
|
20358
|
-
await sql.begin(async (tx) => {
|
|
20359
|
-
for (const r of rows)
|
|
20360
|
-
await tx.unsafe(UPSERT_HEALTH, healthValues(r));
|
|
20361
|
-
});
|
|
20362
|
-
emit2({ type: "healthSaved", rows });
|
|
20363
|
-
},
|
|
20364
20696
|
async updateHealth(credentialId, model, apply) {
|
|
20365
20697
|
const written = await sql.begin(async (tx) => {
|
|
20366
20698
|
await tx.unsafe("SELECT pg_advisory_xact_lock(hashtext($1))", [`${credentialId}|${model}`]);
|
|
@@ -20502,7 +20834,8 @@ var toKey = (r, logger2) => ({
|
|
|
20502
20834
|
limits: parseLimits(r.id, r.limits, logger2),
|
|
20503
20835
|
bodyLoggingOptOut: r.body_logging_opt_out,
|
|
20504
20836
|
createdAt: num(r.created_at),
|
|
20505
|
-
revokedAt: numOrNull(r.revoked_at)
|
|
20837
|
+
revokedAt: numOrNull(r.revoked_at),
|
|
20838
|
+
expiresAt: numOrNull(r.expires_at)
|
|
20506
20839
|
});
|
|
20507
20840
|
function createKeyRepo(sql, logger2) {
|
|
20508
20841
|
const one = async (where, arg) => {
|
|
@@ -20520,8 +20853,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20520
20853
|
async create(input2) {
|
|
20521
20854
|
const now = Date.now();
|
|
20522
20855
|
await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
20523
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
20524
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL)`, [
|
|
20856
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
20857
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9)`, [
|
|
20525
20858
|
input2.id,
|
|
20526
20859
|
input2.label,
|
|
20527
20860
|
input2.prefix,
|
|
@@ -20529,7 +20862,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20529
20862
|
input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
|
|
20530
20863
|
JSON.stringify(parseLimitConfig(input2.limits)),
|
|
20531
20864
|
input2.bodyLoggingOptOut,
|
|
20532
|
-
now
|
|
20865
|
+
now,
|
|
20866
|
+
input2.expiresAt
|
|
20533
20867
|
]);
|
|
20534
20868
|
return { ...input2, createdAt: now, revokedAt: null };
|
|
20535
20869
|
},
|
|
@@ -20537,8 +20871,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20537
20871
|
if (row.limits === null)
|
|
20538
20872
|
throw new Error(`api key ${row.id} has unreadable limits`);
|
|
20539
20873
|
await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
20540
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
20541
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, [
|
|
20874
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
20875
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [
|
|
20542
20876
|
row.id,
|
|
20543
20877
|
row.label,
|
|
20544
20878
|
row.prefix,
|
|
@@ -20547,7 +20881,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
20547
20881
|
JSON.stringify(parseLimitConfig(row.limits)),
|
|
20548
20882
|
row.bodyLoggingOptOut,
|
|
20549
20883
|
row.createdAt,
|
|
20550
|
-
row.revokedAt
|
|
20884
|
+
row.revokedAt,
|
|
20885
|
+
row.expiresAt
|
|
20551
20886
|
]);
|
|
20552
20887
|
},
|
|
20553
20888
|
async setLimits(id, limits) {
|
|
@@ -20562,6 +20897,9 @@ function createKeyRepo(sql, logger2) {
|
|
|
20562
20897
|
id
|
|
20563
20898
|
]);
|
|
20564
20899
|
},
|
|
20900
|
+
async setExpiry(id, expiresAt) {
|
|
20901
|
+
await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
|
|
20902
|
+
},
|
|
20565
20903
|
async revoke(id) {
|
|
20566
20904
|
await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
|
|
20567
20905
|
}
|
|
@@ -20860,229 +21198,6 @@ function isRtkFilterId(value) {
|
|
|
20860
21198
|
return FILTER_IDS.has(value);
|
|
20861
21199
|
}
|
|
20862
21200
|
|
|
20863
|
-
// packages/store/src/sqlite/rollup.ts
|
|
20864
|
-
var HOUR_MS = 3600000;
|
|
20865
|
-
function hourOf(at) {
|
|
20866
|
-
return Math.floor(at / HOUR_MS);
|
|
20867
|
-
}
|
|
20868
|
-
function startOfLocalDay(at) {
|
|
20869
|
-
const day = new Date(at);
|
|
20870
|
-
day.setHours(0, 0, 0, 0);
|
|
20871
|
-
return day.getTime();
|
|
20872
|
-
}
|
|
20873
|
-
var UPSERT = `
|
|
20874
|
-
INSERT INTO usage_daily
|
|
20875
|
-
(day, provider, credential_id, requested_model, resolved_model, api_key_id,
|
|
20876
|
-
requests, errors, input_tokens, output_tokens, cache_read_tokens,
|
|
20877
|
-
cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
|
|
20878
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
20879
|
-
ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
|
|
20880
|
-
DO UPDATE SET
|
|
20881
|
-
requests = requests + excluded.requests,
|
|
20882
|
-
errors = errors + excluded.errors,
|
|
20883
|
-
input_tokens = input_tokens + excluded.input_tokens,
|
|
20884
|
-
output_tokens = output_tokens + excluded.output_tokens,
|
|
20885
|
-
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
20886
|
-
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
20887
|
-
rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
|
|
20888
|
-
rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
|
|
20889
|
-
cost_usd = cost_usd + excluded.cost_usd,
|
|
20890
|
-
duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
|
|
20891
|
-
function keyOf(log) {
|
|
20892
|
-
return [
|
|
20893
|
-
startOfLocalDay(log.at),
|
|
20894
|
-
log.resolvedProvider ?? "",
|
|
20895
|
-
log.credentialId ?? "",
|
|
20896
|
-
log.requestedModel,
|
|
20897
|
-
log.resolvedModel ?? "",
|
|
20898
|
-
log.apiKeyId ?? ""
|
|
20899
|
-
];
|
|
20900
|
-
}
|
|
20901
|
-
function countersOf(log) {
|
|
20902
|
-
return {
|
|
20903
|
-
requests: 1,
|
|
20904
|
-
errors: log.status >= 400 ? 1 : 0,
|
|
20905
|
-
inputTokens: log.inputTokens,
|
|
20906
|
-
outputTokens: log.outputTokens,
|
|
20907
|
-
cacheReadTokens: log.cacheReadTokens,
|
|
20908
|
-
cacheWriteTokens: log.cacheWriteTokens,
|
|
20909
|
-
rtkSavedTokens: log.rtkEstimatedTokensSaved,
|
|
20910
|
-
rtkAppliedRequests: log.rtkApplied ? 1 : 0,
|
|
20911
|
-
costUsd: log.costUsd,
|
|
20912
|
-
durationMsSum: log.durationMs
|
|
20913
|
-
};
|
|
20914
|
-
}
|
|
20915
|
-
function upsert(db, key, c) {
|
|
20916
|
-
db.run(UPSERT, [
|
|
20917
|
-
...key,
|
|
20918
|
-
c.requests,
|
|
20919
|
-
c.errors,
|
|
20920
|
-
c.inputTokens,
|
|
20921
|
-
c.outputTokens,
|
|
20922
|
-
c.cacheReadTokens,
|
|
20923
|
-
c.cacheWriteTokens,
|
|
20924
|
-
c.rtkSavedTokens,
|
|
20925
|
-
c.rtkAppliedRequests,
|
|
20926
|
-
c.costUsd,
|
|
20927
|
-
c.durationMsSum
|
|
20928
|
-
]);
|
|
20929
|
-
}
|
|
20930
|
-
function rollupLog(db, log) {
|
|
20931
|
-
upsert(db, keyOf(log), countersOf(log));
|
|
20932
|
-
}
|
|
20933
|
-
function backfillDaily(db) {
|
|
20934
|
-
const groups = new Map;
|
|
20935
|
-
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
20936
|
-
status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
20937
|
-
duration_ms, cost_usd
|
|
20938
|
-
FROM request_logs`).all()) {
|
|
20939
|
-
const key = [
|
|
20940
|
-
startOfLocalDay(row.at),
|
|
20941
|
-
row.resolved_provider ?? "",
|
|
20942
|
-
row.credential_id ?? "",
|
|
20943
|
-
row.requested_model,
|
|
20944
|
-
row.resolved_model ?? "",
|
|
20945
|
-
row.api_key_id ?? ""
|
|
20946
|
-
];
|
|
20947
|
-
const id = key.join("\x00");
|
|
20948
|
-
const seen = groups.get(id);
|
|
20949
|
-
const counters = seen?.counters ?? {
|
|
20950
|
-
requests: 0,
|
|
20951
|
-
errors: 0,
|
|
20952
|
-
inputTokens: 0,
|
|
20953
|
-
outputTokens: 0,
|
|
20954
|
-
cacheReadTokens: 0,
|
|
20955
|
-
cacheWriteTokens: 0,
|
|
20956
|
-
rtkSavedTokens: 0,
|
|
20957
|
-
rtkAppliedRequests: 0,
|
|
20958
|
-
costUsd: 0,
|
|
20959
|
-
durationMsSum: 0
|
|
20960
|
-
};
|
|
20961
|
-
counters.requests += 1;
|
|
20962
|
-
counters.errors += row.status >= 400 ? 1 : 0;
|
|
20963
|
-
counters.inputTokens += row.input_tokens;
|
|
20964
|
-
counters.outputTokens += row.output_tokens;
|
|
20965
|
-
counters.cacheReadTokens += row.cache_read_tokens;
|
|
20966
|
-
counters.cacheWriteTokens += row.cache_write_tokens;
|
|
20967
|
-
counters.costUsd += row.cost_usd;
|
|
20968
|
-
counters.durationMsSum += row.duration_ms;
|
|
20969
|
-
if (seen === undefined)
|
|
20970
|
-
groups.set(id, { key, counters });
|
|
20971
|
-
}
|
|
20972
|
-
for (const group of groups.values())
|
|
20973
|
-
upsert(db, group.key, group.counters);
|
|
20974
|
-
return groups.size;
|
|
20975
|
-
}
|
|
20976
|
-
var HOURLY_UPSERT = `
|
|
20977
|
-
INSERT INTO usage_rollup
|
|
20978
|
-
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
20979
|
-
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
20980
|
-
VALUES (?,?,?,?,?,?,?,?)
|
|
20981
|
-
ON CONFLICT (api_key_id, hour)
|
|
20982
|
-
DO UPDATE SET
|
|
20983
|
-
requests = requests + excluded.requests,
|
|
20984
|
-
input_tokens = input_tokens + excluded.input_tokens,
|
|
20985
|
-
output_tokens = output_tokens + excluded.output_tokens,
|
|
20986
|
-
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
20987
|
-
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
20988
|
-
cost_usd = cost_usd + excluded.cost_usd`;
|
|
20989
|
-
function rollupHour(db, log) {
|
|
20990
|
-
const apiKeyId = log.apiKeyId;
|
|
20991
|
-
if (apiKeyId === null)
|
|
20992
|
-
return;
|
|
20993
|
-
db.run(HOURLY_UPSERT, [
|
|
20994
|
-
apiKeyId,
|
|
20995
|
-
hourOf(log.at),
|
|
20996
|
-
1,
|
|
20997
|
-
log.inputTokens,
|
|
20998
|
-
log.outputTokens,
|
|
20999
|
-
log.cacheReadTokens,
|
|
21000
|
-
log.cacheWriteTokens,
|
|
21001
|
-
log.costUsd
|
|
21002
|
-
]);
|
|
21003
|
-
}
|
|
21004
|
-
var REBUILD = `
|
|
21005
|
-
INSERT INTO usage_rollup
|
|
21006
|
-
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
21007
|
-
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
21008
|
-
SELECT api_key_id,
|
|
21009
|
-
CAST(at / 3600000 AS INTEGER),
|
|
21010
|
-
COUNT(*),
|
|
21011
|
-
COALESCE(SUM(input_tokens), 0),
|
|
21012
|
-
COALESCE(SUM(output_tokens), 0),
|
|
21013
|
-
COALESCE(SUM(cache_read_tokens), 0),
|
|
21014
|
-
COALESCE(SUM(cache_write_tokens), 0),
|
|
21015
|
-
COALESCE(SUM(cost_usd), 0)
|
|
21016
|
-
FROM request_logs
|
|
21017
|
-
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
21018
|
-
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
21019
|
-
function rebuildRollup(db) {
|
|
21020
|
-
db.run("DELETE FROM usage_rollup");
|
|
21021
|
-
db.run(REBUILD);
|
|
21022
|
-
}
|
|
21023
|
-
function auditRollup(db) {
|
|
21024
|
-
const row = db.query(`WITH truth AS (
|
|
21025
|
-
SELECT api_key_id AS k,
|
|
21026
|
-
CAST(at / 3600000 AS INTEGER) AS h,
|
|
21027
|
-
COUNT(*) AS requests,
|
|
21028
|
-
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
21029
|
-
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
21030
|
-
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
21031
|
-
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
21032
|
-
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
21033
|
-
FROM request_logs
|
|
21034
|
-
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
21035
|
-
GROUP BY k, h
|
|
21036
|
-
)
|
|
21037
|
-
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
21038
|
-
(SELECT COUNT(*)
|
|
21039
|
-
FROM truth
|
|
21040
|
-
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
21041
|
-
WHERE r.api_key_id IS NULL
|
|
21042
|
-
OR r.requests <> truth.requests
|
|
21043
|
-
OR r.input_tokens <> truth.input_tokens
|
|
21044
|
-
OR r.output_tokens <> truth.output_tokens
|
|
21045
|
-
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
21046
|
-
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
21047
|
-
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
21048
|
-
) AS mismatched,
|
|
21049
|
-
(SELECT COUNT(*)
|
|
21050
|
-
FROM usage_rollup r
|
|
21051
|
-
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
21052
|
-
WHERE truth.k IS NULL
|
|
21053
|
-
) AS orphans`).get();
|
|
21054
|
-
const buckets = row?.buckets ?? 0;
|
|
21055
|
-
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
21056
|
-
return { buckets, mismatched, ok: mismatched === 0 };
|
|
21057
|
-
}
|
|
21058
|
-
function backfillRtkUsage(db) {
|
|
21059
|
-
const groups = new Map;
|
|
21060
|
-
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
21061
|
-
rtk_applied, rtk_estimated_tokens_saved
|
|
21062
|
-
FROM request_logs
|
|
21063
|
-
WHERE state = 'done'`).all()) {
|
|
21064
|
-
const key = [
|
|
21065
|
-
startOfLocalDay(row.at),
|
|
21066
|
-
row.resolved_provider ?? "",
|
|
21067
|
-
row.credential_id ?? "",
|
|
21068
|
-
row.requested_model,
|
|
21069
|
-
row.resolved_model ?? "",
|
|
21070
|
-
row.api_key_id ?? ""
|
|
21071
|
-
];
|
|
21072
|
-
const id = key.join("\x00");
|
|
21073
|
-
const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
|
|
21074
|
-
group.saved += row.rtk_estimated_tokens_saved;
|
|
21075
|
-
group.applied += row.rtk_applied === 1 ? 1 : 0;
|
|
21076
|
-
groups.set(id, group);
|
|
21077
|
-
}
|
|
21078
|
-
for (const { key, saved, applied } of groups.values()) {
|
|
21079
|
-
db.run(`UPDATE usage_daily
|
|
21080
|
-
SET rtk_saved_tokens = ?, rtk_applied_requests = ?
|
|
21081
|
-
WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
|
|
21082
|
-
AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
|
|
21083
|
-
}
|
|
21084
|
-
}
|
|
21085
|
-
|
|
21086
21201
|
// packages/store/src/postgres/rollup.ts
|
|
21087
21202
|
var DAILY_UPSERT = `
|
|
21088
21203
|
INSERT INTO usage_daily
|
|
@@ -21102,9 +21217,9 @@ var DAILY_UPSERT = `
|
|
|
21102
21217
|
rtk_applied_requests = usage_daily.rtk_applied_requests + EXCLUDED.rtk_applied_requests,
|
|
21103
21218
|
cost_usd = usage_daily.cost_usd + EXCLUDED.cost_usd,
|
|
21104
21219
|
duration_ms_sum = usage_daily.duration_ms_sum + EXCLUDED.duration_ms_sum`;
|
|
21105
|
-
async function rollupLog2(conn, log) {
|
|
21220
|
+
async function rollupLog2(conn, log, dayOffsetMinutes) {
|
|
21106
21221
|
await conn.unsafe(DAILY_UPSERT, [
|
|
21107
|
-
|
|
21222
|
+
startOfDay(log.at, dayOffsetMinutes),
|
|
21108
21223
|
log.resolvedProvider ?? "",
|
|
21109
21224
|
log.credentialId ?? "",
|
|
21110
21225
|
log.requestedModel,
|
|
@@ -21348,7 +21463,7 @@ var sum = (row) => ({
|
|
|
21348
21463
|
tokens: row === undefined ? 0 : num(row.tokens),
|
|
21349
21464
|
costUsd: row === undefined ? 0 : row.cost_usd
|
|
21350
21465
|
});
|
|
21351
|
-
function createUsageRepo(sql, nodeId) {
|
|
21466
|
+
function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
|
|
21352
21467
|
const complete = async (conn, log) => {
|
|
21353
21468
|
const written = (await conn.unsafe(COMPLETE, values(log, "done", nodeId))).count;
|
|
21354
21469
|
if (written === 0)
|
|
@@ -21356,7 +21471,7 @@ function createUsageRepo(sql, nodeId) {
|
|
|
21356
21471
|
const stored = (await conn.unsafe("SELECT * FROM request_logs WHERE id = $1", [log.id]))[0];
|
|
21357
21472
|
if (stored !== undefined) {
|
|
21358
21473
|
const restored = toLog(stored);
|
|
21359
|
-
await rollupLog2(conn, restored);
|
|
21474
|
+
await rollupLog2(conn, restored, dayOffsetMinutes);
|
|
21360
21475
|
await rollupHour2(conn, restored);
|
|
21361
21476
|
}
|
|
21362
21477
|
};
|
|
@@ -21387,6 +21502,10 @@ function createUsageRepo(sql, nodeId) {
|
|
|
21387
21502
|
]) : await sql.unsafe("SELECT * FROM request_logs WHERE api_key_id = $1 ORDER BY at DESC LIMIT $2", [apiKeyId, limit]);
|
|
21388
21503
|
return rows.map(toLog);
|
|
21389
21504
|
},
|
|
21505
|
+
async lastUsedByCredential(credentialId) {
|
|
21506
|
+
const rows = await sql.unsafe("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = $1", [credentialId]);
|
|
21507
|
+
return numOrNull(rows[0]?.at ?? null);
|
|
21508
|
+
},
|
|
21390
21509
|
async scan(cursor, limit) {
|
|
21391
21510
|
const rows = cursor === null ? await sql.unsafe("SELECT * FROM request_logs ORDER BY at, id LIMIT $1", [
|
|
21392
21511
|
limit
|
|
@@ -21449,7 +21568,7 @@ function createUsageRepo(sql, nodeId) {
|
|
|
21449
21568
|
const daily = grain === "daily";
|
|
21450
21569
|
const key = columnFor(grain, q.groupBy);
|
|
21451
21570
|
const split = q.splitBy === undefined ? null : columnFor(grain, q.splitBy);
|
|
21452
|
-
const since = daily ?
|
|
21571
|
+
const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
|
|
21453
21572
|
const until = q.until ?? Number.MAX_SAFE_INTEGER;
|
|
21454
21573
|
const timeColumn = daily ? "day" : "at";
|
|
21455
21574
|
const scope = q.apiKeyId;
|
|
@@ -21525,6 +21644,7 @@ function redacted(url2) {
|
|
|
21525
21644
|
async function createPostgresStore(opts) {
|
|
21526
21645
|
const logger2 = opts.logger ?? noopLogger;
|
|
21527
21646
|
const nodeId = opts.nodeId ?? crypto.randomUUID();
|
|
21647
|
+
const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
|
|
21528
21648
|
const listeners = new Set;
|
|
21529
21649
|
const emit2 = (change) => {
|
|
21530
21650
|
for (const listener of listeners) {
|
|
@@ -21556,7 +21676,7 @@ async function createPostgresStore(opts) {
|
|
|
21556
21676
|
credentials: createCredentialRepo(sql, opts.encryptionKey, emit2),
|
|
21557
21677
|
config: createConfigRepo(sql, emit2),
|
|
21558
21678
|
keys: createKeyRepo(sql, logger2),
|
|
21559
|
-
usage: createUsageRepo(sql, nodeId),
|
|
21679
|
+
usage: createUsageRepo(sql, nodeId, dayOffsetMinutes),
|
|
21560
21680
|
bodies: createBodyRepo(sql, opts.encryptionKey),
|
|
21561
21681
|
maintenance: createMaintenanceRepo(sql, nodeId),
|
|
21562
21682
|
plugins: createPluginRepo(sql),
|
|
@@ -21793,22 +21913,18 @@ function toHealth2(r) {
|
|
|
21793
21913
|
breakerState: r.breaker_state,
|
|
21794
21914
|
consecutiveFailures: r.consecutive_failures,
|
|
21795
21915
|
openedAt: r.opened_at,
|
|
21796
|
-
rateLimitedUntil: r.rate_limited_until
|
|
21797
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
21798
|
-
lastUsedAt: r.last_used_at
|
|
21916
|
+
rateLimitedUntil: r.rate_limited_until
|
|
21799
21917
|
};
|
|
21800
21918
|
}
|
|
21801
21919
|
var UPSERT_HEALTH2 = `INSERT INTO credential_health
|
|
21802
21920
|
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
21803
|
-
rate_limited_until
|
|
21804
|
-
VALUES (
|
|
21921
|
+
rate_limited_until)
|
|
21922
|
+
VALUES (?,?,?,?,?,?)
|
|
21805
21923
|
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
21806
21924
|
breaker_state = excluded.breaker_state,
|
|
21807
21925
|
consecutive_failures = excluded.consecutive_failures,
|
|
21808
21926
|
opened_at = excluded.opened_at,
|
|
21809
|
-
rate_limited_until = excluded.rate_limited_until
|
|
21810
|
-
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
21811
|
-
last_used_at = excluded.last_used_at`;
|
|
21927
|
+
rate_limited_until = excluded.rate_limited_until`;
|
|
21812
21928
|
function createCredentialRepo2(db, key, emit2 = () => {}) {
|
|
21813
21929
|
const open2 = async (v) => v === null ? null : decrypt(key, v);
|
|
21814
21930
|
const secretsFrom = async (row) => ({
|
|
@@ -21991,22 +22107,13 @@ function createCredentialRepo2(db, key, emit2 = () => {}) {
|
|
|
21991
22107
|
async listHealth() {
|
|
21992
22108
|
return db.query("SELECT * FROM credential_health").all().map(toHealth2);
|
|
21993
22109
|
},
|
|
21994
|
-
async saveHealth(rows) {
|
|
21995
|
-
const stmt = db.prepare(UPSERT_HEALTH2);
|
|
21996
|
-
db.transaction(() => {
|
|
21997
|
-
for (const r of rows) {
|
|
21998
|
-
stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
|
|
21999
|
-
}
|
|
22000
|
-
})();
|
|
22001
|
-
emit2({ type: "healthSaved", rows });
|
|
22002
|
-
},
|
|
22003
22110
|
async updateHealth(credentialId, model, apply) {
|
|
22004
22111
|
const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
|
|
22005
22112
|
const stmt = db.prepare(UPSERT_HEALTH2);
|
|
22006
22113
|
const written = db.transaction(() => {
|
|
22007
22114
|
const row = current.get(credentialId, model);
|
|
22008
22115
|
const next = apply(row === null ? null : toHealth2(row));
|
|
22009
|
-
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil
|
|
22116
|
+
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil);
|
|
22010
22117
|
return next;
|
|
22011
22118
|
})();
|
|
22012
22119
|
emit2({ type: "healthSaved", rows: [written] });
|
|
@@ -22569,6 +22676,63 @@ CREATE TABLE nodes (
|
|
|
22569
22676
|
ALTER TABLE credentials ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;
|
|
22570
22677
|
`;
|
|
22571
22678
|
|
|
22679
|
+
// packages/store/src/sqlite/migrations/013_health_measurements.sql
|
|
22680
|
+
var _013_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before upgrading an install that is still serving.
|
|
22681
|
+
--
|
|
22682
|
+
-- This migration drops two columns that every gateway before it writes on
|
|
22683
|
+
-- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
|
|
22684
|
+
-- SQLite is single-process, so the hazard is the rollback, not the rollout: an
|
|
22685
|
+
-- older gateway started against this database fails every successful request
|
|
22686
|
+
-- after the upstream call has completed and been billed \u2014 content frames out,
|
|
22687
|
+
-- then an aborted connection with no terminal frame. Rolling back the binary
|
|
22688
|
+
-- does not roll back the schema. The only way back is a snapshot taken before
|
|
22689
|
+
-- the upgrade. Deliberate: kept as a drop, not expand/contract, on the
|
|
22690
|
+
-- operator's decision \u2014 see the write-path spec.
|
|
22691
|
+
--
|
|
22692
|
+
-- \`credential_health\` keeps decisions, not measurements.
|
|
22693
|
+
--
|
|
22694
|
+
-- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
|
|
22695
|
+
-- and a success that changes nothing else now writes nothing. Both live in the
|
|
22696
|
+
-- gateway's process-local \`loadRegistry\` instead: latency is a property of
|
|
22697
|
+
-- *this* process's path to the provider, and the round-robin tiebreak reads
|
|
22698
|
+
-- the value the process holds. The console's "last used" reads
|
|
22699
|
+
-- \`request_logs\` through \`usage.lastUsedByCredential\`, served by
|
|
22700
|
+
-- \`idx_request_logs_cred\`.
|
|
22701
|
+
--
|
|
22702
|
+
-- Neither column is indexed or constrained, so \`DROP COLUMN\` is permitted.
|
|
22703
|
+
ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
|
|
22704
|
+
ALTER TABLE credential_health DROP COLUMN last_used_at;
|
|
22705
|
+
`;
|
|
22706
|
+
|
|
22707
|
+
// packages/store/src/sqlite/migrations/014_key_expiry.sql
|
|
22708
|
+
var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an absolute epoch-ms instant after
|
|
22709
|
+
-- which the key is refused, spelled exactly as \`credentials.expires_at\` is.
|
|
22710
|
+
--
|
|
22711
|
+
-- Nullable rather than \`NOT NULL DEFAULT\`, which is the opposite of the choice
|
|
22712
|
+
-- migration 9 made for \`limits\`, and for the opposite reason. There, every
|
|
22713
|
+
-- possible value is a policy and \`{}\` is the one that means "no ceiling", so a
|
|
22714
|
+
-- default gives "unlimited" a single spelling. Here the column is a deadline,
|
|
22715
|
+
-- and no number means "no deadline" \u2014 every finite default is a date on which
|
|
22716
|
+
-- somebody's client stops working. NULL is the only value that says nothing was
|
|
22717
|
+
-- decided, so it is the column's own default and every reader maps it to
|
|
22718
|
+
-- "never expires".
|
|
22719
|
+
--
|
|
22720
|
+
-- No backfill, deliberately, and this is the whole substance of the migration.
|
|
22721
|
+
-- Every key on every existing installation was minted on the promise that it
|
|
22722
|
+
-- works until revoked; an upgrade that attaches an expiry to those rows breaks
|
|
22723
|
+
-- clients on a date the operator never chose, and the failure surfaces days
|
|
22724
|
+
-- later as an authentication error with nothing in it that names this change.
|
|
22725
|
+
-- Adding the column and stopping is what keeps that promise, so the absence of
|
|
22726
|
+
-- an \`UPDATE\` here is load-bearing rather than an omission \u2014
|
|
22727
|
+
-- \`packages/store/test/migrations.test.ts\` pins it.
|
|
22728
|
+
--
|
|
22729
|
+
-- No index. The column is read only after a row has already been found by
|
|
22730
|
+
-- \`hash\` or by \`id\`, both of which are unique, so there is no scan for an index
|
|
22731
|
+
-- to help; and nothing sweeps expired keys, because an expired key stays listed
|
|
22732
|
+
-- exactly as a revoked one does so its usage history keeps its attribution.
|
|
22733
|
+
ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
|
|
22734
|
+
`;
|
|
22735
|
+
|
|
22572
22736
|
// packages/store/src/sqlite/db.ts
|
|
22573
22737
|
var MIGRATIONS2 = [
|
|
22574
22738
|
{ id: 1, sql: _001_init_default2 },
|
|
@@ -22582,9 +22746,16 @@ var MIGRATIONS2 = [
|
|
|
22582
22746
|
{ id: 9, sql: _009_key_limits_default },
|
|
22583
22747
|
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
|
|
22584
22748
|
{ id: 11, sql: _011_plugin_migrations_default },
|
|
22585
|
-
{ id: 12, sql: _012_nodes_default }
|
|
22749
|
+
{ id: 12, sql: _012_nodes_default },
|
|
22750
|
+
{ id: 13, sql: _013_health_measurements_default },
|
|
22751
|
+
{ id: 14, sql: _014_key_expiry_default }
|
|
22586
22752
|
];
|
|
22587
|
-
function
|
|
22753
|
+
function statements(sql) {
|
|
22754
|
+
return sql.split(`
|
|
22755
|
+
`).filter((line) => !line.trimStart().startsWith("--")).join(`
|
|
22756
|
+
`).split(/;[ \t]*(?:\n|$)/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
22757
|
+
}
|
|
22758
|
+
function openDb(path, dayOffsetMinutes = hostDayOffsetMinutes()) {
|
|
22588
22759
|
const db = new Database(path, { create: true });
|
|
22589
22760
|
db.run("PRAGMA journal_mode = WAL");
|
|
22590
22761
|
db.run("PRAGMA synchronous = NORMAL");
|
|
@@ -22596,8 +22767,9 @@ function openDb(path) {
|
|
|
22596
22767
|
if (done.has(m.id))
|
|
22597
22768
|
continue;
|
|
22598
22769
|
db.transaction(() => {
|
|
22599
|
-
|
|
22600
|
-
|
|
22770
|
+
for (const statement of statements(m.sql))
|
|
22771
|
+
db.run(statement);
|
|
22772
|
+
m.after?.(db, dayOffsetMinutes);
|
|
22601
22773
|
db.run("INSERT INTO migrations (id, applied_at) VALUES (?, ?)", [m.id, Date.now()]);
|
|
22602
22774
|
})();
|
|
22603
22775
|
}
|
|
@@ -22632,7 +22804,8 @@ var toKey2 = (r, logger2) => ({
|
|
|
22632
22804
|
limits: parseLimits2(r.id, r.limits, logger2),
|
|
22633
22805
|
bodyLoggingOptOut: r.body_logging_opt_out === 1,
|
|
22634
22806
|
createdAt: r.created_at,
|
|
22635
|
-
revokedAt: r.revoked_at
|
|
22807
|
+
revokedAt: r.revoked_at,
|
|
22808
|
+
expiresAt: r.expires_at
|
|
22636
22809
|
});
|
|
22637
22810
|
function createKeyRepo2(db, logger2 = noopLogger) {
|
|
22638
22811
|
return {
|
|
@@ -22650,8 +22823,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22650
22823
|
async create(input2) {
|
|
22651
22824
|
const now = Date.now();
|
|
22652
22825
|
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
22653
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
22654
|
-
VALUES (?,?,?,?,?,?,?,?,NULL)`, [
|
|
22826
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
22827
|
+
VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
|
|
22655
22828
|
input2.id,
|
|
22656
22829
|
input2.label,
|
|
22657
22830
|
input2.prefix,
|
|
@@ -22659,7 +22832,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22659
22832
|
input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
|
|
22660
22833
|
JSON.stringify(parseLimitConfig(input2.limits)),
|
|
22661
22834
|
input2.bodyLoggingOptOut ? 1 : 0,
|
|
22662
|
-
now
|
|
22835
|
+
now,
|
|
22836
|
+
input2.expiresAt
|
|
22663
22837
|
]);
|
|
22664
22838
|
return { ...input2, createdAt: now, revokedAt: null };
|
|
22665
22839
|
},
|
|
@@ -22667,8 +22841,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22667
22841
|
if (row.limits === null)
|
|
22668
22842
|
throw new Error(`api key ${row.id} has unreadable limits`);
|
|
22669
22843
|
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
22670
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
22671
|
-
VALUES (
|
|
22844
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
22845
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)`, [
|
|
22672
22846
|
row.id,
|
|
22673
22847
|
row.label,
|
|
22674
22848
|
row.prefix,
|
|
@@ -22677,7 +22851,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22677
22851
|
JSON.stringify(parseLimitConfig(row.limits)),
|
|
22678
22852
|
row.bodyLoggingOptOut ? 1 : 0,
|
|
22679
22853
|
row.createdAt,
|
|
22680
|
-
row.revokedAt
|
|
22854
|
+
row.revokedAt,
|
|
22855
|
+
row.expiresAt
|
|
22681
22856
|
]);
|
|
22682
22857
|
},
|
|
22683
22858
|
async setLimits(id, limits) {
|
|
@@ -22692,6 +22867,9 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
22692
22867
|
id
|
|
22693
22868
|
]);
|
|
22694
22869
|
},
|
|
22870
|
+
async setExpiry(id, expiresAt) {
|
|
22871
|
+
db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
|
|
22872
|
+
},
|
|
22695
22873
|
async revoke(id) {
|
|
22696
22874
|
db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
|
|
22697
22875
|
}
|
|
@@ -23014,14 +23192,14 @@ var COMPLETE2 = `INSERT INTO request_logs ${COLUMNS2} VALUES ${PLACEHOLDERS2}
|
|
|
23014
23192
|
rtk_estimated_tokens_saved = excluded.rtk_estimated_tokens_saved,
|
|
23015
23193
|
rtk_filters = excluded.rtk_filters
|
|
23016
23194
|
WHERE request_logs.state = 'pending'`;
|
|
23017
|
-
function createUsageRepo2(db, nodeId) {
|
|
23195
|
+
function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes()) {
|
|
23018
23196
|
const complete = db.transaction((log) => {
|
|
23019
23197
|
if (db.run(COMPLETE2, values2(log, "done", nodeId)).changes === 0)
|
|
23020
23198
|
return;
|
|
23021
23199
|
const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
|
|
23022
23200
|
if (stored !== null) {
|
|
23023
23201
|
const restored = toLog2(stored);
|
|
23024
|
-
rollupLog(db, restored);
|
|
23202
|
+
rollupLog(db, restored, dayOffsetMinutes);
|
|
23025
23203
|
rollupHour(db, restored);
|
|
23026
23204
|
}
|
|
23027
23205
|
});
|
|
@@ -23071,6 +23249,10 @@ function createUsageRepo2(db, nodeId) {
|
|
|
23071
23249
|
}
|
|
23072
23250
|
return db.query("SELECT * FROM request_logs WHERE api_key_id = ? ORDER BY at DESC LIMIT ?").all(apiKeyId, limit).map(toLog2);
|
|
23073
23251
|
},
|
|
23252
|
+
async lastUsedByCredential(credentialId) {
|
|
23253
|
+
const row = db.query("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = ?").get(credentialId);
|
|
23254
|
+
return row?.at ?? null;
|
|
23255
|
+
},
|
|
23074
23256
|
async scan(cursor, limit) {
|
|
23075
23257
|
const rows = cursor === null ? db.query("SELECT * FROM request_logs ORDER BY at, id LIMIT ?").all(limit) : db.query(`SELECT * FROM request_logs
|
|
23076
23258
|
WHERE at > ? OR (at = ? AND id > ?)
|
|
@@ -23132,7 +23314,7 @@ function createUsageRepo2(db, nodeId) {
|
|
|
23132
23314
|
const daily = grain === "daily";
|
|
23133
23315
|
const key = columnFor2(grain, q.groupBy);
|
|
23134
23316
|
const split = q.splitBy === undefined ? null : columnFor2(grain, q.splitBy);
|
|
23135
|
-
const since = daily ?
|
|
23317
|
+
const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
|
|
23136
23318
|
const until = q.until ?? Number.MAX_SAFE_INTEGER;
|
|
23137
23319
|
const timeColumn = daily ? "day" : "at";
|
|
23138
23320
|
const scope = q.apiKeyId;
|
|
@@ -23183,6 +23365,7 @@ function createUsageRepo2(db, nodeId) {
|
|
|
23183
23365
|
async function createStore(opts) {
|
|
23184
23366
|
const logger2 = opts.logger ?? noopLogger;
|
|
23185
23367
|
const nodeId = opts.nodeId ?? crypto.randomUUID();
|
|
23368
|
+
const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
|
|
23186
23369
|
const listeners = new Set;
|
|
23187
23370
|
const emit2 = (change) => {
|
|
23188
23371
|
for (const listener of listeners) {
|
|
@@ -23192,14 +23375,14 @@ async function createStore(opts) {
|
|
|
23192
23375
|
}
|
|
23193
23376
|
};
|
|
23194
23377
|
const open2 = () => {
|
|
23195
|
-
const db = openDb(opts.path);
|
|
23378
|
+
const db = openDb(opts.path, dayOffsetMinutes);
|
|
23196
23379
|
logger2.debug("store opened", { path: opts.path });
|
|
23197
23380
|
return {
|
|
23198
23381
|
db,
|
|
23199
23382
|
credentials: createCredentialRepo2(db, opts.encryptionKey, emit2),
|
|
23200
23383
|
config: createConfigRepo2(db, emit2),
|
|
23201
23384
|
keys: createKeyRepo2(db, logger2),
|
|
23202
|
-
usage: createUsageRepo2(db, nodeId),
|
|
23385
|
+
usage: createUsageRepo2(db, nodeId, dayOffsetMinutes),
|
|
23203
23386
|
bodies: createBodyRepo2(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
23204
23387
|
maintenance: createMaintenanceRepo2(db, nodeId),
|
|
23205
23388
|
plugins: createPluginRepo2(db)
|
|
@@ -23225,7 +23408,6 @@ async function createStore(opts) {
|
|
|
23225
23408
|
updateSecrets: (id, secrets, expiresAt, expectedVersion) => handle.credentials.updateSecrets(id, secrets, expiresAt, expectedVersion),
|
|
23226
23409
|
remove: (id) => handle.credentials.remove(id),
|
|
23227
23410
|
listHealth: () => handle.credentials.listHealth(),
|
|
23228
|
-
saveHealth: (rows) => handle.credentials.saveHealth(rows),
|
|
23229
23411
|
updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
|
|
23230
23412
|
listQuota: () => handle.credentials.listQuota(),
|
|
23231
23413
|
saveQuota: (rows) => handle.credentials.saveQuota(rows),
|
|
@@ -23252,6 +23434,7 @@ async function createStore(opts) {
|
|
|
23252
23434
|
importRow: (row) => handle.keys.importRow(row),
|
|
23253
23435
|
setLimits: (id, limits) => handle.keys.setLimits(id, limits),
|
|
23254
23436
|
setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
|
|
23437
|
+
setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
|
|
23255
23438
|
revoke: (id) => handle.keys.revoke(id)
|
|
23256
23439
|
},
|
|
23257
23440
|
usage: {
|
|
@@ -23260,6 +23443,7 @@ async function createStore(opts) {
|
|
|
23260
23443
|
append: (log) => handle.usage.append(log),
|
|
23261
23444
|
sweepPending: (now) => handle.usage.sweepPending(now),
|
|
23262
23445
|
recent: (limit, apiKeyId) => handle.usage.recent(limit, apiKeyId),
|
|
23446
|
+
lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
|
|
23263
23447
|
scan: (cursor, limit) => handle.usage.scan(cursor, limit),
|
|
23264
23448
|
aggregate: (q) => handle.usage.aggregate(q),
|
|
23265
23449
|
sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
|
|
@@ -23353,7 +23537,7 @@ function createAdminAuth(store, opts) {
|
|
|
23353
23537
|
};
|
|
23354
23538
|
const keyStillValid = async (apiKeyId) => {
|
|
23355
23539
|
const key = await store.keys.get(apiKeyId);
|
|
23356
|
-
return key !== null && key.
|
|
23540
|
+
return key !== null && keyUsable(key, opts.now());
|
|
23357
23541
|
};
|
|
23358
23542
|
const applyPassword = async (password) => {
|
|
23359
23543
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
@@ -23407,7 +23591,7 @@ function createAdminAuth(store, opts) {
|
|
|
23407
23591
|
},
|
|
23408
23592
|
async loginClient(rawKey) {
|
|
23409
23593
|
const key = await store.keys.findByHash(await hashApiKey(rawKey));
|
|
23410
|
-
if (key === null || key.
|
|
23594
|
+
if (key === null || !keyUsable(key, opts.now()))
|
|
23411
23595
|
return null;
|
|
23412
23596
|
return issue2({ kind: "client", apiKeyId: key.id });
|
|
23413
23597
|
},
|
|
@@ -24138,6 +24322,7 @@ var PROVIDER_IDS = Object.keys(PROVIDER_DESCRIPTORS);
|
|
|
24138
24322
|
var MIN_KEY_LENGTH = 16;
|
|
24139
24323
|
var TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
24140
24324
|
var DECIMAL_INTEGER = /^\d+$/;
|
|
24325
|
+
var SIGNED_INTEGER = /^-?\d+$/;
|
|
24141
24326
|
function optionalText(value, fallback) {
|
|
24142
24327
|
return value?.trim() || fallback;
|
|
24143
24328
|
}
|
|
@@ -24168,6 +24353,11 @@ function loadConfig(env) {
|
|
|
24168
24353
|
} else if (databaseUrl !== undefined || redisUrl !== undefined) {
|
|
24169
24354
|
throw new Error("OMNI_DATABASE_URL and OMNI_REDIS_URL are cluster-mode settings; set OMNI_CLUSTER_MODE=true or unset them");
|
|
24170
24355
|
}
|
|
24356
|
+
const rawDayOffset = env.OMNI_DAY_OFFSET_MINUTES?.trim();
|
|
24357
|
+
const parsedDayOffset = rawDayOffset === undefined ? NaN : Number(rawDayOffset);
|
|
24358
|
+
const validDayOffset = rawDayOffset !== undefined && SIGNED_INTEGER.test(rawDayOffset) && Number.isInteger(parsedDayOffset) && parsedDayOffset >= -1440 && parsedDayOffset <= 1440;
|
|
24359
|
+
const dayOffsetMinutes = validDayOffset ? parsedDayOffset : hostDayOffsetMinutes();
|
|
24360
|
+
const dayOffsetFallbackFrom = rawDayOffset !== undefined && !validDayOffset ? rawDayOffset : null;
|
|
24171
24361
|
const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
|
|
24172
24362
|
const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
|
|
24173
24363
|
const logLevel = parseLogLevel(rawLogLevel);
|
|
@@ -24198,6 +24388,8 @@ function loadConfig(env) {
|
|
|
24198
24388
|
clusterMode,
|
|
24199
24389
|
databaseUrl: clusterMode ? databaseUrl ?? null : null,
|
|
24200
24390
|
redisUrl: clusterMode ? redisUrl ?? null : null,
|
|
24391
|
+
dayOffsetMinutes,
|
|
24392
|
+
dayOffsetFallbackFrom,
|
|
24201
24393
|
metricsToken,
|
|
24202
24394
|
metricsMaxSeries,
|
|
24203
24395
|
otlpEndpoint,
|
|
@@ -30874,16 +31066,19 @@ var modelSchema = exports_external.object({
|
|
|
30874
31066
|
isAlias: exports_external.boolean(),
|
|
30875
31067
|
targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
|
|
30876
31068
|
});
|
|
31069
|
+
var MAX_INSTANT = 8640000000000000;
|
|
30877
31070
|
var keyCreateSchema = exports_external.object({
|
|
30878
31071
|
label: exports_external.string().min(1).default("api key"),
|
|
30879
31072
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
|
|
30880
31073
|
limits: limitConfigSchema.default({}),
|
|
30881
|
-
bodyLoggingOptOut: exports_external.boolean().default(false)
|
|
31074
|
+
bodyLoggingOptOut: exports_external.boolean().default(false),
|
|
31075
|
+
expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable().default(null)
|
|
30882
31076
|
}).strict();
|
|
30883
31077
|
var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
|
|
30884
31078
|
var keyModelsSchema = exports_external.object({
|
|
30885
31079
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
|
|
30886
31080
|
}).strict();
|
|
31081
|
+
var keyExpirySchema = exports_external.object({ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable() }).strict();
|
|
30887
31082
|
var retentionSchema = exports_external.object({
|
|
30888
31083
|
keepLatest: exports_external.number().int().min(1).max(100),
|
|
30889
31084
|
maxAgeDays: exports_external.number().int().min(1).max(3650)
|
|
@@ -31181,10 +31376,11 @@ function eligible(input2) {
|
|
|
31181
31376
|
drop("disabled");
|
|
31182
31377
|
continue;
|
|
31183
31378
|
}
|
|
31184
|
-
if (credential
|
|
31379
|
+
if (credentialExpired(credential, now)) {
|
|
31185
31380
|
drop("expired");
|
|
31186
31381
|
continue;
|
|
31187
31382
|
}
|
|
31383
|
+
let probe = false;
|
|
31188
31384
|
const h = snapshot.health.get(healthKey(credential.id, target.model));
|
|
31189
31385
|
if (h !== undefined) {
|
|
31190
31386
|
if (h.rateLimitedUntil !== null && h.rateLimitedUntil > now) {
|
|
@@ -31197,6 +31393,9 @@ function eligible(input2) {
|
|
|
31197
31393
|
drop("breaker:open");
|
|
31198
31394
|
continue;
|
|
31199
31395
|
}
|
|
31396
|
+
probe = true;
|
|
31397
|
+
} else if (h.breakerState === "halfOpen") {
|
|
31398
|
+
probe = true;
|
|
31200
31399
|
}
|
|
31201
31400
|
}
|
|
31202
31401
|
const spent = (snapshot.quota.get(credential.id) ?? []).find((w) => w.limit !== null && w.used >= w.limit && (w.resetsAt === null || w.resetsAt > now));
|
|
@@ -31204,7 +31403,7 @@ function eligible(input2) {
|
|
|
31204
31403
|
drop(`quota:${spent.windowType}`);
|
|
31205
31404
|
continue;
|
|
31206
31405
|
}
|
|
31207
|
-
pairs.push({ credential, target });
|
|
31406
|
+
pairs.push({ credential, target, probe });
|
|
31208
31407
|
}
|
|
31209
31408
|
if (target.credentialId === undefined && providerSeen && !servedAny) {
|
|
31210
31409
|
excluded.push({
|
|
@@ -31281,7 +31480,7 @@ function healthScore(h) {
|
|
|
31281
31480
|
return h?.breakerState === "open" || h?.breakerState === "halfOpen" ? base * 0.5 : base;
|
|
31282
31481
|
}
|
|
31283
31482
|
function score(pairs, input2) {
|
|
31284
|
-
const { snapshot, now, load } = input2;
|
|
31483
|
+
const { snapshot, now, load, recent } = input2;
|
|
31285
31484
|
const w = snapshot.settings.weights;
|
|
31286
31485
|
const tiers = pairs.map((p) => p.target.tier);
|
|
31287
31486
|
const minTier = Math.min(...tiers);
|
|
@@ -31289,9 +31488,10 @@ function score(pairs, input2) {
|
|
|
31289
31488
|
const tokens2 = estimateTokens(input2.request);
|
|
31290
31489
|
const costs = pairs.map((p) => requestCost(p.target, tokens2));
|
|
31291
31490
|
const bestCost = bestPositive(costs);
|
|
31491
|
+
const ewmaOf = (key) => recent?.get(key)?.ewmaTtftMs ?? null;
|
|
31292
31492
|
const bestLatency = bestPositive(pairs.flatMap((p) => {
|
|
31293
|
-
const
|
|
31294
|
-
return
|
|
31493
|
+
const ewma = ewmaOf(healthKey(p.credential.id, p.target.model));
|
|
31494
|
+
return ewma === null ? [] : [ewma];
|
|
31295
31495
|
}));
|
|
31296
31496
|
return pairs.map((pair, i) => {
|
|
31297
31497
|
const key = healthKey(pair.credential.id, pair.target.model);
|
|
@@ -31303,12 +31503,14 @@ function score(pairs, input2) {
|
|
|
31303
31503
|
const quota = quotaHeadroom(pair.credential, snapshot.quota.get(pair.credential.id) ?? [], now, snapshot.settings.quotaPollIntervalMs);
|
|
31304
31504
|
const ownCost = costs[i];
|
|
31305
31505
|
const cost = ownCost <= 0 || bestCost === null ? UNKNOWN : ratio(ownCost, bestCost);
|
|
31306
|
-
const
|
|
31506
|
+
const ewma = ewmaOf(key);
|
|
31507
|
+
const latency = ewma === null || ewma <= 0 || bestLatency === null ? UNKNOWN : ratio(ewma, bestLatency);
|
|
31307
31508
|
const reasons = { tier, health, quota, cost, latency, load: loadTerm };
|
|
31308
31509
|
const base = tier * w.tier + health * w.health + quota * w.quota + cost * w.cost + latency * w.latency + loadTerm * w.load;
|
|
31309
31510
|
return {
|
|
31310
31511
|
credential: pair.credential,
|
|
31311
31512
|
target: pair.target,
|
|
31513
|
+
probe: pair.probe,
|
|
31312
31514
|
score: base * pair.credential.weight * pair.target.weight,
|
|
31313
31515
|
reasons
|
|
31314
31516
|
};
|
|
@@ -31348,8 +31550,8 @@ function rank(input2) {
|
|
|
31348
31550
|
break;
|
|
31349
31551
|
case "roundRobin": {
|
|
31350
31552
|
const idle = (c) => {
|
|
31351
|
-
const
|
|
31352
|
-
return
|
|
31553
|
+
const at = input2.recent?.get(healthKey(c.credential.id, c.target.model))?.lastReleasedAt;
|
|
31554
|
+
return at === undefined ? Number.POSITIVE_INFINITY : input2.now - at;
|
|
31353
31555
|
};
|
|
31354
31556
|
const spent = (c) => headroom(c) < QUOTA_FLOOR ? 1 : 0;
|
|
31355
31557
|
scored.sort((a, b) => spent(a) - spent(b) || inflight(a) - inflight(b) || idle(b) - idle(a));
|
|
@@ -31521,16 +31723,23 @@ async function refreshCredential(deps, id) {
|
|
|
31521
31723
|
return getCredential(deps.store, id);
|
|
31522
31724
|
}
|
|
31523
31725
|
async function credentialHealth(deps) {
|
|
31524
|
-
const [health, quota, settings] = await Promise.all([
|
|
31726
|
+
const [health, quota, settings, credentials] = await Promise.all([
|
|
31525
31727
|
deps.store.credentials.listHealth(),
|
|
31526
31728
|
deps.store.credentials.listQuota(),
|
|
31527
|
-
deps.store.config.getSettings()
|
|
31729
|
+
deps.store.config.getSettings(),
|
|
31730
|
+
deps.store.credentials.list()
|
|
31528
31731
|
]);
|
|
31732
|
+
const lastUsed = {};
|
|
31733
|
+
for (const c of credentials) {
|
|
31734
|
+
const at = await deps.store.usage.lastUsedByCredential(c.id);
|
|
31735
|
+
if (at !== null)
|
|
31736
|
+
lastUsed[c.id] = at;
|
|
31737
|
+
}
|
|
31529
31738
|
const burn = burnEstimates(quota, {
|
|
31530
31739
|
now: deps.now(),
|
|
31531
31740
|
pollIntervalMs: settings.quotaPollIntervalMs
|
|
31532
31741
|
});
|
|
31533
|
-
return { health, quota, burn };
|
|
31742
|
+
return { health, quota, burn, lastUsed };
|
|
31534
31743
|
}
|
|
31535
31744
|
async function credentialStatus(store, options) {
|
|
31536
31745
|
const [credentials, quota, adminConfigured] = await Promise.all([
|
|
@@ -31644,14 +31853,14 @@ function listSnapshots(deps) {
|
|
|
31644
31853
|
}
|
|
31645
31854
|
return snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
31646
31855
|
}
|
|
31647
|
-
var
|
|
31856
|
+
var DAY_MS2 = 86400000;
|
|
31648
31857
|
var SNAPSHOT_HEADROOM = 1.1;
|
|
31649
31858
|
function retentionOf(settings) {
|
|
31650
31859
|
return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
|
|
31651
31860
|
}
|
|
31652
31861
|
function applyRetention(deps, policy, now) {
|
|
31653
31862
|
const dir = snapshotsDir(deps);
|
|
31654
|
-
const cutoff = now - policy.maxAgeDays *
|
|
31863
|
+
const cutoff = now - policy.maxAgeDays * DAY_MS2;
|
|
31655
31864
|
const snapshots = listSnapshots(deps);
|
|
31656
31865
|
const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
|
|
31657
31866
|
let removed = 0;
|
|
@@ -31971,7 +32180,8 @@ async function toSummary(store, key, now) {
|
|
|
31971
32180
|
limitUsage: await readLimitUsage(store, key.id, key.limits, now),
|
|
31972
32181
|
bodyLoggingOptOut: key.bodyLoggingOptOut,
|
|
31973
32182
|
createdAt: key.createdAt,
|
|
31974
|
-
revokedAt: key.revokedAt
|
|
32183
|
+
revokedAt: key.revokedAt,
|
|
32184
|
+
expiresAt: key.expiresAt
|
|
31975
32185
|
};
|
|
31976
32186
|
}
|
|
31977
32187
|
async function listKeys(store, now = Date.now()) {
|
|
@@ -31988,7 +32198,8 @@ async function createKey(store, input2) {
|
|
|
31988
32198
|
hash: await hashApiKey(raw),
|
|
31989
32199
|
modelAllowlist: body2.modelAllowlist,
|
|
31990
32200
|
limits: body2.limits,
|
|
31991
|
-
bodyLoggingOptOut: body2.bodyLoggingOptOut
|
|
32201
|
+
bodyLoggingOptOut: body2.bodyLoggingOptOut,
|
|
32202
|
+
expiresAt: body2.expiresAt
|
|
31992
32203
|
});
|
|
31993
32204
|
return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
|
|
31994
32205
|
}
|
|
@@ -32008,6 +32219,14 @@ async function setKeyModels(store, id, input2, now = Date.now()) {
|
|
|
32008
32219
|
await store.keys.setModelAllowlist(id, body2.modelAllowlist);
|
|
32009
32220
|
return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
|
|
32010
32221
|
}
|
|
32222
|
+
async function setKeyExpiry(store, id, input2, now = Date.now()) {
|
|
32223
|
+
const body2 = parseOrThrow(keyExpirySchema, input2);
|
|
32224
|
+
const key = await store.keys.get(id);
|
|
32225
|
+
if (key === null)
|
|
32226
|
+
throw new GatewayError("BAD_REQUEST", "no such api key");
|
|
32227
|
+
await store.keys.setExpiry(id, body2.expiresAt);
|
|
32228
|
+
return toSummary(store, { ...key, expiresAt: body2.expiresAt }, now);
|
|
32229
|
+
}
|
|
32011
32230
|
async function revokeKey(store, id) {
|
|
32012
32231
|
await store.keys.revoke(id);
|
|
32013
32232
|
}
|
|
@@ -33476,12 +33695,18 @@ function readsNothing(scope) {
|
|
|
33476
33695
|
return scope.kind === "none" || scope.kind === "key" && scope.apiKeyId === "";
|
|
33477
33696
|
}
|
|
33478
33697
|
// packages/control/src/quota/history.ts
|
|
33479
|
-
var
|
|
33698
|
+
var DAY_MS3 = 24 * 60 * 60 * 1000;
|
|
33480
33699
|
var HOUR_MS3 = 3600000;
|
|
33700
|
+
var MAX_SAMPLES = 50000;
|
|
33701
|
+
var SAMPLE_QUERY_LIMIT = MAX_SAMPLES + 1;
|
|
33702
|
+
function pageSamples(samples) {
|
|
33703
|
+
const truncated = samples.length > MAX_SAMPLES;
|
|
33704
|
+
return { page: truncated ? samples.slice(0, MAX_SAMPLES) : samples, truncated };
|
|
33705
|
+
}
|
|
33481
33706
|
async function retainedSpan(deps, input2, maxSpanMs) {
|
|
33482
33707
|
const now = deps.now();
|
|
33483
33708
|
const settings = await deps.store.config.getSettings();
|
|
33484
|
-
const retained = now - settings.logRetentionDays *
|
|
33709
|
+
const retained = now - settings.logRetentionDays * DAY_MS3;
|
|
33485
33710
|
const oldest = maxSpanMs === undefined ? retained : Math.max(retained, now - maxSpanMs);
|
|
33486
33711
|
return {
|
|
33487
33712
|
since: Math.max(optionalNumber(input2.since, oldest), oldest),
|
|
@@ -33524,12 +33749,14 @@ async function quotaHistory(deps, input2) {
|
|
|
33524
33749
|
deps.store.credentials.listQuotaSamples({
|
|
33525
33750
|
since,
|
|
33526
33751
|
until,
|
|
33752
|
+
limit: SAMPLE_QUERY_LIMIT,
|
|
33527
33753
|
...credentialId === undefined ? {} : { credentialId }
|
|
33528
33754
|
}),
|
|
33529
33755
|
deps.store.credentials.listQuota()
|
|
33530
33756
|
]);
|
|
33531
33757
|
const scoped = credentialId === undefined ? quota : quota.filter((w) => w.credentialId === credentialId);
|
|
33532
|
-
|
|
33758
|
+
const { page, truncated } = pageSamples(samples);
|
|
33759
|
+
return { samples: page, truncated, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
|
|
33533
33760
|
}
|
|
33534
33761
|
|
|
33535
33762
|
// packages/control/src/quota/clientHistory.ts
|
|
@@ -33924,7 +34151,11 @@ function createContext(parsed2, options = {}) {
|
|
|
33924
34151
|
const current = config2;
|
|
33925
34152
|
if (current === null)
|
|
33926
34153
|
throw new CliError(configError ?? "invalid configuration");
|
|
33927
|
-
opened = deriveKey(current.encryptionKey).then((key) => createStore({
|
|
34154
|
+
opened = deriveKey(current.encryptionKey).then((key) => createStore({
|
|
34155
|
+
path: databasePath,
|
|
34156
|
+
encryptionKey: key,
|
|
34157
|
+
dayOffsetMinutes: current.dayOffsetMinutes
|
|
34158
|
+
})).then((created) => {
|
|
33928
34159
|
store = created;
|
|
33929
34160
|
return created;
|
|
33930
34161
|
});
|
|
@@ -34575,12 +34806,13 @@ var console_ = {
|
|
|
34575
34806
|
};
|
|
34576
34807
|
|
|
34577
34808
|
// apps/cli/src/commands/credentials.ts
|
|
34578
|
-
function condition(credential) {
|
|
34809
|
+
function condition(credential, now) {
|
|
34579
34810
|
if (!credential.enabled)
|
|
34580
34811
|
return { ok: false, text: credential.disabledReason ?? "disabled" };
|
|
34581
|
-
if (credential
|
|
34582
|
-
return
|
|
34583
|
-
|
|
34812
|
+
if (credentialExpired(credential, now))
|
|
34813
|
+
return { ok: false, text: "expired" };
|
|
34814
|
+
if (credentialPastExpiry(credential, now))
|
|
34815
|
+
return { ok: true, text: "expired (refreshable)" };
|
|
34584
34816
|
return { ok: true, text: "enabled" };
|
|
34585
34817
|
}
|
|
34586
34818
|
var credentialsList = {
|
|
@@ -34601,7 +34833,7 @@ var credentialsList = {
|
|
|
34601
34833
|
{ header: "STATE" },
|
|
34602
34834
|
{ header: "EXPIRES" }
|
|
34603
34835
|
], credentials.map((c) => {
|
|
34604
|
-
const status2 = condition(c);
|
|
34836
|
+
const status2 = condition(c, ctx.now());
|
|
34605
34837
|
return [
|
|
34606
34838
|
c.id,
|
|
34607
34839
|
provider(ctx, c.provider),
|
|
@@ -34625,7 +34857,7 @@ var credentialsShow = {
|
|
|
34625
34857
|
async run(args, { ctx, writer }) {
|
|
34626
34858
|
const id = requirePositional(args, 0, "credential id");
|
|
34627
34859
|
const credential = await findCredential(ctx, id);
|
|
34628
|
-
const status2 = condition(credential);
|
|
34860
|
+
const status2 = condition(credential, ctx.now());
|
|
34629
34861
|
const data = {
|
|
34630
34862
|
id: credential.id,
|
|
34631
34863
|
provider: credential.provider,
|
|
@@ -34775,17 +35007,17 @@ var credentialsAddKey = {
|
|
|
34775
35007
|
};
|
|
34776
35008
|
var credentialsHealth = {
|
|
34777
35009
|
usage: "credentials health",
|
|
34778
|
-
summary: "Show breaker state
|
|
35010
|
+
summary: "Show breaker state per credential and model",
|
|
34779
35011
|
options: { all: { type: "boolean" } },
|
|
34780
35012
|
async run(args, { ctx, writer }) {
|
|
34781
35013
|
const store = await ctx.store();
|
|
34782
|
-
const [{ health: rows }, credentials] = await Promise.all([
|
|
35014
|
+
const [{ health: rows, lastUsed }, credentials] = await Promise.all([
|
|
34783
35015
|
credentialHealth({ store, now: ctx.now }),
|
|
34784
35016
|
listCredentials(store)
|
|
34785
35017
|
]);
|
|
34786
35018
|
const labels = new Map(credentials.map((c) => [c.id, c.label]));
|
|
34787
35019
|
const shown = boolFlag(args.values, "all") ? rows : rows.filter((r) => r.breakerState !== "closed" || r.consecutiveFailures > 0);
|
|
34788
|
-
emit(ctx, writer, { health: rows }, () => {
|
|
35020
|
+
emit(ctx, writer, { health: rows, lastUsed }, () => {
|
|
34789
35021
|
if (shown.length === 0)
|
|
34790
35022
|
return "every credential is healthy";
|
|
34791
35023
|
return table([
|
|
@@ -34793,15 +35025,13 @@ var credentialsHealth = {
|
|
|
34793
35025
|
{ header: "MODEL" },
|
|
34794
35026
|
{ header: "BREAKER" },
|
|
34795
35027
|
{ header: "FAILS", align: "right" },
|
|
34796
|
-
{ header: "TTFT", align: "right" },
|
|
34797
35028
|
{ header: "LAST USED" }
|
|
34798
35029
|
], shown.map((row) => [
|
|
34799
35030
|
labels.get(row.credentialId) ?? row.credentialId,
|
|
34800
35031
|
row.model,
|
|
34801
35032
|
state(ctx, row.breakerState === "closed", row.breakerState),
|
|
34802
35033
|
String(row.consecutiveFailures),
|
|
34803
|
-
|
|
34804
|
-
paint(ctx, "dim", formatTime(row.lastUsedAt))
|
|
35034
|
+
paint(ctx, "dim", formatTime(lastUsed[row.credentialId] ?? null))
|
|
34805
35035
|
]));
|
|
34806
35036
|
});
|
|
34807
35037
|
}
|
|
@@ -34842,7 +35072,8 @@ var dbMigrate = {
|
|
|
34842
35072
|
const source = await ctx.store();
|
|
34843
35073
|
const target = await createPostgresStore({
|
|
34844
35074
|
url: to,
|
|
34845
|
-
encryptionKey: await deriveKey(ctx.config().encryptionKey)
|
|
35075
|
+
encryptionKey: await deriveKey(ctx.config().encryptionKey),
|
|
35076
|
+
dayOffsetMinutes: ctx.config().dayOffsetMinutes
|
|
34846
35077
|
});
|
|
34847
35078
|
try {
|
|
34848
35079
|
const report = await copyStore(source, target);
|
|
@@ -35148,11 +35379,24 @@ function summarizeLimits(limits) {
|
|
|
35148
35379
|
}
|
|
35149
35380
|
return parts.length === 0 ? "\u2014" : parts.join(" ");
|
|
35150
35381
|
}
|
|
35382
|
+
function parseWhen(flag, raw) {
|
|
35383
|
+
const at = Date.parse(raw);
|
|
35384
|
+
if (!Number.isFinite(at)) {
|
|
35385
|
+
throw new UsageError(`${flag} must be a date, e.g. 2030-01-01, got "${raw}"`);
|
|
35386
|
+
}
|
|
35387
|
+
return at;
|
|
35388
|
+
}
|
|
35389
|
+
function keyState(key, now) {
|
|
35390
|
+
if (key.revokedAt !== null)
|
|
35391
|
+
return "revoked";
|
|
35392
|
+
return keyUsable(key, now) ? "active" : "expired";
|
|
35393
|
+
}
|
|
35151
35394
|
var keysList = {
|
|
35152
35395
|
usage: "keys list",
|
|
35153
35396
|
summary: "List gateway API keys",
|
|
35154
35397
|
async run(_args, { ctx, writer }) {
|
|
35155
35398
|
const keys = await listKeys(await ctx.store());
|
|
35399
|
+
const now = Date.now();
|
|
35156
35400
|
emit(ctx, writer, { keys }, () => {
|
|
35157
35401
|
if (keys.length === 0)
|
|
35158
35402
|
return "no api keys; create one with: omni keys create";
|
|
@@ -35164,6 +35408,7 @@ var keysList = {
|
|
|
35164
35408
|
{ header: "LIMITS" },
|
|
35165
35409
|
{ header: "BODY CAPTURE" },
|
|
35166
35410
|
{ header: "STATE" },
|
|
35411
|
+
{ header: "EXPIRES" },
|
|
35167
35412
|
{ header: "CREATED" }
|
|
35168
35413
|
], keys.map((key) => [
|
|
35169
35414
|
key.id,
|
|
@@ -35172,28 +35417,32 @@ var keysList = {
|
|
|
35172
35417
|
key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
|
|
35173
35418
|
key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
|
|
35174
35419
|
key.bodyLoggingOptOut ? "no bodies" : "\u2014",
|
|
35175
|
-
state(ctx,
|
|
35420
|
+
((verdict) => state(ctx, verdict === "active", verdict))(keyState(key, now)),
|
|
35421
|
+
formatTime(key.expiresAt),
|
|
35176
35422
|
formatTime(key.createdAt)
|
|
35177
35423
|
]));
|
|
35178
35424
|
});
|
|
35179
35425
|
}
|
|
35180
35426
|
};
|
|
35181
35427
|
var keysCreate = {
|
|
35182
|
-
usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--no-bodies]",
|
|
35428
|
+
usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--expires <when>] [--no-bodies]",
|
|
35183
35429
|
summary: "Mint a gateway API key, printed once",
|
|
35184
35430
|
options: {
|
|
35185
35431
|
label: { type: "string" },
|
|
35186
35432
|
allow: { type: "string", multiple: true },
|
|
35187
35433
|
limit: { type: "string", multiple: true },
|
|
35434
|
+
expires: { type: "string" },
|
|
35188
35435
|
"no-bodies": { type: "boolean" }
|
|
35189
35436
|
},
|
|
35190
35437
|
async run(args, { ctx, writer }) {
|
|
35191
35438
|
const allow = listFlag(args.values, "allow");
|
|
35439
|
+
const expires = stringFlag(args.values, "expires");
|
|
35192
35440
|
const created = await createKey(await ctx.store(), {
|
|
35193
35441
|
...stringFlag(args.values, "label") === undefined ? {} : { label: stringFlag(args.values, "label") },
|
|
35194
35442
|
modelAllowlist: allow ?? null,
|
|
35195
35443
|
limits: parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit"),
|
|
35196
|
-
bodyLoggingOptOut: boolFlag(args.values, "no-bodies")
|
|
35444
|
+
bodyLoggingOptOut: boolFlag(args.values, "no-bodies"),
|
|
35445
|
+
expiresAt: expires === undefined ? null : parseWhen("--expires", expires)
|
|
35197
35446
|
});
|
|
35198
35447
|
emit(ctx, writer, created, () => {
|
|
35199
35448
|
note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
|
|
@@ -35260,6 +35509,44 @@ ${paint(ctx, "dim", "usage is counted from completed requests still inside each
|
|
|
35260
35509
|
});
|
|
35261
35510
|
}
|
|
35262
35511
|
};
|
|
35512
|
+
var keysExpiry = {
|
|
35513
|
+
usage: "keys expiry <id> [--at <when>] [--never]",
|
|
35514
|
+
summary: "Show or replace one key's expiry",
|
|
35515
|
+
options: {
|
|
35516
|
+
at: { type: "string" },
|
|
35517
|
+
never: { type: "boolean" }
|
|
35518
|
+
},
|
|
35519
|
+
async run(args, { ctx, writer }) {
|
|
35520
|
+
const id = requirePositional(args, 0, "key id");
|
|
35521
|
+
const at = stringFlag(args.values, "at");
|
|
35522
|
+
const never2 = boolFlag(args.values, "never");
|
|
35523
|
+
const store = await ctx.store();
|
|
35524
|
+
const existing = (await listKeys(store)).find((entry) => entry.id === id);
|
|
35525
|
+
if (existing === undefined)
|
|
35526
|
+
throw new CliError(`no api key "${id}"`);
|
|
35527
|
+
let key = existing;
|
|
35528
|
+
if (never2 || at !== undefined) {
|
|
35529
|
+
if (never2 && at !== undefined) {
|
|
35530
|
+
throw new UsageError("--at and --never cannot be combined");
|
|
35531
|
+
}
|
|
35532
|
+
const next = never2 ? null : parseWhen("--at", at);
|
|
35533
|
+
key = await setKeyExpiry(store, id, { expiresAt: next });
|
|
35534
|
+
}
|
|
35535
|
+
emit(ctx, writer, key, () => {
|
|
35536
|
+
const head = fields([
|
|
35537
|
+
["id", key.id],
|
|
35538
|
+
["label", key.label],
|
|
35539
|
+
["prefix", `${key.prefix}\u2026`]
|
|
35540
|
+
]);
|
|
35541
|
+
if (key.expiresAt === null)
|
|
35542
|
+
return `${head}
|
|
35543
|
+
expires: never`;
|
|
35544
|
+
const verdict = keyState(key, Date.now());
|
|
35545
|
+
return `${head}
|
|
35546
|
+
expires: ${formatTime(key.expiresAt)}${verdict === "expired" ? ` ${paint(ctx, "red", "(expired)")}` : ""}`;
|
|
35547
|
+
});
|
|
35548
|
+
}
|
|
35549
|
+
};
|
|
35263
35550
|
var keysRevoke = {
|
|
35264
35551
|
usage: "keys revoke <id>",
|
|
35265
35552
|
summary: "Revoke a gateway API key, keeping its usage history",
|
|
@@ -35755,7 +36042,7 @@ function createServiceDeps(input2) {
|
|
|
35755
36042
|
}
|
|
35756
36043
|
|
|
35757
36044
|
// apps/cli/src/version.ts
|
|
35758
|
-
var VERSION = "0.
|
|
36045
|
+
var VERSION = "0.11.1";
|
|
35759
36046
|
|
|
35760
36047
|
// apps/cli/src/commands/service.ts
|
|
35761
36048
|
function gatewayArgv(root) {
|
|
@@ -36355,7 +36642,7 @@ var logs = {
|
|
|
36355
36642
|
return;
|
|
36356
36643
|
let seen = first[0]?.at ?? 0;
|
|
36357
36644
|
for (;; ) {
|
|
36358
|
-
await Bun.sleep(
|
|
36645
|
+
await Bun.sleep(FOLLOW_INTERVAL_MS);
|
|
36359
36646
|
const next = (await recentLogs(store, limit)).filter((row) => row.at > seen);
|
|
36360
36647
|
if (next.length === 0)
|
|
36361
36648
|
continue;
|
|
@@ -36399,6 +36686,7 @@ var COMMANDS = {
|
|
|
36399
36686
|
"keys create": keysCreate,
|
|
36400
36687
|
"keys limits": keysLimits,
|
|
36401
36688
|
"keys models": keysModels,
|
|
36689
|
+
"keys expiry": keysExpiry,
|
|
36402
36690
|
"keys revoke": keysRevoke,
|
|
36403
36691
|
"plugin list": pluginList,
|
|
36404
36692
|
"plugin verify": pluginVerify,
|