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/gateway.js
CHANGED
|
@@ -24443,13 +24443,13 @@ function prepareArtifact(input2) {
|
|
|
24443
24443
|
error: error61.value
|
|
24444
24444
|
};
|
|
24445
24445
|
const json2 = JSON.stringify(bounded);
|
|
24446
|
-
const size =
|
|
24446
|
+
const size = Buffer.byteLength(json2);
|
|
24447
24447
|
if (size <= MAX_ARTIFACT_BYTES)
|
|
24448
24448
|
return { artifact: bounded, json: json2 };
|
|
24449
24449
|
const marker = omission(size);
|
|
24450
24450
|
const omitted = omitBodies(bounded, marker);
|
|
24451
24451
|
const omittedJson = JSON.stringify(omitted);
|
|
24452
|
-
if (
|
|
24452
|
+
if (Buffer.byteLength(omittedJson) <= MAX_ARTIFACT_BYTES) {
|
|
24453
24453
|
return { artifact: omitted, json: omittedJson };
|
|
24454
24454
|
}
|
|
24455
24455
|
const stripped = { ...omitted, error: marker };
|
|
@@ -24630,6 +24630,7 @@ var FIELD_ORDER = [
|
|
|
24630
24630
|
"sizeBytes",
|
|
24631
24631
|
"host",
|
|
24632
24632
|
"port",
|
|
24633
|
+
"dayOffsetMinutes",
|
|
24633
24634
|
"path",
|
|
24634
24635
|
"supervisor",
|
|
24635
24636
|
"reason"
|
|
@@ -24926,14 +24927,14 @@ function estimateCachedInputTokens(request) {
|
|
|
24926
24927
|
return cached2;
|
|
24927
24928
|
}
|
|
24928
24929
|
// packages/ir/src/validate.ts
|
|
24929
|
-
function validateRequest(req) {
|
|
24930
|
+
function validateRequest(req, newToolUseId = () => `tu_${crypto.randomUUID()}`) {
|
|
24930
24931
|
const seenToolUseIds = new Set;
|
|
24931
24932
|
const cleaned = [];
|
|
24932
24933
|
for (const message of req.messages) {
|
|
24933
24934
|
const content = [];
|
|
24934
24935
|
for (const block of message.content) {
|
|
24935
24936
|
if (block.type === "toolUse") {
|
|
24936
|
-
const id = block.id.length > 0 ? block.id :
|
|
24937
|
+
const id = block.id.length > 0 ? block.id : newToolUseId();
|
|
24937
24938
|
seenToolUseIds.add(id);
|
|
24938
24939
|
content.push({ ...block, id });
|
|
24939
24940
|
continue;
|
|
@@ -24953,6 +24954,261 @@ function validateRequest(req) {
|
|
|
24953
24954
|
}
|
|
24954
24955
|
return { ...req, messages: cleaned };
|
|
24955
24956
|
}
|
|
24957
|
+
// packages/store/src/sqlite/rollup.ts
|
|
24958
|
+
var HOUR_MS = 3600000;
|
|
24959
|
+
function hourOf(at) {
|
|
24960
|
+
return Math.floor(at / HOUR_MS);
|
|
24961
|
+
}
|
|
24962
|
+
var DAY_MS = 86400000;
|
|
24963
|
+
function hostDayOffsetMinutes() {
|
|
24964
|
+
return -new Date().getTimezoneOffset() || 0;
|
|
24965
|
+
}
|
|
24966
|
+
function startOfDay(at, offsetMinutes) {
|
|
24967
|
+
const offsetMs = offsetMinutes * 60000;
|
|
24968
|
+
return Math.floor((at + offsetMs) / DAY_MS) * DAY_MS - offsetMs;
|
|
24969
|
+
}
|
|
24970
|
+
var UPSERT = `
|
|
24971
|
+
INSERT INTO usage_daily
|
|
24972
|
+
(day, provider, credential_id, requested_model, resolved_model, api_key_id,
|
|
24973
|
+
requests, errors, input_tokens, output_tokens, cache_read_tokens,
|
|
24974
|
+
cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
|
|
24975
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
24976
|
+
ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
|
|
24977
|
+
DO UPDATE SET
|
|
24978
|
+
requests = requests + excluded.requests,
|
|
24979
|
+
errors = errors + excluded.errors,
|
|
24980
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
24981
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
24982
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
24983
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
24984
|
+
rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
|
|
24985
|
+
rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
|
|
24986
|
+
cost_usd = cost_usd + excluded.cost_usd,
|
|
24987
|
+
duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
|
|
24988
|
+
function keyOf(log, dayOffsetMinutes) {
|
|
24989
|
+
return [
|
|
24990
|
+
startOfDay(log.at, dayOffsetMinutes),
|
|
24991
|
+
log.resolvedProvider ?? "",
|
|
24992
|
+
log.credentialId ?? "",
|
|
24993
|
+
log.requestedModel,
|
|
24994
|
+
log.resolvedModel ?? "",
|
|
24995
|
+
log.apiKeyId ?? ""
|
|
24996
|
+
];
|
|
24997
|
+
}
|
|
24998
|
+
function countersOf(log) {
|
|
24999
|
+
return {
|
|
25000
|
+
requests: 1,
|
|
25001
|
+
errors: log.status >= 400 ? 1 : 0,
|
|
25002
|
+
inputTokens: log.inputTokens,
|
|
25003
|
+
outputTokens: log.outputTokens,
|
|
25004
|
+
cacheReadTokens: log.cacheReadTokens,
|
|
25005
|
+
cacheWriteTokens: log.cacheWriteTokens,
|
|
25006
|
+
rtkSavedTokens: log.rtkEstimatedTokensSaved,
|
|
25007
|
+
rtkAppliedRequests: log.rtkApplied ? 1 : 0,
|
|
25008
|
+
costUsd: log.costUsd,
|
|
25009
|
+
durationMsSum: log.durationMs
|
|
25010
|
+
};
|
|
25011
|
+
}
|
|
25012
|
+
var BACKFILL_UPSERT = `
|
|
25013
|
+
INSERT INTO usage_daily
|
|
25014
|
+
(day, provider, credential_id, requested_model, resolved_model, api_key_id,
|
|
25015
|
+
requests, errors, input_tokens, output_tokens, cache_read_tokens,
|
|
25016
|
+
cache_write_tokens, cost_usd, duration_ms_sum)
|
|
25017
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
25018
|
+
ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
|
|
25019
|
+
DO UPDATE SET
|
|
25020
|
+
requests = requests + excluded.requests,
|
|
25021
|
+
errors = errors + excluded.errors,
|
|
25022
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
25023
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
25024
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
25025
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
25026
|
+
cost_usd = cost_usd + excluded.cost_usd,
|
|
25027
|
+
duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
|
|
25028
|
+
function upsertPreRtk(db, key, c) {
|
|
25029
|
+
db.run(BACKFILL_UPSERT, [
|
|
25030
|
+
...key,
|
|
25031
|
+
c.requests,
|
|
25032
|
+
c.errors,
|
|
25033
|
+
c.inputTokens,
|
|
25034
|
+
c.outputTokens,
|
|
25035
|
+
c.cacheReadTokens,
|
|
25036
|
+
c.cacheWriteTokens,
|
|
25037
|
+
c.costUsd,
|
|
25038
|
+
c.durationMsSum
|
|
25039
|
+
]);
|
|
25040
|
+
}
|
|
25041
|
+
function upsert(db, key, c) {
|
|
25042
|
+
db.run(UPSERT, [
|
|
25043
|
+
...key,
|
|
25044
|
+
c.requests,
|
|
25045
|
+
c.errors,
|
|
25046
|
+
c.inputTokens,
|
|
25047
|
+
c.outputTokens,
|
|
25048
|
+
c.cacheReadTokens,
|
|
25049
|
+
c.cacheWriteTokens,
|
|
25050
|
+
c.rtkSavedTokens,
|
|
25051
|
+
c.rtkAppliedRequests,
|
|
25052
|
+
c.costUsd,
|
|
25053
|
+
c.durationMsSum
|
|
25054
|
+
]);
|
|
25055
|
+
}
|
|
25056
|
+
function rollupLog(db, log, dayOffsetMinutes) {
|
|
25057
|
+
upsert(db, keyOf(log, dayOffsetMinutes), countersOf(log));
|
|
25058
|
+
}
|
|
25059
|
+
function backfillDaily(db, dayOffsetMinutes) {
|
|
25060
|
+
const groups = new Map;
|
|
25061
|
+
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
25062
|
+
status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
25063
|
+
duration_ms, cost_usd
|
|
25064
|
+
FROM request_logs`).all()) {
|
|
25065
|
+
const key = [
|
|
25066
|
+
startOfDay(row.at, dayOffsetMinutes),
|
|
25067
|
+
row.resolved_provider ?? "",
|
|
25068
|
+
row.credential_id ?? "",
|
|
25069
|
+
row.requested_model,
|
|
25070
|
+
row.resolved_model ?? "",
|
|
25071
|
+
row.api_key_id ?? ""
|
|
25072
|
+
];
|
|
25073
|
+
const id = key.join("\x00");
|
|
25074
|
+
const seen = groups.get(id);
|
|
25075
|
+
const counters = seen?.counters ?? {
|
|
25076
|
+
requests: 0,
|
|
25077
|
+
errors: 0,
|
|
25078
|
+
inputTokens: 0,
|
|
25079
|
+
outputTokens: 0,
|
|
25080
|
+
cacheReadTokens: 0,
|
|
25081
|
+
cacheWriteTokens: 0,
|
|
25082
|
+
rtkSavedTokens: 0,
|
|
25083
|
+
rtkAppliedRequests: 0,
|
|
25084
|
+
costUsd: 0,
|
|
25085
|
+
durationMsSum: 0
|
|
25086
|
+
};
|
|
25087
|
+
counters.requests += 1;
|
|
25088
|
+
counters.errors += row.status >= 400 ? 1 : 0;
|
|
25089
|
+
counters.inputTokens += row.input_tokens;
|
|
25090
|
+
counters.outputTokens += row.output_tokens;
|
|
25091
|
+
counters.cacheReadTokens += row.cache_read_tokens;
|
|
25092
|
+
counters.cacheWriteTokens += row.cache_write_tokens;
|
|
25093
|
+
counters.costUsd += row.cost_usd;
|
|
25094
|
+
counters.durationMsSum += row.duration_ms;
|
|
25095
|
+
if (seen === undefined)
|
|
25096
|
+
groups.set(id, { key, counters });
|
|
25097
|
+
}
|
|
25098
|
+
for (const group of groups.values())
|
|
25099
|
+
upsertPreRtk(db, group.key, group.counters);
|
|
25100
|
+
return groups.size;
|
|
25101
|
+
}
|
|
25102
|
+
var HOURLY_UPSERT = `
|
|
25103
|
+
INSERT INTO usage_rollup
|
|
25104
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
25105
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
25106
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
25107
|
+
ON CONFLICT (api_key_id, hour)
|
|
25108
|
+
DO UPDATE SET
|
|
25109
|
+
requests = requests + excluded.requests,
|
|
25110
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
25111
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
25112
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
25113
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
25114
|
+
cost_usd = cost_usd + excluded.cost_usd`;
|
|
25115
|
+
function rollupHour(db, log) {
|
|
25116
|
+
const apiKeyId = log.apiKeyId;
|
|
25117
|
+
if (apiKeyId === null)
|
|
25118
|
+
return;
|
|
25119
|
+
db.run(HOURLY_UPSERT, [
|
|
25120
|
+
apiKeyId,
|
|
25121
|
+
hourOf(log.at),
|
|
25122
|
+
1,
|
|
25123
|
+
log.inputTokens,
|
|
25124
|
+
log.outputTokens,
|
|
25125
|
+
log.cacheReadTokens,
|
|
25126
|
+
log.cacheWriteTokens,
|
|
25127
|
+
log.costUsd
|
|
25128
|
+
]);
|
|
25129
|
+
}
|
|
25130
|
+
var REBUILD = `
|
|
25131
|
+
INSERT INTO usage_rollup
|
|
25132
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
25133
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
25134
|
+
SELECT api_key_id,
|
|
25135
|
+
CAST(at / 3600000 AS INTEGER),
|
|
25136
|
+
COUNT(*),
|
|
25137
|
+
COALESCE(SUM(input_tokens), 0),
|
|
25138
|
+
COALESCE(SUM(output_tokens), 0),
|
|
25139
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
25140
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
25141
|
+
COALESCE(SUM(cost_usd), 0)
|
|
25142
|
+
FROM request_logs
|
|
25143
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
25144
|
+
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
25145
|
+
function rebuildRollup(db) {
|
|
25146
|
+
db.run("DELETE FROM usage_rollup");
|
|
25147
|
+
db.run(REBUILD);
|
|
25148
|
+
}
|
|
25149
|
+
function auditRollup(db) {
|
|
25150
|
+
const row = db.query(`WITH truth AS (
|
|
25151
|
+
SELECT api_key_id AS k,
|
|
25152
|
+
CAST(at / 3600000 AS INTEGER) AS h,
|
|
25153
|
+
COUNT(*) AS requests,
|
|
25154
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
25155
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
25156
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
25157
|
+
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
25158
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
25159
|
+
FROM request_logs
|
|
25160
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
25161
|
+
GROUP BY k, h
|
|
25162
|
+
)
|
|
25163
|
+
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
25164
|
+
(SELECT COUNT(*)
|
|
25165
|
+
FROM truth
|
|
25166
|
+
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
25167
|
+
WHERE r.api_key_id IS NULL
|
|
25168
|
+
OR r.requests <> truth.requests
|
|
25169
|
+
OR r.input_tokens <> truth.input_tokens
|
|
25170
|
+
OR r.output_tokens <> truth.output_tokens
|
|
25171
|
+
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
25172
|
+
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
25173
|
+
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
25174
|
+
) AS mismatched,
|
|
25175
|
+
(SELECT COUNT(*)
|
|
25176
|
+
FROM usage_rollup r
|
|
25177
|
+
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
25178
|
+
WHERE truth.k IS NULL
|
|
25179
|
+
) AS orphans`).get();
|
|
25180
|
+
const buckets = row?.buckets ?? 0;
|
|
25181
|
+
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
25182
|
+
return { buckets, mismatched, ok: mismatched === 0 };
|
|
25183
|
+
}
|
|
25184
|
+
function backfillRtkUsage(db, dayOffsetMinutes) {
|
|
25185
|
+
const groups = new Map;
|
|
25186
|
+
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
25187
|
+
rtk_applied, rtk_estimated_tokens_saved
|
|
25188
|
+
FROM request_logs
|
|
25189
|
+
WHERE state = 'done'`).all()) {
|
|
25190
|
+
const key = [
|
|
25191
|
+
startOfDay(row.at, dayOffsetMinutes),
|
|
25192
|
+
row.resolved_provider ?? "",
|
|
25193
|
+
row.credential_id ?? "",
|
|
25194
|
+
row.requested_model,
|
|
25195
|
+
row.resolved_model ?? "",
|
|
25196
|
+
row.api_key_id ?? ""
|
|
25197
|
+
];
|
|
25198
|
+
const id = key.join("\x00");
|
|
25199
|
+
const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
|
|
25200
|
+
group.saved += row.rtk_estimated_tokens_saved;
|
|
25201
|
+
group.applied += row.rtk_applied === 1 ? 1 : 0;
|
|
25202
|
+
groups.set(id, group);
|
|
25203
|
+
}
|
|
25204
|
+
for (const { key, saved, applied } of groups.values()) {
|
|
25205
|
+
db.run(`UPDATE usage_daily
|
|
25206
|
+
SET rtk_saved_tokens = ?, rtk_applied_requests = ?
|
|
25207
|
+
WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
|
|
25208
|
+
AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
|
|
25209
|
+
}
|
|
25210
|
+
}
|
|
25211
|
+
|
|
24956
25212
|
// packages/store/src/postgres/db.ts
|
|
24957
25213
|
var {SQL } = globalThis.Bun;
|
|
24958
25214
|
|
|
@@ -25258,11 +25514,95 @@ CREATE TRIGGER api_keys_config_version
|
|
|
25258
25514
|
FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
|
|
25259
25515
|
`;
|
|
25260
25516
|
|
|
25517
|
+
// packages/store/src/postgres/migrations/002_health_measurements.sql
|
|
25518
|
+
var _002_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before applying to a fleet.
|
|
25519
|
+
--
|
|
25520
|
+
-- This migration drops two columns that every gateway before it writes on
|
|
25521
|
+
-- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
|
|
25522
|
+
-- The first new replica to boot applies it under the migration lock; from that
|
|
25523
|
+
-- moment every OLD replica still running throws on every successful request:
|
|
25524
|
+
-- the upstream call has completed and been billed, the client has received its
|
|
25525
|
+
-- content frames, and then the connection aborts with no terminal frame. The
|
|
25526
|
+
-- \`RollingUpdate\` in \`k8s/deployment.yaml\` does not protect against this \u2014 the
|
|
25527
|
+
-- "surviving" replica is the one that breaks.
|
|
25528
|
+
--
|
|
25529
|
+
-- Required: scale to ONE replica before deploying this release, or accept that
|
|
25530
|
+
-- every old replica fails every successful request until it is replaced.
|
|
25531
|
+
-- Rollback is unsafe: an old image cannot re-add the columns, and running it
|
|
25532
|
+
-- against this schema fails the same way. The only way back is a snapshot
|
|
25533
|
+
-- restore taken before the deploy. Deliberate: kept as a drop, not
|
|
25534
|
+
-- expand/contract, on the operator's decision \u2014 see the write-path spec.
|
|
25535
|
+
--
|
|
25536
|
+
-- \`credential_health\` keeps decisions, not measurements.
|
|
25537
|
+
--
|
|
25538
|
+
-- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
|
|
25539
|
+
-- and a success that changes nothing else now writes nothing. Both live in the
|
|
25540
|
+
-- gateway's process-local \`loadRegistry\` instead. The console's "last used"
|
|
25541
|
+
-- reads \`request_logs\` through \`usage.lastUsedByCredential\`, served by
|
|
25542
|
+
-- \`idx_request_logs_cred\`.
|
|
25543
|
+
ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
|
|
25544
|
+
ALTER TABLE credential_health DROP COLUMN last_used_at;
|
|
25545
|
+
|
|
25546
|
+
-- The statement-level trigger bumped \`config_version\` \u2014 one row, locked until
|
|
25547
|
+
-- commit, by every writer on every replica \u2014 on every health write, and every
|
|
25548
|
+
-- bump makes every replica rebuild its routing snapshot. Only a change to a
|
|
25549
|
+
-- column routing *decides* on needs that; a bare \`consecutive_failures\`
|
|
25550
|
+
-- increment is patched into the held snapshot from the \`healthSaved\` change.
|
|
25551
|
+
--
|
|
25552
|
+
-- The INSERT arm must stay \`FOR EACH ROW\`. The write is an upsert, and a
|
|
25553
|
+
-- statement-level INSERT trigger fires on an upsert whichever path each row
|
|
25554
|
+
-- took, which would bump on every write and make the WHEN clause below moot.
|
|
25555
|
+
-- Row-level \`AFTER INSERT\` fires only for rows actually inserted.
|
|
25556
|
+
DROP TRIGGER credential_health_config_version ON credential_health;
|
|
25557
|
+
|
|
25558
|
+
CREATE TRIGGER credential_health_config_version_upd
|
|
25559
|
+
AFTER UPDATE ON credential_health
|
|
25560
|
+
FOR EACH ROW
|
|
25561
|
+
WHEN (OLD.breaker_state IS DISTINCT FROM NEW.breaker_state
|
|
25562
|
+
OR OLD.rate_limited_until IS DISTINCT FROM NEW.rate_limited_until
|
|
25563
|
+
OR OLD.opened_at IS DISTINCT FROM NEW.opened_at)
|
|
25564
|
+
EXECUTE FUNCTION bump_config_version();
|
|
25565
|
+
|
|
25566
|
+
CREATE TRIGGER credential_health_config_version_ins
|
|
25567
|
+
AFTER INSERT ON credential_health
|
|
25568
|
+
FOR EACH ROW EXECUTE FUNCTION bump_config_version();
|
|
25569
|
+
|
|
25570
|
+
-- DELETE needs no WHEN, so it stays statement-level as in 001: a credential
|
|
25571
|
+
-- with M model rows bumps once, not M times.
|
|
25572
|
+
CREATE TRIGGER credential_health_config_version_del
|
|
25573
|
+
AFTER DELETE ON credential_health
|
|
25574
|
+
FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
|
|
25575
|
+
`;
|
|
25576
|
+
|
|
25577
|
+
// packages/store/src/postgres/migrations/003_key_expiry.sql
|
|
25578
|
+
var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of the same name. \`BIGINT\`, like
|
|
25579
|
+
-- every other epoch-ms column in this schema, and read back through \`numOrNull\`.
|
|
25580
|
+
--
|
|
25581
|
+
-- Nullable rather than \`NOT NULL DEFAULT\`: the column is a deadline, and every
|
|
25582
|
+
-- finite default is a date on which somebody's client stops working. NULL is
|
|
25583
|
+
-- the only value that says nothing was decided, and every reader maps it to
|
|
25584
|
+
-- "never expires".
|
|
25585
|
+
--
|
|
25586
|
+
-- No backfill, deliberately. Every key on every existing installation was
|
|
25587
|
+
-- minted on the promise that it works until revoked, so adding the column and
|
|
25588
|
+
-- stopping is what keeps that promise. The absence of an \`UPDATE\` here is the
|
|
25589
|
+
-- substance of the migration, not an omission.
|
|
25590
|
+
--
|
|
25591
|
+
-- Safe to apply under a rolling deploy, unlike migration 2: this adds a
|
|
25592
|
+
-- nullable column and drops nothing, so a replica running the previous image
|
|
25593
|
+
-- keeps serving \u2014 \`SELECT *\` gains a field its mapper ignores.
|
|
25594
|
+
ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
|
|
25595
|
+
`;
|
|
25596
|
+
|
|
25261
25597
|
// packages/store/src/postgres/db.ts
|
|
25262
25598
|
var num = (v) => Number(v);
|
|
25263
25599
|
var numOrNull = (v) => v === null ? null : Number(v);
|
|
25264
25600
|
var MIGRATION_LOCK = 7140641;
|
|
25265
|
-
var MIGRATIONS = [
|
|
25601
|
+
var MIGRATIONS = [
|
|
25602
|
+
{ id: 1, sql: _001_init_default },
|
|
25603
|
+
{ id: 2, sql: _002_health_measurements_default },
|
|
25604
|
+
{ id: 3, sql: _003_key_expiry_default }
|
|
25605
|
+
];
|
|
25266
25606
|
async function openPg(url2) {
|
|
25267
25607
|
const sql = new SQL({ url: url2 });
|
|
25268
25608
|
const conn = await sql.reserve();
|
|
@@ -25447,6 +25787,9 @@ function resolvePin(target, accounts) {
|
|
|
25447
25787
|
return;
|
|
25448
25788
|
return accounts.find((account) => servesTarget(target, account));
|
|
25449
25789
|
}
|
|
25790
|
+
var keyUsable = (key, now) => key.revokedAt === null && (key.expiresAt === null || key.expiresAt > now);
|
|
25791
|
+
var credentialPastExpiry = (credential, now) => credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now;
|
|
25792
|
+
var credentialExpired = (credential, now) => credentialPastExpiry(credential, now) && !credential.hasRefreshToken;
|
|
25450
25793
|
var NODE_GRACE_MS = 60000;
|
|
25451
25794
|
var DEFAULT_SETTINGS = {
|
|
25452
25795
|
weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
|
|
@@ -25574,31 +25917,25 @@ function toHealth(r) {
|
|
|
25574
25917
|
breakerState: r.breaker_state,
|
|
25575
25918
|
consecutiveFailures: r.consecutive_failures,
|
|
25576
25919
|
openedAt: numOrNull(r.opened_at),
|
|
25577
|
-
rateLimitedUntil: numOrNull(r.rate_limited_until)
|
|
25578
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
25579
|
-
lastUsedAt: numOrNull(r.last_used_at)
|
|
25920
|
+
rateLimitedUntil: numOrNull(r.rate_limited_until)
|
|
25580
25921
|
};
|
|
25581
25922
|
}
|
|
25582
25923
|
var UPSERT_HEALTH = `INSERT INTO credential_health
|
|
25583
25924
|
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
25584
|
-
rate_limited_until
|
|
25585
|
-
VALUES ($1,$2,$3,$4,$5,$6
|
|
25925
|
+
rate_limited_until)
|
|
25926
|
+
VALUES ($1,$2,$3,$4,$5,$6)
|
|
25586
25927
|
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
25587
25928
|
breaker_state = EXCLUDED.breaker_state,
|
|
25588
25929
|
consecutive_failures = EXCLUDED.consecutive_failures,
|
|
25589
25930
|
opened_at = EXCLUDED.opened_at,
|
|
25590
|
-
rate_limited_until = EXCLUDED.rate_limited_until
|
|
25591
|
-
ewma_ttft_ms = EXCLUDED.ewma_ttft_ms,
|
|
25592
|
-
last_used_at = EXCLUDED.last_used_at`;
|
|
25931
|
+
rate_limited_until = EXCLUDED.rate_limited_until`;
|
|
25593
25932
|
var healthValues = (r) => [
|
|
25594
25933
|
r.credentialId,
|
|
25595
25934
|
r.model,
|
|
25596
25935
|
r.breakerState,
|
|
25597
25936
|
r.consecutiveFailures,
|
|
25598
25937
|
r.openedAt,
|
|
25599
|
-
r.rateLimitedUntil
|
|
25600
|
-
r.ewmaTtftMs,
|
|
25601
|
-
r.lastUsedAt
|
|
25938
|
+
r.rateLimitedUntil
|
|
25602
25939
|
];
|
|
25603
25940
|
function createCredentialRepo(sql, key, emit) {
|
|
25604
25941
|
const open2 = async (v) => v === null ? null : decrypt(key, v);
|
|
@@ -25791,13 +26128,6 @@ function createCredentialRepo(sql, key, emit) {
|
|
|
25791
26128
|
const rows = await sql.unsafe("SELECT * FROM credential_health");
|
|
25792
26129
|
return rows.map(toHealth);
|
|
25793
26130
|
},
|
|
25794
|
-
async saveHealth(rows) {
|
|
25795
|
-
await sql.begin(async (tx) => {
|
|
25796
|
-
for (const r of rows)
|
|
25797
|
-
await tx.unsafe(UPSERT_HEALTH, healthValues(r));
|
|
25798
|
-
});
|
|
25799
|
-
emit({ type: "healthSaved", rows });
|
|
25800
|
-
},
|
|
25801
26131
|
async updateHealth(credentialId, model, apply) {
|
|
25802
26132
|
const written = await sql.begin(async (tx) => {
|
|
25803
26133
|
await tx.unsafe("SELECT pg_advisory_xact_lock(hashtext($1))", [`${credentialId}|${model}`]);
|
|
@@ -25939,7 +26269,8 @@ var toKey = (r, logger2) => ({
|
|
|
25939
26269
|
limits: parseLimits(r.id, r.limits, logger2),
|
|
25940
26270
|
bodyLoggingOptOut: r.body_logging_opt_out,
|
|
25941
26271
|
createdAt: num(r.created_at),
|
|
25942
|
-
revokedAt: numOrNull(r.revoked_at)
|
|
26272
|
+
revokedAt: numOrNull(r.revoked_at),
|
|
26273
|
+
expiresAt: numOrNull(r.expires_at)
|
|
25943
26274
|
});
|
|
25944
26275
|
function createKeyRepo(sql, logger2) {
|
|
25945
26276
|
const one = async (where, arg) => {
|
|
@@ -25957,8 +26288,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
25957
26288
|
async create(input2) {
|
|
25958
26289
|
const now = Date.now();
|
|
25959
26290
|
await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
25960
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
25961
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL)`, [
|
|
26291
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
26292
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9)`, [
|
|
25962
26293
|
input2.id,
|
|
25963
26294
|
input2.label,
|
|
25964
26295
|
input2.prefix,
|
|
@@ -25966,7 +26297,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
25966
26297
|
input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
|
|
25967
26298
|
JSON.stringify(parseLimitConfig(input2.limits)),
|
|
25968
26299
|
input2.bodyLoggingOptOut,
|
|
25969
|
-
now
|
|
26300
|
+
now,
|
|
26301
|
+
input2.expiresAt
|
|
25970
26302
|
]);
|
|
25971
26303
|
return { ...input2, createdAt: now, revokedAt: null };
|
|
25972
26304
|
},
|
|
@@ -25974,8 +26306,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
25974
26306
|
if (row.limits === null)
|
|
25975
26307
|
throw new Error(`api key ${row.id} has unreadable limits`);
|
|
25976
26308
|
await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
25977
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
25978
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, [
|
|
26309
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
26310
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [
|
|
25979
26311
|
row.id,
|
|
25980
26312
|
row.label,
|
|
25981
26313
|
row.prefix,
|
|
@@ -25984,7 +26316,8 @@ function createKeyRepo(sql, logger2) {
|
|
|
25984
26316
|
JSON.stringify(parseLimitConfig(row.limits)),
|
|
25985
26317
|
row.bodyLoggingOptOut,
|
|
25986
26318
|
row.createdAt,
|
|
25987
|
-
row.revokedAt
|
|
26319
|
+
row.revokedAt,
|
|
26320
|
+
row.expiresAt
|
|
25988
26321
|
]);
|
|
25989
26322
|
},
|
|
25990
26323
|
async setLimits(id, limits) {
|
|
@@ -25999,6 +26332,9 @@ function createKeyRepo(sql, logger2) {
|
|
|
25999
26332
|
id
|
|
26000
26333
|
]);
|
|
26001
26334
|
},
|
|
26335
|
+
async setExpiry(id, expiresAt) {
|
|
26336
|
+
await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
|
|
26337
|
+
},
|
|
26002
26338
|
async revoke(id) {
|
|
26003
26339
|
await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
|
|
26004
26340
|
}
|
|
@@ -26297,229 +26633,6 @@ function isRtkFilterId(value) {
|
|
|
26297
26633
|
return FILTER_IDS.has(value);
|
|
26298
26634
|
}
|
|
26299
26635
|
|
|
26300
|
-
// packages/store/src/sqlite/rollup.ts
|
|
26301
|
-
var HOUR_MS = 3600000;
|
|
26302
|
-
function hourOf(at) {
|
|
26303
|
-
return Math.floor(at / HOUR_MS);
|
|
26304
|
-
}
|
|
26305
|
-
function startOfLocalDay(at) {
|
|
26306
|
-
const day = new Date(at);
|
|
26307
|
-
day.setHours(0, 0, 0, 0);
|
|
26308
|
-
return day.getTime();
|
|
26309
|
-
}
|
|
26310
|
-
var UPSERT = `
|
|
26311
|
-
INSERT INTO usage_daily
|
|
26312
|
-
(day, provider, credential_id, requested_model, resolved_model, api_key_id,
|
|
26313
|
-
requests, errors, input_tokens, output_tokens, cache_read_tokens,
|
|
26314
|
-
cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
|
|
26315
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
26316
|
-
ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
|
|
26317
|
-
DO UPDATE SET
|
|
26318
|
-
requests = requests + excluded.requests,
|
|
26319
|
-
errors = errors + excluded.errors,
|
|
26320
|
-
input_tokens = input_tokens + excluded.input_tokens,
|
|
26321
|
-
output_tokens = output_tokens + excluded.output_tokens,
|
|
26322
|
-
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
26323
|
-
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
26324
|
-
rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
|
|
26325
|
-
rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
|
|
26326
|
-
cost_usd = cost_usd + excluded.cost_usd,
|
|
26327
|
-
duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
|
|
26328
|
-
function keyOf(log) {
|
|
26329
|
-
return [
|
|
26330
|
-
startOfLocalDay(log.at),
|
|
26331
|
-
log.resolvedProvider ?? "",
|
|
26332
|
-
log.credentialId ?? "",
|
|
26333
|
-
log.requestedModel,
|
|
26334
|
-
log.resolvedModel ?? "",
|
|
26335
|
-
log.apiKeyId ?? ""
|
|
26336
|
-
];
|
|
26337
|
-
}
|
|
26338
|
-
function countersOf(log) {
|
|
26339
|
-
return {
|
|
26340
|
-
requests: 1,
|
|
26341
|
-
errors: log.status >= 400 ? 1 : 0,
|
|
26342
|
-
inputTokens: log.inputTokens,
|
|
26343
|
-
outputTokens: log.outputTokens,
|
|
26344
|
-
cacheReadTokens: log.cacheReadTokens,
|
|
26345
|
-
cacheWriteTokens: log.cacheWriteTokens,
|
|
26346
|
-
rtkSavedTokens: log.rtkEstimatedTokensSaved,
|
|
26347
|
-
rtkAppliedRequests: log.rtkApplied ? 1 : 0,
|
|
26348
|
-
costUsd: log.costUsd,
|
|
26349
|
-
durationMsSum: log.durationMs
|
|
26350
|
-
};
|
|
26351
|
-
}
|
|
26352
|
-
function upsert(db, key, c) {
|
|
26353
|
-
db.run(UPSERT, [
|
|
26354
|
-
...key,
|
|
26355
|
-
c.requests,
|
|
26356
|
-
c.errors,
|
|
26357
|
-
c.inputTokens,
|
|
26358
|
-
c.outputTokens,
|
|
26359
|
-
c.cacheReadTokens,
|
|
26360
|
-
c.cacheWriteTokens,
|
|
26361
|
-
c.rtkSavedTokens,
|
|
26362
|
-
c.rtkAppliedRequests,
|
|
26363
|
-
c.costUsd,
|
|
26364
|
-
c.durationMsSum
|
|
26365
|
-
]);
|
|
26366
|
-
}
|
|
26367
|
-
function rollupLog(db, log) {
|
|
26368
|
-
upsert(db, keyOf(log), countersOf(log));
|
|
26369
|
-
}
|
|
26370
|
-
function backfillDaily(db) {
|
|
26371
|
-
const groups = new Map;
|
|
26372
|
-
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
26373
|
-
status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
26374
|
-
duration_ms, cost_usd
|
|
26375
|
-
FROM request_logs`).all()) {
|
|
26376
|
-
const key = [
|
|
26377
|
-
startOfLocalDay(row.at),
|
|
26378
|
-
row.resolved_provider ?? "",
|
|
26379
|
-
row.credential_id ?? "",
|
|
26380
|
-
row.requested_model,
|
|
26381
|
-
row.resolved_model ?? "",
|
|
26382
|
-
row.api_key_id ?? ""
|
|
26383
|
-
];
|
|
26384
|
-
const id = key.join("\x00");
|
|
26385
|
-
const seen = groups.get(id);
|
|
26386
|
-
const counters = seen?.counters ?? {
|
|
26387
|
-
requests: 0,
|
|
26388
|
-
errors: 0,
|
|
26389
|
-
inputTokens: 0,
|
|
26390
|
-
outputTokens: 0,
|
|
26391
|
-
cacheReadTokens: 0,
|
|
26392
|
-
cacheWriteTokens: 0,
|
|
26393
|
-
rtkSavedTokens: 0,
|
|
26394
|
-
rtkAppliedRequests: 0,
|
|
26395
|
-
costUsd: 0,
|
|
26396
|
-
durationMsSum: 0
|
|
26397
|
-
};
|
|
26398
|
-
counters.requests += 1;
|
|
26399
|
-
counters.errors += row.status >= 400 ? 1 : 0;
|
|
26400
|
-
counters.inputTokens += row.input_tokens;
|
|
26401
|
-
counters.outputTokens += row.output_tokens;
|
|
26402
|
-
counters.cacheReadTokens += row.cache_read_tokens;
|
|
26403
|
-
counters.cacheWriteTokens += row.cache_write_tokens;
|
|
26404
|
-
counters.costUsd += row.cost_usd;
|
|
26405
|
-
counters.durationMsSum += row.duration_ms;
|
|
26406
|
-
if (seen === undefined)
|
|
26407
|
-
groups.set(id, { key, counters });
|
|
26408
|
-
}
|
|
26409
|
-
for (const group of groups.values())
|
|
26410
|
-
upsert(db, group.key, group.counters);
|
|
26411
|
-
return groups.size;
|
|
26412
|
-
}
|
|
26413
|
-
var HOURLY_UPSERT = `
|
|
26414
|
-
INSERT INTO usage_rollup
|
|
26415
|
-
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
26416
|
-
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
26417
|
-
VALUES (?,?,?,?,?,?,?,?)
|
|
26418
|
-
ON CONFLICT (api_key_id, hour)
|
|
26419
|
-
DO UPDATE SET
|
|
26420
|
-
requests = requests + excluded.requests,
|
|
26421
|
-
input_tokens = input_tokens + excluded.input_tokens,
|
|
26422
|
-
output_tokens = output_tokens + excluded.output_tokens,
|
|
26423
|
-
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
26424
|
-
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
26425
|
-
cost_usd = cost_usd + excluded.cost_usd`;
|
|
26426
|
-
function rollupHour(db, log) {
|
|
26427
|
-
const apiKeyId = log.apiKeyId;
|
|
26428
|
-
if (apiKeyId === null)
|
|
26429
|
-
return;
|
|
26430
|
-
db.run(HOURLY_UPSERT, [
|
|
26431
|
-
apiKeyId,
|
|
26432
|
-
hourOf(log.at),
|
|
26433
|
-
1,
|
|
26434
|
-
log.inputTokens,
|
|
26435
|
-
log.outputTokens,
|
|
26436
|
-
log.cacheReadTokens,
|
|
26437
|
-
log.cacheWriteTokens,
|
|
26438
|
-
log.costUsd
|
|
26439
|
-
]);
|
|
26440
|
-
}
|
|
26441
|
-
var REBUILD = `
|
|
26442
|
-
INSERT INTO usage_rollup
|
|
26443
|
-
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
26444
|
-
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
26445
|
-
SELECT api_key_id,
|
|
26446
|
-
CAST(at / 3600000 AS INTEGER),
|
|
26447
|
-
COUNT(*),
|
|
26448
|
-
COALESCE(SUM(input_tokens), 0),
|
|
26449
|
-
COALESCE(SUM(output_tokens), 0),
|
|
26450
|
-
COALESCE(SUM(cache_read_tokens), 0),
|
|
26451
|
-
COALESCE(SUM(cache_write_tokens), 0),
|
|
26452
|
-
COALESCE(SUM(cost_usd), 0)
|
|
26453
|
-
FROM request_logs
|
|
26454
|
-
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
26455
|
-
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
26456
|
-
function rebuildRollup(db) {
|
|
26457
|
-
db.run("DELETE FROM usage_rollup");
|
|
26458
|
-
db.run(REBUILD);
|
|
26459
|
-
}
|
|
26460
|
-
function auditRollup(db) {
|
|
26461
|
-
const row = db.query(`WITH truth AS (
|
|
26462
|
-
SELECT api_key_id AS k,
|
|
26463
|
-
CAST(at / 3600000 AS INTEGER) AS h,
|
|
26464
|
-
COUNT(*) AS requests,
|
|
26465
|
-
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
26466
|
-
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
26467
|
-
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
26468
|
-
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
26469
|
-
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
26470
|
-
FROM request_logs
|
|
26471
|
-
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
26472
|
-
GROUP BY k, h
|
|
26473
|
-
)
|
|
26474
|
-
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
26475
|
-
(SELECT COUNT(*)
|
|
26476
|
-
FROM truth
|
|
26477
|
-
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
26478
|
-
WHERE r.api_key_id IS NULL
|
|
26479
|
-
OR r.requests <> truth.requests
|
|
26480
|
-
OR r.input_tokens <> truth.input_tokens
|
|
26481
|
-
OR r.output_tokens <> truth.output_tokens
|
|
26482
|
-
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
26483
|
-
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
26484
|
-
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
26485
|
-
) AS mismatched,
|
|
26486
|
-
(SELECT COUNT(*)
|
|
26487
|
-
FROM usage_rollup r
|
|
26488
|
-
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
26489
|
-
WHERE truth.k IS NULL
|
|
26490
|
-
) AS orphans`).get();
|
|
26491
|
-
const buckets = row?.buckets ?? 0;
|
|
26492
|
-
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
26493
|
-
return { buckets, mismatched, ok: mismatched === 0 };
|
|
26494
|
-
}
|
|
26495
|
-
function backfillRtkUsage(db) {
|
|
26496
|
-
const groups = new Map;
|
|
26497
|
-
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
26498
|
-
rtk_applied, rtk_estimated_tokens_saved
|
|
26499
|
-
FROM request_logs
|
|
26500
|
-
WHERE state = 'done'`).all()) {
|
|
26501
|
-
const key = [
|
|
26502
|
-
startOfLocalDay(row.at),
|
|
26503
|
-
row.resolved_provider ?? "",
|
|
26504
|
-
row.credential_id ?? "",
|
|
26505
|
-
row.requested_model,
|
|
26506
|
-
row.resolved_model ?? "",
|
|
26507
|
-
row.api_key_id ?? ""
|
|
26508
|
-
];
|
|
26509
|
-
const id = key.join("\x00");
|
|
26510
|
-
const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
|
|
26511
|
-
group.saved += row.rtk_estimated_tokens_saved;
|
|
26512
|
-
group.applied += row.rtk_applied === 1 ? 1 : 0;
|
|
26513
|
-
groups.set(id, group);
|
|
26514
|
-
}
|
|
26515
|
-
for (const { key, saved, applied } of groups.values()) {
|
|
26516
|
-
db.run(`UPDATE usage_daily
|
|
26517
|
-
SET rtk_saved_tokens = ?, rtk_applied_requests = ?
|
|
26518
|
-
WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
|
|
26519
|
-
AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
|
|
26520
|
-
}
|
|
26521
|
-
}
|
|
26522
|
-
|
|
26523
26636
|
// packages/store/src/postgres/rollup.ts
|
|
26524
26637
|
var DAILY_UPSERT = `
|
|
26525
26638
|
INSERT INTO usage_daily
|
|
@@ -26539,9 +26652,9 @@ var DAILY_UPSERT = `
|
|
|
26539
26652
|
rtk_applied_requests = usage_daily.rtk_applied_requests + EXCLUDED.rtk_applied_requests,
|
|
26540
26653
|
cost_usd = usage_daily.cost_usd + EXCLUDED.cost_usd,
|
|
26541
26654
|
duration_ms_sum = usage_daily.duration_ms_sum + EXCLUDED.duration_ms_sum`;
|
|
26542
|
-
async function rollupLog2(conn, log) {
|
|
26655
|
+
async function rollupLog2(conn, log, dayOffsetMinutes) {
|
|
26543
26656
|
await conn.unsafe(DAILY_UPSERT, [
|
|
26544
|
-
|
|
26657
|
+
startOfDay(log.at, dayOffsetMinutes),
|
|
26545
26658
|
log.resolvedProvider ?? "",
|
|
26546
26659
|
log.credentialId ?? "",
|
|
26547
26660
|
log.requestedModel,
|
|
@@ -26785,7 +26898,7 @@ var sum = (row) => ({
|
|
|
26785
26898
|
tokens: row === undefined ? 0 : num(row.tokens),
|
|
26786
26899
|
costUsd: row === undefined ? 0 : row.cost_usd
|
|
26787
26900
|
});
|
|
26788
|
-
function createUsageRepo(sql, nodeId) {
|
|
26901
|
+
function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
|
|
26789
26902
|
const complete = async (conn, log) => {
|
|
26790
26903
|
const written = (await conn.unsafe(COMPLETE, values(log, "done", nodeId))).count;
|
|
26791
26904
|
if (written === 0)
|
|
@@ -26793,7 +26906,7 @@ function createUsageRepo(sql, nodeId) {
|
|
|
26793
26906
|
const stored = (await conn.unsafe("SELECT * FROM request_logs WHERE id = $1", [log.id]))[0];
|
|
26794
26907
|
if (stored !== undefined) {
|
|
26795
26908
|
const restored = toLog(stored);
|
|
26796
|
-
await rollupLog2(conn, restored);
|
|
26909
|
+
await rollupLog2(conn, restored, dayOffsetMinutes);
|
|
26797
26910
|
await rollupHour2(conn, restored);
|
|
26798
26911
|
}
|
|
26799
26912
|
};
|
|
@@ -26824,6 +26937,10 @@ function createUsageRepo(sql, nodeId) {
|
|
|
26824
26937
|
]) : await sql.unsafe("SELECT * FROM request_logs WHERE api_key_id = $1 ORDER BY at DESC LIMIT $2", [apiKeyId, limit]);
|
|
26825
26938
|
return rows.map(toLog);
|
|
26826
26939
|
},
|
|
26940
|
+
async lastUsedByCredential(credentialId) {
|
|
26941
|
+
const rows = await sql.unsafe("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = $1", [credentialId]);
|
|
26942
|
+
return numOrNull(rows[0]?.at ?? null);
|
|
26943
|
+
},
|
|
26827
26944
|
async scan(cursor, limit) {
|
|
26828
26945
|
const rows = cursor === null ? await sql.unsafe("SELECT * FROM request_logs ORDER BY at, id LIMIT $1", [
|
|
26829
26946
|
limit
|
|
@@ -26886,7 +27003,7 @@ function createUsageRepo(sql, nodeId) {
|
|
|
26886
27003
|
const daily = grain === "daily";
|
|
26887
27004
|
const key = columnFor(grain, q.groupBy);
|
|
26888
27005
|
const split = q.splitBy === undefined ? null : columnFor(grain, q.splitBy);
|
|
26889
|
-
const since = daily ?
|
|
27006
|
+
const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
|
|
26890
27007
|
const until = q.until ?? Number.MAX_SAFE_INTEGER;
|
|
26891
27008
|
const timeColumn = daily ? "day" : "at";
|
|
26892
27009
|
const scope = q.apiKeyId;
|
|
@@ -26962,6 +27079,7 @@ function redacted(url2) {
|
|
|
26962
27079
|
async function createPostgresStore(opts) {
|
|
26963
27080
|
const logger2 = opts.logger ?? noopLogger;
|
|
26964
27081
|
const nodeId = opts.nodeId ?? crypto.randomUUID();
|
|
27082
|
+
const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
|
|
26965
27083
|
const listeners = new Set;
|
|
26966
27084
|
const emit = (change) => {
|
|
26967
27085
|
for (const listener of listeners) {
|
|
@@ -26993,7 +27111,7 @@ async function createPostgresStore(opts) {
|
|
|
26993
27111
|
credentials: createCredentialRepo(sql, opts.encryptionKey, emit),
|
|
26994
27112
|
config: createConfigRepo(sql, emit),
|
|
26995
27113
|
keys: createKeyRepo(sql, logger2),
|
|
26996
|
-
usage: createUsageRepo(sql, nodeId),
|
|
27114
|
+
usage: createUsageRepo(sql, nodeId, dayOffsetMinutes),
|
|
26997
27115
|
bodies: createBodyRepo(sql, opts.encryptionKey),
|
|
26998
27116
|
maintenance: createMaintenanceRepo(sql, nodeId),
|
|
26999
27117
|
plugins: createPluginRepo(sql),
|
|
@@ -27230,22 +27348,18 @@ function toHealth2(r) {
|
|
|
27230
27348
|
breakerState: r.breaker_state,
|
|
27231
27349
|
consecutiveFailures: r.consecutive_failures,
|
|
27232
27350
|
openedAt: r.opened_at,
|
|
27233
|
-
rateLimitedUntil: r.rate_limited_until
|
|
27234
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
27235
|
-
lastUsedAt: r.last_used_at
|
|
27351
|
+
rateLimitedUntil: r.rate_limited_until
|
|
27236
27352
|
};
|
|
27237
27353
|
}
|
|
27238
27354
|
var UPSERT_HEALTH2 = `INSERT INTO credential_health
|
|
27239
27355
|
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
27240
|
-
rate_limited_until
|
|
27241
|
-
VALUES (
|
|
27356
|
+
rate_limited_until)
|
|
27357
|
+
VALUES (?,?,?,?,?,?)
|
|
27242
27358
|
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
27243
27359
|
breaker_state = excluded.breaker_state,
|
|
27244
27360
|
consecutive_failures = excluded.consecutive_failures,
|
|
27245
27361
|
opened_at = excluded.opened_at,
|
|
27246
|
-
rate_limited_until = excluded.rate_limited_until
|
|
27247
|
-
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
27248
|
-
last_used_at = excluded.last_used_at`;
|
|
27362
|
+
rate_limited_until = excluded.rate_limited_until`;
|
|
27249
27363
|
function createCredentialRepo2(db, key, emit = () => {}) {
|
|
27250
27364
|
const open2 = async (v) => v === null ? null : decrypt(key, v);
|
|
27251
27365
|
const secretsFrom = async (row) => ({
|
|
@@ -27428,22 +27542,13 @@ function createCredentialRepo2(db, key, emit = () => {}) {
|
|
|
27428
27542
|
async listHealth() {
|
|
27429
27543
|
return db.query("SELECT * FROM credential_health").all().map(toHealth2);
|
|
27430
27544
|
},
|
|
27431
|
-
async saveHealth(rows) {
|
|
27432
|
-
const stmt = db.prepare(UPSERT_HEALTH2);
|
|
27433
|
-
db.transaction(() => {
|
|
27434
|
-
for (const r of rows) {
|
|
27435
|
-
stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
|
|
27436
|
-
}
|
|
27437
|
-
})();
|
|
27438
|
-
emit({ type: "healthSaved", rows });
|
|
27439
|
-
},
|
|
27440
27545
|
async updateHealth(credentialId, model, apply) {
|
|
27441
27546
|
const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
|
|
27442
27547
|
const stmt = db.prepare(UPSERT_HEALTH2);
|
|
27443
27548
|
const written = db.transaction(() => {
|
|
27444
27549
|
const row = current.get(credentialId, model);
|
|
27445
27550
|
const next = apply(row === null ? null : toHealth2(row));
|
|
27446
|
-
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil
|
|
27551
|
+
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil);
|
|
27447
27552
|
return next;
|
|
27448
27553
|
})();
|
|
27449
27554
|
emit({ type: "healthSaved", rows: [written] });
|
|
@@ -28006,6 +28111,63 @@ CREATE TABLE nodes (
|
|
|
28006
28111
|
ALTER TABLE credentials ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;
|
|
28007
28112
|
`;
|
|
28008
28113
|
|
|
28114
|
+
// packages/store/src/sqlite/migrations/013_health_measurements.sql
|
|
28115
|
+
var _013_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before upgrading an install that is still serving.
|
|
28116
|
+
--
|
|
28117
|
+
-- This migration drops two columns that every gateway before it writes on
|
|
28118
|
+
-- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
|
|
28119
|
+
-- SQLite is single-process, so the hazard is the rollback, not the rollout: an
|
|
28120
|
+
-- older gateway started against this database fails every successful request
|
|
28121
|
+
-- after the upstream call has completed and been billed \u2014 content frames out,
|
|
28122
|
+
-- then an aborted connection with no terminal frame. Rolling back the binary
|
|
28123
|
+
-- does not roll back the schema. The only way back is a snapshot taken before
|
|
28124
|
+
-- the upgrade. Deliberate: kept as a drop, not expand/contract, on the
|
|
28125
|
+
-- operator's decision \u2014 see the write-path spec.
|
|
28126
|
+
--
|
|
28127
|
+
-- \`credential_health\` keeps decisions, not measurements.
|
|
28128
|
+
--
|
|
28129
|
+
-- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
|
|
28130
|
+
-- and a success that changes nothing else now writes nothing. Both live in the
|
|
28131
|
+
-- gateway's process-local \`loadRegistry\` instead: latency is a property of
|
|
28132
|
+
-- *this* process's path to the provider, and the round-robin tiebreak reads
|
|
28133
|
+
-- the value the process holds. The console's "last used" reads
|
|
28134
|
+
-- \`request_logs\` through \`usage.lastUsedByCredential\`, served by
|
|
28135
|
+
-- \`idx_request_logs_cred\`.
|
|
28136
|
+
--
|
|
28137
|
+
-- Neither column is indexed or constrained, so \`DROP COLUMN\` is permitted.
|
|
28138
|
+
ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
|
|
28139
|
+
ALTER TABLE credential_health DROP COLUMN last_used_at;
|
|
28140
|
+
`;
|
|
28141
|
+
|
|
28142
|
+
// packages/store/src/sqlite/migrations/014_key_expiry.sql
|
|
28143
|
+
var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an absolute epoch-ms instant after
|
|
28144
|
+
-- which the key is refused, spelled exactly as \`credentials.expires_at\` is.
|
|
28145
|
+
--
|
|
28146
|
+
-- Nullable rather than \`NOT NULL DEFAULT\`, which is the opposite of the choice
|
|
28147
|
+
-- migration 9 made for \`limits\`, and for the opposite reason. There, every
|
|
28148
|
+
-- possible value is a policy and \`{}\` is the one that means "no ceiling", so a
|
|
28149
|
+
-- default gives "unlimited" a single spelling. Here the column is a deadline,
|
|
28150
|
+
-- and no number means "no deadline" \u2014 every finite default is a date on which
|
|
28151
|
+
-- somebody's client stops working. NULL is the only value that says nothing was
|
|
28152
|
+
-- decided, so it is the column's own default and every reader maps it to
|
|
28153
|
+
-- "never expires".
|
|
28154
|
+
--
|
|
28155
|
+
-- No backfill, deliberately, and this is the whole substance of the migration.
|
|
28156
|
+
-- Every key on every existing installation was minted on the promise that it
|
|
28157
|
+
-- works until revoked; an upgrade that attaches an expiry to those rows breaks
|
|
28158
|
+
-- clients on a date the operator never chose, and the failure surfaces days
|
|
28159
|
+
-- later as an authentication error with nothing in it that names this change.
|
|
28160
|
+
-- Adding the column and stopping is what keeps that promise, so the absence of
|
|
28161
|
+
-- an \`UPDATE\` here is load-bearing rather than an omission \u2014
|
|
28162
|
+
-- \`packages/store/test/migrations.test.ts\` pins it.
|
|
28163
|
+
--
|
|
28164
|
+
-- No index. The column is read only after a row has already been found by
|
|
28165
|
+
-- \`hash\` or by \`id\`, both of which are unique, so there is no scan for an index
|
|
28166
|
+
-- to help; and nothing sweeps expired keys, because an expired key stays listed
|
|
28167
|
+
-- exactly as a revoked one does so its usage history keeps its attribution.
|
|
28168
|
+
ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
|
|
28169
|
+
`;
|
|
28170
|
+
|
|
28009
28171
|
// packages/store/src/sqlite/db.ts
|
|
28010
28172
|
var MIGRATIONS2 = [
|
|
28011
28173
|
{ id: 1, sql: _001_init_default2 },
|
|
@@ -28019,9 +28181,16 @@ var MIGRATIONS2 = [
|
|
|
28019
28181
|
{ id: 9, sql: _009_key_limits_default },
|
|
28020
28182
|
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
|
|
28021
28183
|
{ id: 11, sql: _011_plugin_migrations_default },
|
|
28022
|
-
{ id: 12, sql: _012_nodes_default }
|
|
28184
|
+
{ id: 12, sql: _012_nodes_default },
|
|
28185
|
+
{ id: 13, sql: _013_health_measurements_default },
|
|
28186
|
+
{ id: 14, sql: _014_key_expiry_default }
|
|
28023
28187
|
];
|
|
28024
|
-
function
|
|
28188
|
+
function statements(sql) {
|
|
28189
|
+
return sql.split(`
|
|
28190
|
+
`).filter((line) => !line.trimStart().startsWith("--")).join(`
|
|
28191
|
+
`).split(/;[ \t]*(?:\n|$)/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
28192
|
+
}
|
|
28193
|
+
function openDb(path, dayOffsetMinutes = hostDayOffsetMinutes()) {
|
|
28025
28194
|
const db = new Database(path, { create: true });
|
|
28026
28195
|
db.run("PRAGMA journal_mode = WAL");
|
|
28027
28196
|
db.run("PRAGMA synchronous = NORMAL");
|
|
@@ -28033,8 +28202,9 @@ function openDb(path) {
|
|
|
28033
28202
|
if (done.has(m.id))
|
|
28034
28203
|
continue;
|
|
28035
28204
|
db.transaction(() => {
|
|
28036
|
-
|
|
28037
|
-
|
|
28205
|
+
for (const statement of statements(m.sql))
|
|
28206
|
+
db.run(statement);
|
|
28207
|
+
m.after?.(db, dayOffsetMinutes);
|
|
28038
28208
|
db.run("INSERT INTO migrations (id, applied_at) VALUES (?, ?)", [m.id, Date.now()]);
|
|
28039
28209
|
})();
|
|
28040
28210
|
}
|
|
@@ -28069,7 +28239,8 @@ var toKey2 = (r, logger2) => ({
|
|
|
28069
28239
|
limits: parseLimits2(r.id, r.limits, logger2),
|
|
28070
28240
|
bodyLoggingOptOut: r.body_logging_opt_out === 1,
|
|
28071
28241
|
createdAt: r.created_at,
|
|
28072
|
-
revokedAt: r.revoked_at
|
|
28242
|
+
revokedAt: r.revoked_at,
|
|
28243
|
+
expiresAt: r.expires_at
|
|
28073
28244
|
});
|
|
28074
28245
|
function createKeyRepo2(db, logger2 = noopLogger) {
|
|
28075
28246
|
return {
|
|
@@ -28087,8 +28258,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
28087
28258
|
async create(input2) {
|
|
28088
28259
|
const now = Date.now();
|
|
28089
28260
|
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
28090
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
28091
|
-
VALUES (?,?,?,?,?,?,?,?,NULL)`, [
|
|
28261
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
28262
|
+
VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
|
|
28092
28263
|
input2.id,
|
|
28093
28264
|
input2.label,
|
|
28094
28265
|
input2.prefix,
|
|
@@ -28096,7 +28267,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
28096
28267
|
input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
|
|
28097
28268
|
JSON.stringify(parseLimitConfig(input2.limits)),
|
|
28098
28269
|
input2.bodyLoggingOptOut ? 1 : 0,
|
|
28099
|
-
now
|
|
28270
|
+
now,
|
|
28271
|
+
input2.expiresAt
|
|
28100
28272
|
]);
|
|
28101
28273
|
return { ...input2, createdAt: now, revokedAt: null };
|
|
28102
28274
|
},
|
|
@@ -28104,8 +28276,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
28104
28276
|
if (row.limits === null)
|
|
28105
28277
|
throw new Error(`api key ${row.id} has unreadable limits`);
|
|
28106
28278
|
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
28107
|
-
body_logging_opt_out, created_at, revoked_at)
|
|
28108
|
-
VALUES (
|
|
28279
|
+
body_logging_opt_out, created_at, revoked_at, expires_at)
|
|
28280
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)`, [
|
|
28109
28281
|
row.id,
|
|
28110
28282
|
row.label,
|
|
28111
28283
|
row.prefix,
|
|
@@ -28114,7 +28286,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
28114
28286
|
JSON.stringify(parseLimitConfig(row.limits)),
|
|
28115
28287
|
row.bodyLoggingOptOut ? 1 : 0,
|
|
28116
28288
|
row.createdAt,
|
|
28117
|
-
row.revokedAt
|
|
28289
|
+
row.revokedAt,
|
|
28290
|
+
row.expiresAt
|
|
28118
28291
|
]);
|
|
28119
28292
|
},
|
|
28120
28293
|
async setLimits(id, limits) {
|
|
@@ -28129,6 +28302,9 @@ function createKeyRepo2(db, logger2 = noopLogger) {
|
|
|
28129
28302
|
id
|
|
28130
28303
|
]);
|
|
28131
28304
|
},
|
|
28305
|
+
async setExpiry(id, expiresAt) {
|
|
28306
|
+
db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
|
|
28307
|
+
},
|
|
28132
28308
|
async revoke(id) {
|
|
28133
28309
|
db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
|
|
28134
28310
|
}
|
|
@@ -28451,14 +28627,14 @@ var COMPLETE2 = `INSERT INTO request_logs ${COLUMNS2} VALUES ${PLACEHOLDERS2}
|
|
|
28451
28627
|
rtk_estimated_tokens_saved = excluded.rtk_estimated_tokens_saved,
|
|
28452
28628
|
rtk_filters = excluded.rtk_filters
|
|
28453
28629
|
WHERE request_logs.state = 'pending'`;
|
|
28454
|
-
function createUsageRepo2(db, nodeId) {
|
|
28630
|
+
function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes()) {
|
|
28455
28631
|
const complete = db.transaction((log) => {
|
|
28456
28632
|
if (db.run(COMPLETE2, values2(log, "done", nodeId)).changes === 0)
|
|
28457
28633
|
return;
|
|
28458
28634
|
const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
|
|
28459
28635
|
if (stored !== null) {
|
|
28460
28636
|
const restored = toLog2(stored);
|
|
28461
|
-
rollupLog(db, restored);
|
|
28637
|
+
rollupLog(db, restored, dayOffsetMinutes);
|
|
28462
28638
|
rollupHour(db, restored);
|
|
28463
28639
|
}
|
|
28464
28640
|
});
|
|
@@ -28508,6 +28684,10 @@ function createUsageRepo2(db, nodeId) {
|
|
|
28508
28684
|
}
|
|
28509
28685
|
return db.query("SELECT * FROM request_logs WHERE api_key_id = ? ORDER BY at DESC LIMIT ?").all(apiKeyId, limit).map(toLog2);
|
|
28510
28686
|
},
|
|
28687
|
+
async lastUsedByCredential(credentialId) {
|
|
28688
|
+
const row = db.query("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = ?").get(credentialId);
|
|
28689
|
+
return row?.at ?? null;
|
|
28690
|
+
},
|
|
28511
28691
|
async scan(cursor, limit) {
|
|
28512
28692
|
const rows = cursor === null ? db.query("SELECT * FROM request_logs ORDER BY at, id LIMIT ?").all(limit) : db.query(`SELECT * FROM request_logs
|
|
28513
28693
|
WHERE at > ? OR (at = ? AND id > ?)
|
|
@@ -28569,7 +28749,7 @@ function createUsageRepo2(db, nodeId) {
|
|
|
28569
28749
|
const daily = grain === "daily";
|
|
28570
28750
|
const key = columnFor2(grain, q.groupBy);
|
|
28571
28751
|
const split = q.splitBy === undefined ? null : columnFor2(grain, q.splitBy);
|
|
28572
|
-
const since = daily ?
|
|
28752
|
+
const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
|
|
28573
28753
|
const until = q.until ?? Number.MAX_SAFE_INTEGER;
|
|
28574
28754
|
const timeColumn = daily ? "day" : "at";
|
|
28575
28755
|
const scope = q.apiKeyId;
|
|
@@ -28620,6 +28800,7 @@ function createUsageRepo2(db, nodeId) {
|
|
|
28620
28800
|
async function createStore(opts) {
|
|
28621
28801
|
const logger2 = opts.logger ?? noopLogger;
|
|
28622
28802
|
const nodeId = opts.nodeId ?? crypto.randomUUID();
|
|
28803
|
+
const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
|
|
28623
28804
|
const listeners = new Set;
|
|
28624
28805
|
const emit = (change) => {
|
|
28625
28806
|
for (const listener of listeners) {
|
|
@@ -28629,14 +28810,14 @@ async function createStore(opts) {
|
|
|
28629
28810
|
}
|
|
28630
28811
|
};
|
|
28631
28812
|
const open2 = () => {
|
|
28632
|
-
const db = openDb(opts.path);
|
|
28813
|
+
const db = openDb(opts.path, dayOffsetMinutes);
|
|
28633
28814
|
logger2.debug("store opened", { path: opts.path });
|
|
28634
28815
|
return {
|
|
28635
28816
|
db,
|
|
28636
28817
|
credentials: createCredentialRepo2(db, opts.encryptionKey, emit),
|
|
28637
28818
|
config: createConfigRepo2(db, emit),
|
|
28638
28819
|
keys: createKeyRepo2(db, logger2),
|
|
28639
|
-
usage: createUsageRepo2(db, nodeId),
|
|
28820
|
+
usage: createUsageRepo2(db, nodeId, dayOffsetMinutes),
|
|
28640
28821
|
bodies: createBodyRepo2(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
28641
28822
|
maintenance: createMaintenanceRepo2(db, nodeId),
|
|
28642
28823
|
plugins: createPluginRepo2(db)
|
|
@@ -28662,7 +28843,6 @@ async function createStore(opts) {
|
|
|
28662
28843
|
updateSecrets: (id, secrets, expiresAt, expectedVersion) => handle.credentials.updateSecrets(id, secrets, expiresAt, expectedVersion),
|
|
28663
28844
|
remove: (id) => handle.credentials.remove(id),
|
|
28664
28845
|
listHealth: () => handle.credentials.listHealth(),
|
|
28665
|
-
saveHealth: (rows) => handle.credentials.saveHealth(rows),
|
|
28666
28846
|
updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
|
|
28667
28847
|
listQuota: () => handle.credentials.listQuota(),
|
|
28668
28848
|
saveQuota: (rows) => handle.credentials.saveQuota(rows),
|
|
@@ -28689,6 +28869,7 @@ async function createStore(opts) {
|
|
|
28689
28869
|
importRow: (row) => handle.keys.importRow(row),
|
|
28690
28870
|
setLimits: (id, limits) => handle.keys.setLimits(id, limits),
|
|
28691
28871
|
setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
|
|
28872
|
+
setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
|
|
28692
28873
|
revoke: (id) => handle.keys.revoke(id)
|
|
28693
28874
|
},
|
|
28694
28875
|
usage: {
|
|
@@ -28697,6 +28878,7 @@ async function createStore(opts) {
|
|
|
28697
28878
|
append: (log) => handle.usage.append(log),
|
|
28698
28879
|
sweepPending: (now) => handle.usage.sweepPending(now),
|
|
28699
28880
|
recent: (limit, apiKeyId) => handle.usage.recent(limit, apiKeyId),
|
|
28881
|
+
lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
|
|
28700
28882
|
scan: (cursor, limit) => handle.usage.scan(cursor, limit),
|
|
28701
28883
|
aggregate: (q) => handle.usage.aggregate(q),
|
|
28702
28884
|
sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
|
|
@@ -28796,7 +28978,7 @@ function createAdminAuth(store, opts) {
|
|
|
28796
28978
|
};
|
|
28797
28979
|
const keyStillValid = async (apiKeyId) => {
|
|
28798
28980
|
const key = await store.keys.get(apiKeyId);
|
|
28799
|
-
return key !== null && key.
|
|
28981
|
+
return key !== null && keyUsable(key, opts.now());
|
|
28800
28982
|
};
|
|
28801
28983
|
const applyPassword = async (password) => {
|
|
28802
28984
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
@@ -28850,7 +29032,7 @@ function createAdminAuth(store, opts) {
|
|
|
28850
29032
|
},
|
|
28851
29033
|
async loginClient(rawKey) {
|
|
28852
29034
|
const key = await store.keys.findByHash(await hashApiKey(rawKey));
|
|
28853
|
-
if (key === null || key.
|
|
29035
|
+
if (key === null || !keyUsable(key, opts.now()))
|
|
28854
29036
|
return null;
|
|
28855
29037
|
return issue2({ kind: "client", apiKeyId: key.id });
|
|
28856
29038
|
},
|
|
@@ -29689,6 +29871,7 @@ function toClientLog(log) {
|
|
|
29689
29871
|
var MIN_KEY_LENGTH = 16;
|
|
29690
29872
|
var TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
29691
29873
|
var DECIMAL_INTEGER = /^\d+$/;
|
|
29874
|
+
var SIGNED_INTEGER = /^-?\d+$/;
|
|
29692
29875
|
function optionalText(value, fallback) {
|
|
29693
29876
|
return value?.trim() || fallback;
|
|
29694
29877
|
}
|
|
@@ -29719,6 +29902,11 @@ function loadConfig(env) {
|
|
|
29719
29902
|
} else if (databaseUrl !== undefined || redisUrl !== undefined) {
|
|
29720
29903
|
throw new Error("OMNI_DATABASE_URL and OMNI_REDIS_URL are cluster-mode settings; set OMNI_CLUSTER_MODE=true or unset them");
|
|
29721
29904
|
}
|
|
29905
|
+
const rawDayOffset = env.OMNI_DAY_OFFSET_MINUTES?.trim();
|
|
29906
|
+
const parsedDayOffset = rawDayOffset === undefined ? NaN : Number(rawDayOffset);
|
|
29907
|
+
const validDayOffset = rawDayOffset !== undefined && SIGNED_INTEGER.test(rawDayOffset) && Number.isInteger(parsedDayOffset) && parsedDayOffset >= -1440 && parsedDayOffset <= 1440;
|
|
29908
|
+
const dayOffsetMinutes = validDayOffset ? parsedDayOffset : hostDayOffsetMinutes();
|
|
29909
|
+
const dayOffsetFallbackFrom = rawDayOffset !== undefined && !validDayOffset ? rawDayOffset : null;
|
|
29722
29910
|
const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
|
|
29723
29911
|
const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
|
|
29724
29912
|
const logLevel = parseLogLevel(rawLogLevel);
|
|
@@ -29749,6 +29937,8 @@ function loadConfig(env) {
|
|
|
29749
29937
|
clusterMode,
|
|
29750
29938
|
databaseUrl: clusterMode ? databaseUrl ?? null : null,
|
|
29751
29939
|
redisUrl: clusterMode ? redisUrl ?? null : null,
|
|
29940
|
+
dayOffsetMinutes,
|
|
29941
|
+
dayOffsetFallbackFrom,
|
|
29752
29942
|
metricsToken,
|
|
29753
29943
|
metricsMaxSeries,
|
|
29754
29944
|
otlpEndpoint,
|
|
@@ -36448,16 +36638,19 @@ var modelSchema = exports_external.object({
|
|
|
36448
36638
|
isAlias: exports_external.boolean(),
|
|
36449
36639
|
targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
|
|
36450
36640
|
});
|
|
36641
|
+
var MAX_INSTANT = 8640000000000000;
|
|
36451
36642
|
var keyCreateSchema = exports_external.object({
|
|
36452
36643
|
label: exports_external.string().min(1).default("api key"),
|
|
36453
36644
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
|
|
36454
36645
|
limits: limitConfigSchema.default({}),
|
|
36455
|
-
bodyLoggingOptOut: exports_external.boolean().default(false)
|
|
36646
|
+
bodyLoggingOptOut: exports_external.boolean().default(false),
|
|
36647
|
+
expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable().default(null)
|
|
36456
36648
|
}).strict();
|
|
36457
36649
|
var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
|
|
36458
36650
|
var keyModelsSchema = exports_external.object({
|
|
36459
36651
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
|
|
36460
36652
|
}).strict();
|
|
36653
|
+
var keyExpirySchema = exports_external.object({ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable() }).strict();
|
|
36461
36654
|
var retentionSchema = exports_external.object({
|
|
36462
36655
|
keepLatest: exports_external.number().int().min(1).max(100),
|
|
36463
36656
|
maxAgeDays: exports_external.number().int().min(1).max(3650)
|
|
@@ -36672,10 +36865,11 @@ function eligible(input2) {
|
|
|
36672
36865
|
drop("disabled");
|
|
36673
36866
|
continue;
|
|
36674
36867
|
}
|
|
36675
|
-
if (credential
|
|
36868
|
+
if (credentialExpired(credential, now)) {
|
|
36676
36869
|
drop("expired");
|
|
36677
36870
|
continue;
|
|
36678
36871
|
}
|
|
36872
|
+
let probe = false;
|
|
36679
36873
|
const h = snapshot.health.get(healthKey(credential.id, target.model));
|
|
36680
36874
|
if (h !== undefined) {
|
|
36681
36875
|
if (h.rateLimitedUntil !== null && h.rateLimitedUntil > now) {
|
|
@@ -36688,6 +36882,9 @@ function eligible(input2) {
|
|
|
36688
36882
|
drop("breaker:open");
|
|
36689
36883
|
continue;
|
|
36690
36884
|
}
|
|
36885
|
+
probe = true;
|
|
36886
|
+
} else if (h.breakerState === "halfOpen") {
|
|
36887
|
+
probe = true;
|
|
36691
36888
|
}
|
|
36692
36889
|
}
|
|
36693
36890
|
const spent = (snapshot.quota.get(credential.id) ?? []).find((w) => w.limit !== null && w.used >= w.limit && (w.resetsAt === null || w.resetsAt > now));
|
|
@@ -36695,7 +36892,7 @@ function eligible(input2) {
|
|
|
36695
36892
|
drop(`quota:${spent.windowType}`);
|
|
36696
36893
|
continue;
|
|
36697
36894
|
}
|
|
36698
|
-
pairs.push({ credential, target });
|
|
36895
|
+
pairs.push({ credential, target, probe });
|
|
36699
36896
|
}
|
|
36700
36897
|
if (target.credentialId === undefined && providerSeen && !servedAny) {
|
|
36701
36898
|
excluded.push({
|
|
@@ -36772,7 +36969,7 @@ function healthScore(h) {
|
|
|
36772
36969
|
return h?.breakerState === "open" || h?.breakerState === "halfOpen" ? base * 0.5 : base;
|
|
36773
36970
|
}
|
|
36774
36971
|
function score(pairs, input2) {
|
|
36775
|
-
const { snapshot, now, load } = input2;
|
|
36972
|
+
const { snapshot, now, load, recent } = input2;
|
|
36776
36973
|
const w = snapshot.settings.weights;
|
|
36777
36974
|
const tiers = pairs.map((p) => p.target.tier);
|
|
36778
36975
|
const minTier = Math.min(...tiers);
|
|
@@ -36780,9 +36977,10 @@ function score(pairs, input2) {
|
|
|
36780
36977
|
const tokens2 = estimateTokens(input2.request);
|
|
36781
36978
|
const costs = pairs.map((p) => requestCost(p.target, tokens2));
|
|
36782
36979
|
const bestCost = bestPositive(costs);
|
|
36980
|
+
const ewmaOf = (key) => recent?.get(key)?.ewmaTtftMs ?? null;
|
|
36783
36981
|
const bestLatency = bestPositive(pairs.flatMap((p) => {
|
|
36784
|
-
const
|
|
36785
|
-
return
|
|
36982
|
+
const ewma = ewmaOf(healthKey(p.credential.id, p.target.model));
|
|
36983
|
+
return ewma === null ? [] : [ewma];
|
|
36786
36984
|
}));
|
|
36787
36985
|
return pairs.map((pair, i) => {
|
|
36788
36986
|
const key = healthKey(pair.credential.id, pair.target.model);
|
|
@@ -36794,12 +36992,14 @@ function score(pairs, input2) {
|
|
|
36794
36992
|
const quota = quotaHeadroom(pair.credential, snapshot.quota.get(pair.credential.id) ?? [], now, snapshot.settings.quotaPollIntervalMs);
|
|
36795
36993
|
const ownCost = costs[i];
|
|
36796
36994
|
const cost = ownCost <= 0 || bestCost === null ? UNKNOWN : ratio(ownCost, bestCost);
|
|
36797
|
-
const
|
|
36995
|
+
const ewma = ewmaOf(key);
|
|
36996
|
+
const latency = ewma === null || ewma <= 0 || bestLatency === null ? UNKNOWN : ratio(ewma, bestLatency);
|
|
36798
36997
|
const reasons = { tier, health, quota, cost, latency, load: loadTerm };
|
|
36799
36998
|
const base = tier * w.tier + health * w.health + quota * w.quota + cost * w.cost + latency * w.latency + loadTerm * w.load;
|
|
36800
36999
|
return {
|
|
36801
37000
|
credential: pair.credential,
|
|
36802
37001
|
target: pair.target,
|
|
37002
|
+
probe: pair.probe,
|
|
36803
37003
|
score: base * pair.credential.weight * pair.target.weight,
|
|
36804
37004
|
reasons
|
|
36805
37005
|
};
|
|
@@ -36825,7 +37025,6 @@ var PENALTY = {
|
|
|
36825
37025
|
ALL_CANDIDATES_FAILED: "none",
|
|
36826
37026
|
INTERNAL: "none"
|
|
36827
37027
|
};
|
|
36828
|
-
var EWMA_ALPHA = 0.3;
|
|
36829
37028
|
var DEFAULT_RATE_LIMIT_MS = 60000;
|
|
36830
37029
|
var QUOTA_PARK_MS = 3600000;
|
|
36831
37030
|
var MAX_JITTER_MS = 2000;
|
|
@@ -36836,21 +37035,28 @@ function blankHealth(credentialId, model) {
|
|
|
36836
37035
|
breakerState: "closed",
|
|
36837
37036
|
consecutiveFailures: 0,
|
|
36838
37037
|
openedAt: null,
|
|
36839
|
-
rateLimitedUntil: null
|
|
36840
|
-
ewmaTtftMs: null,
|
|
36841
|
-
lastUsedAt: null
|
|
37038
|
+
rateLimitedUntil: null
|
|
36842
37039
|
};
|
|
36843
37040
|
}
|
|
36844
|
-
|
|
36845
|
-
|
|
37041
|
+
var SUCCESS_RESETS = [
|
|
37042
|
+
"breakerState",
|
|
37043
|
+
"consecutiveFailures",
|
|
37044
|
+
"openedAt",
|
|
37045
|
+
"rateLimitedUntil"
|
|
37046
|
+
];
|
|
37047
|
+
function successWouldChange(current) {
|
|
37048
|
+
if (current === undefined)
|
|
37049
|
+
return false;
|
|
37050
|
+
const blank = blankHealth(current.credentialId, current.model);
|
|
37051
|
+
return SUCCESS_RESETS.some((field) => current[field] !== blank[field]);
|
|
37052
|
+
}
|
|
37053
|
+
function recordSuccess(current) {
|
|
36846
37054
|
return {
|
|
36847
37055
|
...current,
|
|
36848
37056
|
breakerState: "closed",
|
|
36849
37057
|
consecutiveFailures: 0,
|
|
36850
37058
|
openedAt: null,
|
|
36851
|
-
rateLimitedUntil: null
|
|
36852
|
-
ewmaTtftMs: ewma,
|
|
36853
|
-
lastUsedAt: opts.now
|
|
37059
|
+
rateLimitedUntil: null
|
|
36854
37060
|
};
|
|
36855
37061
|
}
|
|
36856
37062
|
function recordFailure(current, opts) {
|
|
@@ -36860,7 +37066,7 @@ function recordFailure(current, opts) {
|
|
|
36860
37066
|
if (penalty === "soft") {
|
|
36861
37067
|
const base = opts.code === "QUOTA_EXHAUSTED" ? QUOTA_PARK_MS : opts.retryAfterMs ?? DEFAULT_RATE_LIMIT_MS;
|
|
36862
37068
|
const until = opts.now + base + Math.round((opts.jitter ?? 0) * MAX_JITTER_MS);
|
|
36863
|
-
return { ...current, rateLimitedUntil: until
|
|
37069
|
+
return { ...current, rateLimitedUntil: until };
|
|
36864
37070
|
}
|
|
36865
37071
|
const failures = current.consecutiveFailures + 1;
|
|
36866
37072
|
const open2 = opts.code === "AUTH" || current.breakerState === "halfOpen" || failures >= opts.settings.breakerThreshold;
|
|
@@ -36868,8 +37074,7 @@ function recordFailure(current, opts) {
|
|
|
36868
37074
|
...current,
|
|
36869
37075
|
consecutiveFailures: failures,
|
|
36870
37076
|
breakerState: open2 ? "open" : "closed",
|
|
36871
|
-
openedAt: open2 ? opts.now : current.openedAt
|
|
36872
|
-
lastUsedAt: opts.now
|
|
37077
|
+
openedAt: open2 ? opts.now : current.openedAt
|
|
36873
37078
|
};
|
|
36874
37079
|
}
|
|
36875
37080
|
// packages/router/src/resolve.ts
|
|
@@ -36950,8 +37155,8 @@ function rank(input2) {
|
|
|
36950
37155
|
break;
|
|
36951
37156
|
case "roundRobin": {
|
|
36952
37157
|
const idle = (c) => {
|
|
36953
|
-
const
|
|
36954
|
-
return
|
|
37158
|
+
const at = input2.recent?.get(healthKey(c.credential.id, c.target.model))?.lastReleasedAt;
|
|
37159
|
+
return at === undefined ? Number.POSITIVE_INFINITY : input2.now - at;
|
|
36955
37160
|
};
|
|
36956
37161
|
const spent = (c) => headroom(c) < QUOTA_FLOOR ? 1 : 0;
|
|
36957
37162
|
scored.sort((a, b) => spent(a) - spent(b) || inflight(a) - inflight(b) || idle(b) - idle(a));
|
|
@@ -37107,16 +37312,23 @@ async function createApiKeyCredential(store, input2, logger2 = noopLogger, provi
|
|
|
37107
37312
|
return summarizeCredential(created);
|
|
37108
37313
|
}
|
|
37109
37314
|
async function credentialHealth(deps) {
|
|
37110
|
-
const [health, quota, settings] = await Promise.all([
|
|
37315
|
+
const [health, quota, settings, credentials] = await Promise.all([
|
|
37111
37316
|
deps.store.credentials.listHealth(),
|
|
37112
37317
|
deps.store.credentials.listQuota(),
|
|
37113
|
-
deps.store.config.getSettings()
|
|
37318
|
+
deps.store.config.getSettings(),
|
|
37319
|
+
deps.store.credentials.list()
|
|
37114
37320
|
]);
|
|
37321
|
+
const lastUsed = {};
|
|
37322
|
+
for (const c of credentials) {
|
|
37323
|
+
const at = await deps.store.usage.lastUsedByCredential(c.id);
|
|
37324
|
+
if (at !== null)
|
|
37325
|
+
lastUsed[c.id] = at;
|
|
37326
|
+
}
|
|
37115
37327
|
const burn = burnEstimates(quota, {
|
|
37116
37328
|
now: deps.now(),
|
|
37117
37329
|
pollIntervalMs: settings.quotaPollIntervalMs
|
|
37118
37330
|
});
|
|
37119
|
-
return { health, quota, burn };
|
|
37331
|
+
return { health, quota, burn, lastUsed };
|
|
37120
37332
|
}
|
|
37121
37333
|
async function patchCredential(deps, id, input2) {
|
|
37122
37334
|
const patch = parseOrThrow(credentialPatchSchema, input2);
|
|
@@ -37228,14 +37440,14 @@ function listSnapshots(deps) {
|
|
|
37228
37440
|
}
|
|
37229
37441
|
return snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
37230
37442
|
}
|
|
37231
|
-
var
|
|
37443
|
+
var DAY_MS2 = 86400000;
|
|
37232
37444
|
var SNAPSHOT_HEADROOM = 1.1;
|
|
37233
37445
|
function retentionOf(settings) {
|
|
37234
37446
|
return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
|
|
37235
37447
|
}
|
|
37236
37448
|
function applyRetention(deps, policy, now) {
|
|
37237
37449
|
const dir = snapshotsDir(deps);
|
|
37238
|
-
const cutoff = now - policy.maxAgeDays *
|
|
37450
|
+
const cutoff = now - policy.maxAgeDays * DAY_MS2;
|
|
37239
37451
|
const snapshots = listSnapshots(deps);
|
|
37240
37452
|
const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
|
|
37241
37453
|
let removed = 0;
|
|
@@ -37599,7 +37811,8 @@ async function toSummary(store, key, now) {
|
|
|
37599
37811
|
limitUsage: await readLimitUsage(store, key.id, key.limits, now),
|
|
37600
37812
|
bodyLoggingOptOut: key.bodyLoggingOptOut,
|
|
37601
37813
|
createdAt: key.createdAt,
|
|
37602
|
-
revokedAt: key.revokedAt
|
|
37814
|
+
revokedAt: key.revokedAt,
|
|
37815
|
+
expiresAt: key.expiresAt
|
|
37603
37816
|
};
|
|
37604
37817
|
}
|
|
37605
37818
|
async function listKeys(store, now = Date.now()) {
|
|
@@ -37622,7 +37835,8 @@ async function createKey(store, input2) {
|
|
|
37622
37835
|
hash: await hashApiKey(raw),
|
|
37623
37836
|
modelAllowlist: body2.modelAllowlist,
|
|
37624
37837
|
limits: body2.limits,
|
|
37625
|
-
bodyLoggingOptOut: body2.bodyLoggingOptOut
|
|
37838
|
+
bodyLoggingOptOut: body2.bodyLoggingOptOut,
|
|
37839
|
+
expiresAt: body2.expiresAt
|
|
37626
37840
|
});
|
|
37627
37841
|
return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
|
|
37628
37842
|
}
|
|
@@ -37642,6 +37856,14 @@ async function setKeyModels(store, id, input2, now = Date.now()) {
|
|
|
37642
37856
|
await store.keys.setModelAllowlist(id, body2.modelAllowlist);
|
|
37643
37857
|
return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
|
|
37644
37858
|
}
|
|
37859
|
+
async function setKeyExpiry(store, id, input2, now = Date.now()) {
|
|
37860
|
+
const body2 = parseOrThrow(keyExpirySchema, input2);
|
|
37861
|
+
const key = await store.keys.get(id);
|
|
37862
|
+
if (key === null)
|
|
37863
|
+
throw new GatewayError("BAD_REQUEST", "no such api key");
|
|
37864
|
+
await store.keys.setExpiry(id, body2.expiresAt);
|
|
37865
|
+
return toSummary(store, { ...key, expiresAt: body2.expiresAt }, now);
|
|
37866
|
+
}
|
|
37645
37867
|
async function revokeKey(store, id) {
|
|
37646
37868
|
await store.keys.revoke(id);
|
|
37647
37869
|
}
|
|
@@ -38490,12 +38712,18 @@ async function accountQuota(deps) {
|
|
|
38490
38712
|
}
|
|
38491
38713
|
|
|
38492
38714
|
// packages/control/src/quota/history.ts
|
|
38493
|
-
var
|
|
38715
|
+
var DAY_MS3 = 24 * 60 * 60 * 1000;
|
|
38494
38716
|
var HOUR_MS3 = 3600000;
|
|
38717
|
+
var MAX_SAMPLES = 50000;
|
|
38718
|
+
var SAMPLE_QUERY_LIMIT = MAX_SAMPLES + 1;
|
|
38719
|
+
function pageSamples(samples) {
|
|
38720
|
+
const truncated = samples.length > MAX_SAMPLES;
|
|
38721
|
+
return { page: truncated ? samples.slice(0, MAX_SAMPLES) : samples, truncated };
|
|
38722
|
+
}
|
|
38495
38723
|
async function retainedSpan(deps, input2, maxSpanMs) {
|
|
38496
38724
|
const now = deps.now();
|
|
38497
38725
|
const settings = await deps.store.config.getSettings();
|
|
38498
|
-
const retained = now - settings.logRetentionDays *
|
|
38726
|
+
const retained = now - settings.logRetentionDays * DAY_MS3;
|
|
38499
38727
|
const oldest = maxSpanMs === undefined ? retained : Math.max(retained, now - maxSpanMs);
|
|
38500
38728
|
return {
|
|
38501
38729
|
since: Math.max(optionalNumber(input2.since, oldest), oldest),
|
|
@@ -38538,26 +38766,26 @@ async function quotaHistory(deps, input2) {
|
|
|
38538
38766
|
deps.store.credentials.listQuotaSamples({
|
|
38539
38767
|
since,
|
|
38540
38768
|
until,
|
|
38769
|
+
limit: SAMPLE_QUERY_LIMIT,
|
|
38541
38770
|
...credentialId === undefined ? {} : { credentialId }
|
|
38542
38771
|
}),
|
|
38543
38772
|
deps.store.credentials.listQuota()
|
|
38544
38773
|
]);
|
|
38545
38774
|
const scoped = credentialId === undefined ? quota : quota.filter((w) => w.credentialId === credentialId);
|
|
38546
|
-
|
|
38775
|
+
const { page, truncated } = pageSamples(samples);
|
|
38776
|
+
return { samples: page, truncated, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
|
|
38547
38777
|
}
|
|
38548
38778
|
|
|
38549
38779
|
// packages/control/src/quota/clientHistory.ts
|
|
38550
38780
|
var MAX_SPAN_MS = 16 * 24 * 60 * 60 * 1000;
|
|
38551
|
-
var MAX_SAMPLES = 50000;
|
|
38552
38781
|
async function accountQuotaHistory(deps, input2) {
|
|
38553
38782
|
const { since, until } = await retainedSpan(deps, input2, MAX_SPAN_MS);
|
|
38554
38783
|
const [samples, credentials] = await Promise.all([
|
|
38555
|
-
deps.store.credentials.listQuotaSamples({ since, until, limit:
|
|
38784
|
+
deps.store.credentials.listQuotaSamples({ since, until, limit: SAMPLE_QUERY_LIMIT }),
|
|
38556
38785
|
deps.store.credentials.list()
|
|
38557
38786
|
]);
|
|
38558
38787
|
const byId = new Map(credentials.map((credential) => [credential.id, credential]));
|
|
38559
|
-
const truncated = samples
|
|
38560
|
-
const page = truncated ? samples.slice(0, MAX_SAMPLES) : samples;
|
|
38788
|
+
const { page, truncated } = pageSamples(samples);
|
|
38561
38789
|
const out = [];
|
|
38562
38790
|
for (const sample of page) {
|
|
38563
38791
|
const credential = byId.get(sample.credentialId);
|
|
@@ -53559,9 +53787,11 @@ function withReset(headroom, window2, resetAt) {
|
|
|
53559
53787
|
// apps/gateway/src/dispatch/loadRegistry.ts
|
|
53560
53788
|
var PREFIX6 = "load:";
|
|
53561
53789
|
var SLOT_TTL_MS = 300000;
|
|
53562
|
-
|
|
53790
|
+
var EWMA_ALPHA = 0.3;
|
|
53791
|
+
function createLoadRegistry(coord = memoryCoord(), now = Date.now) {
|
|
53563
53792
|
const local = new Map;
|
|
53564
53793
|
const providers = new Map;
|
|
53794
|
+
const history = new Map;
|
|
53565
53795
|
let remote = new Map;
|
|
53566
53796
|
return {
|
|
53567
53797
|
acquire(credentialId, model, provider) {
|
|
@@ -53571,10 +53801,15 @@ function createLoadRegistry(coord = memoryCoord()) {
|
|
|
53571
53801
|
providers.set(provider, (providers.get(provider) ?? 0) + 1);
|
|
53572
53802
|
coord.gauge.acquire(PREFIX6 + key, SLOT_TTL_MS);
|
|
53573
53803
|
let released = false;
|
|
53574
|
-
return () => {
|
|
53804
|
+
return (ttftMs) => {
|
|
53575
53805
|
if (released)
|
|
53576
53806
|
return;
|
|
53577
53807
|
released = true;
|
|
53808
|
+
const prior = history.get(key)?.ewmaTtftMs ?? null;
|
|
53809
|
+
history.set(key, {
|
|
53810
|
+
lastReleasedAt: now(),
|
|
53811
|
+
ewmaTtftMs: ttftMs === undefined || ttftMs === null ? prior : prior === null ? ttftMs : prior * (1 - EWMA_ALPHA) + ttftMs * EWMA_ALPHA
|
|
53812
|
+
});
|
|
53578
53813
|
const next = (local.get(key) ?? 0) - 1;
|
|
53579
53814
|
if (next > 0)
|
|
53580
53815
|
local.set(key, next);
|
|
@@ -53601,6 +53836,9 @@ function createLoadRegistry(coord = memoryCoord()) {
|
|
|
53601
53836
|
localCounts() {
|
|
53602
53837
|
return new Map(providers);
|
|
53603
53838
|
},
|
|
53839
|
+
recent() {
|
|
53840
|
+
return history;
|
|
53841
|
+
},
|
|
53604
53842
|
async refresh() {
|
|
53605
53843
|
const held = await coord.gauge.snapshot(PREFIX6);
|
|
53606
53844
|
const sample = new Map;
|
|
@@ -54969,6 +55207,11 @@ function adminRoutes(deps) {
|
|
|
54969
55207
|
const key = await setKeyModels(deps.store, params.id, await readJson(request2));
|
|
54970
55208
|
changed("res:keys");
|
|
54971
55209
|
return key;
|
|
55210
|
+
}).put("/api/keys/:id/expiry", async ({ request: request2, params }) => {
|
|
55211
|
+
await requireAdmin(request2, deps.admin);
|
|
55212
|
+
const key = await setKeyExpiry(deps.store, params.id, await readJson(request2));
|
|
55213
|
+
changed("res:keys");
|
|
55214
|
+
return key;
|
|
54972
55215
|
}).delete("/api/keys/:id", async ({ request: request2, params }) => {
|
|
54973
55216
|
await requireAdmin(request2, deps.admin);
|
|
54974
55217
|
await revokeKey(deps.store, params.id);
|
|
@@ -54979,6 +55222,7 @@ function adminRoutes(deps) {
|
|
|
54979
55222
|
return {
|
|
54980
55223
|
settings: await getSettings(deps.store),
|
|
54981
55224
|
bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
|
|
55225
|
+
dayOffsetMinutes: deps.dayOffsetMinutes ?? hostDayOffsetMinutes(),
|
|
54982
55226
|
viewerConfigured: await deps.admin.isViewerConfigured()
|
|
54983
55227
|
};
|
|
54984
55228
|
}).put("/api/settings/password", async ({ request: request2 }) => {
|
|
@@ -55113,7 +55357,10 @@ function clientRoutes(deps) {
|
|
|
55113
55357
|
return { ok: true };
|
|
55114
55358
|
}).get("/api/client/summary", async ({ request: request2 }) => {
|
|
55115
55359
|
const apiKeyId = await requireClient(request2, deps.admin);
|
|
55116
|
-
return
|
|
55360
|
+
return {
|
|
55361
|
+
...await readOwnKey(deps.store, apiKeyId, now()),
|
|
55362
|
+
dayOffsetMinutes: deps.dayOffsetMinutes ?? hostDayOffsetMinutes()
|
|
55363
|
+
};
|
|
55117
55364
|
}).get("/api/client/usage", async ({ request: request2, query }) => {
|
|
55118
55365
|
const apiKeyId = await requireClient(request2, deps.admin);
|
|
55119
55366
|
return queryUsage({ store: deps.store, now }, {
|
|
@@ -55513,12 +55760,12 @@ function injectPonytail(request2, opts) {
|
|
|
55513
55760
|
}
|
|
55514
55761
|
|
|
55515
55762
|
// apps/gateway/src/auth/apiKey.ts
|
|
55516
|
-
async function authenticateApiKey(store, header, logger2 = noopLogger) {
|
|
55763
|
+
async function authenticateApiKey(store, header, now, logger2 = noopLogger) {
|
|
55517
55764
|
const raw = extractToken(header);
|
|
55518
55765
|
if (raw === null)
|
|
55519
55766
|
throw new GatewayError("AUTH", "missing or malformed Authorization header");
|
|
55520
55767
|
const key = await store.keys.findByHash(await hashApiKey(raw));
|
|
55521
|
-
if (key === null || key
|
|
55768
|
+
if (key === null || !keyUsable(key, now)) {
|
|
55522
55769
|
throw new GatewayError("AUTH", "invalid API key");
|
|
55523
55770
|
}
|
|
55524
55771
|
const limits = key.limits;
|
|
@@ -57826,6 +58073,7 @@ function priceOf(prices, usage, provider, providers = PROVIDER_DESCRIPTORS) {
|
|
|
57826
58073
|
}
|
|
57827
58074
|
|
|
57828
58075
|
// apps/gateway/src/dispatch/index.ts
|
|
58076
|
+
var PROBE_TTL_MS = 180000;
|
|
57829
58077
|
async function dispatch(request2, deps, signal, requestId) {
|
|
57830
58078
|
const logger2 = deps.logger ?? noopLogger;
|
|
57831
58079
|
const startedAt = deps.now();
|
|
@@ -57919,6 +58167,7 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
57919
58167
|
now: startedAt,
|
|
57920
58168
|
rand: deps.rand(),
|
|
57921
58169
|
load: deps.loadRegistry.counts(),
|
|
58170
|
+
recent: deps.loadRegistry.recent(),
|
|
57922
58171
|
providers: deps.providers
|
|
57923
58172
|
});
|
|
57924
58173
|
if (deps.trace !== null && deps.trace !== undefined) {
|
|
@@ -57980,8 +58229,11 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
57980
58229
|
async function* run() {
|
|
57981
58230
|
try {
|
|
57982
58231
|
let lastError = null;
|
|
58232
|
+
let attempts = 0;
|
|
57983
58233
|
candidateLoop:
|
|
57984
|
-
for (
|
|
58234
|
+
for (const candidate of candidates) {
|
|
58235
|
+
if (attempts >= maxAttempts)
|
|
58236
|
+
break;
|
|
57985
58237
|
try {
|
|
57986
58238
|
checkCancellation();
|
|
57987
58239
|
} catch (error61) {
|
|
@@ -57990,22 +58242,45 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
57990
58242
|
lastError = error61 instanceof GatewayError ? error61 : new GatewayError("TIMEOUT", "request deadline exceeded");
|
|
57991
58243
|
break;
|
|
57992
58244
|
}
|
|
57993
|
-
const candidate = candidates[i];
|
|
57994
58245
|
const adoptable = eagerHeld && eager.credentialId === candidate.credential.id && eager.model === candidate.target.model;
|
|
57995
58246
|
if (adoptable)
|
|
57996
58247
|
eagerHeld = false;
|
|
57997
58248
|
const releaseSlot = adoptable ? eager.release : deps.loadRegistry.acquire(candidate.credential.id, candidate.target.model, candidate.target.provider);
|
|
58249
|
+
const successWrites = successWouldChange(snapshot.health.get(healthKey(candidate.credential.id, candidate.target.model)));
|
|
57998
58250
|
const attemptStartedAt = deps.now();
|
|
57999
58251
|
let attemptCode;
|
|
58000
|
-
|
|
58001
|
-
|
|
58002
|
-
attempt: i + 1,
|
|
58003
|
-
provider: candidate.target.provider,
|
|
58004
|
-
model: candidate.target.model,
|
|
58005
|
-
credential_id: candidate.credential.id
|
|
58006
|
-
});
|
|
58007
|
-
}
|
|
58252
|
+
let releaseProbe;
|
|
58253
|
+
let probing = false;
|
|
58008
58254
|
try {
|
|
58255
|
+
if (candidate.probe) {
|
|
58256
|
+
const key = `probe:${healthKey(candidate.credential.id, candidate.target.model)}`;
|
|
58257
|
+
const held = await deps.coord.gauge.acquire(key, PROBE_TTL_MS);
|
|
58258
|
+
releaseProbe = () => deps.coord.gauge.release(key);
|
|
58259
|
+
if (held > 0) {
|
|
58260
|
+
noteDegradations([`excluded:${candidate.credential.id}:breaker:probing`]);
|
|
58261
|
+
logger2.debug("routing candidate excluded", {
|
|
58262
|
+
requestId,
|
|
58263
|
+
credentialId: candidate.credential.id,
|
|
58264
|
+
reason: "breaker:probing"
|
|
58265
|
+
});
|
|
58266
|
+
continue;
|
|
58267
|
+
}
|
|
58268
|
+
await persistHealth(candidate, (current) => {
|
|
58269
|
+
if (current.breakerState === "closed")
|
|
58270
|
+
return current;
|
|
58271
|
+
probing = true;
|
|
58272
|
+
return { ...current, breakerState: "halfOpen" };
|
|
58273
|
+
});
|
|
58274
|
+
}
|
|
58275
|
+
const i = attempts++;
|
|
58276
|
+
if (deps.trace !== null && deps.trace !== undefined) {
|
|
58277
|
+
deps.trace.activeAttempt = addSpan(deps.trace, "dispatch.attempt", 0, attemptStartedAt - startedAt, attemptStartedAt - startedAt, {
|
|
58278
|
+
attempt: i + 1,
|
|
58279
|
+
provider: candidate.target.provider,
|
|
58280
|
+
model: candidate.target.model,
|
|
58281
|
+
credential_id: candidate.credential.id
|
|
58282
|
+
});
|
|
58283
|
+
}
|
|
58009
58284
|
log.attempts = i + 1;
|
|
58010
58285
|
log.credentialId = candidate.credential.id;
|
|
58011
58286
|
log.resolvedProvider = candidate.target.provider;
|
|
@@ -58028,6 +58303,7 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
58028
58303
|
log.cacheWriteTokens = 0;
|
|
58029
58304
|
log.ttftMs = null;
|
|
58030
58305
|
let committed = false;
|
|
58306
|
+
let closedAtCommit = false;
|
|
58031
58307
|
let authRefreshRetried = false;
|
|
58032
58308
|
let retrySecrets;
|
|
58033
58309
|
const attemptNow = deps.now();
|
|
@@ -58082,6 +58358,11 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
58082
58358
|
for (const buffered of pending)
|
|
58083
58359
|
yield buffered;
|
|
58084
58360
|
pending.length = 0;
|
|
58361
|
+
if (probing) {
|
|
58362
|
+
await persistHealth(candidate, recordSuccess);
|
|
58363
|
+
probing = false;
|
|
58364
|
+
closedAtCommit = true;
|
|
58365
|
+
}
|
|
58085
58366
|
}
|
|
58086
58367
|
if (event.type === "end") {
|
|
58087
58368
|
terminal = true;
|
|
@@ -58139,11 +58420,8 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
58139
58420
|
if (!terminal) {
|
|
58140
58421
|
throw new GatewayError("UPSTREAM", "upstream stream ended without a terminal event");
|
|
58141
58422
|
}
|
|
58142
|
-
|
|
58143
|
-
|
|
58144
|
-
now: deps.now(),
|
|
58145
|
-
ttftMs: log.ttftMs
|
|
58146
|
-
}));
|
|
58423
|
+
if (successWrites && !closedAtCommit)
|
|
58424
|
+
await persistHealth(candidate, recordSuccess);
|
|
58147
58425
|
log.status = 200;
|
|
58148
58426
|
log.errorCode = null;
|
|
58149
58427
|
log.durationMs = deps.now() - startedAt;
|
|
@@ -58196,7 +58474,7 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
58196
58474
|
...failure.retryAfterMs === undefined ? {} : { retryAfterMs: failure.retryAfterMs },
|
|
58197
58475
|
jitter: deps.rand()
|
|
58198
58476
|
}));
|
|
58199
|
-
if (!committed && RETRYABLE[failure.code] &&
|
|
58477
|
+
if (!committed && RETRYABLE[failure.code] && attempts < maxAttempts) {
|
|
58200
58478
|
logger2.warn("attempt failed; retrying", {
|
|
58201
58479
|
requestId,
|
|
58202
58480
|
provider: candidate.target.provider,
|
|
@@ -58236,14 +58514,16 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
58236
58514
|
}
|
|
58237
58515
|
deps.trace.activeAttempt = null;
|
|
58238
58516
|
}
|
|
58239
|
-
releaseSlot();
|
|
58517
|
+
releaseSlot(log.ttftMs);
|
|
58518
|
+
releaseProbe?.();
|
|
58240
58519
|
}
|
|
58241
58520
|
}
|
|
58242
|
-
const
|
|
58521
|
+
const untried = attempts === 0 && lastError === null;
|
|
58522
|
+
const code = lastError?.code === "TIMEOUT" ? "TIMEOUT" : lastError !== null && !RETRYABLE[lastError.code] ? lastError.code : untried ? "NO_CANDIDATES" : "ALL_CANDIDATES_FAILED";
|
|
58243
58523
|
log.status = HTTP_STATUS[code];
|
|
58244
58524
|
log.errorCode = code;
|
|
58245
58525
|
log.durationMs = deps.now() - startedAt;
|
|
58246
|
-
const message = lastError?.message ?? "all candidates failed";
|
|
58526
|
+
const message = lastError?.message ?? (untried ? `no eligible credential for model "${safeToken(request2.model)}"` : "all candidates failed");
|
|
58247
58527
|
reject(lastError?.provider === undefined ? new GatewayError(code, message) : new GatewayError(code, message, {
|
|
58248
58528
|
provider: lastError.provider,
|
|
58249
58529
|
...lastError.gatewayAuthored ? { gatewayAuthored: true } : {}
|
|
@@ -59549,7 +59829,7 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
59549
59829
|
try {
|
|
59550
59830
|
let key;
|
|
59551
59831
|
try {
|
|
59552
|
-
key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.logger);
|
|
59832
|
+
key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), startedAt, deps.logger);
|
|
59553
59833
|
} catch (error61) {
|
|
59554
59834
|
if (error61 instanceof GatewayError && error61.code === "AUTH") {
|
|
59555
59835
|
deps.logger.warn("authentication rejected", {
|
|
@@ -59706,17 +59986,14 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
59706
59986
|
}
|
|
59707
59987
|
function proxyRoutes(deps) {
|
|
59708
59988
|
const logger2 = deps.logger ?? noopLogger;
|
|
59709
|
-
const
|
|
59710
|
-
|
|
59711
|
-
now: deps.now,
|
|
59712
|
-
logger: logger2,
|
|
59713
|
-
...deps.coord === undefined ? {} : { coord: deps.coord }
|
|
59714
|
-
});
|
|
59989
|
+
const coord = deps.coord ?? memoryCoord();
|
|
59990
|
+
const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({ store: deps.store, now: deps.now, logger: logger2, coord });
|
|
59715
59991
|
const dispatchDeps = {
|
|
59716
59992
|
...deps,
|
|
59717
59993
|
logger: logger2,
|
|
59994
|
+
coord,
|
|
59718
59995
|
snapshots: deps.snapshots ?? createRoutingSnapshotCache(deps.store, logger2),
|
|
59719
|
-
loadRegistry: deps.loadRegistry ?? createLoadRegistry(
|
|
59996
|
+
loadRegistry: deps.loadRegistry ?? createLoadRegistry(coord),
|
|
59720
59997
|
keepaliveMs: deps.keepaliveMs ?? KEEPALIVE_MS
|
|
59721
59998
|
};
|
|
59722
59999
|
return new Elysia().post("/v1/messages", ({ request: request2, server }) => {
|
|
@@ -59730,7 +60007,7 @@ function proxyRoutes(deps) {
|
|
|
59730
60007
|
return handle(dispatchDeps, rateLimiter, "openai", request2);
|
|
59731
60008
|
}).get("/v1/models", async ({ request: request2 }) => {
|
|
59732
60009
|
try {
|
|
59733
|
-
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
|
|
60010
|
+
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.now(), logger2);
|
|
59734
60011
|
const snapshot = await dispatchDeps.snapshots.get(deps.now());
|
|
59735
60012
|
const models = [...snapshot.models.values()];
|
|
59736
60013
|
const visibleModels = key.modelAllowlist === null ? models : models.filter((model) => key.modelAllowlist?.includes(model.id));
|
|
@@ -59746,7 +60023,7 @@ function proxyRoutes(deps) {
|
|
|
59746
60023
|
}
|
|
59747
60024
|
}).post("/v1/messages/count_tokens", async ({ request: request2 }) => {
|
|
59748
60025
|
try {
|
|
59749
|
-
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
|
|
60026
|
+
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.now(), logger2);
|
|
59750
60027
|
await rateLimiter.consume(key.id, key.limits, deps.requestId());
|
|
59751
60028
|
const body2 = await request2.json();
|
|
59752
60029
|
const chatRequest = parseAnthropicRequest(body2, request2.headers);
|
|
@@ -60597,7 +60874,8 @@ function sizeOf(payload) {
|
|
|
60597
60874
|
return 0;
|
|
60598
60875
|
}
|
|
60599
60876
|
}
|
|
60600
|
-
|
|
60877
|
+
var DEFAULT_RING_LIMITS = { frames: 500, bytes: 2 * 1024 * 1024 };
|
|
60878
|
+
function createRing(limits = DEFAULT_RING_LIMITS) {
|
|
60601
60879
|
const topics = new Map;
|
|
60602
60880
|
const stateFor = (topic) => {
|
|
60603
60881
|
const existing = topics.get(topic);
|
|
@@ -60660,7 +60938,7 @@ function createRing(limits) {
|
|
|
60660
60938
|
}
|
|
60661
60939
|
|
|
60662
60940
|
// apps/gateway/src/version.ts
|
|
60663
|
-
var VERSION = "0.
|
|
60941
|
+
var VERSION = "0.11.1";
|
|
60664
60942
|
|
|
60665
60943
|
// apps/gateway/src/app.ts
|
|
60666
60944
|
var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -60747,7 +61025,7 @@ function createApp(deps) {
|
|
|
60747
61025
|
now,
|
|
60748
61026
|
onDetach: (id, topics) => channelsRef?.closed(id, topics)
|
|
60749
61027
|
});
|
|
60750
|
-
const ring = deps.ring ?? createRing(
|
|
61028
|
+
const ring = deps.ring ?? createRing();
|
|
60751
61029
|
const broadcaster = deps.broadcaster ?? createBroadcaster({ registry: registry2, ring, coord, nodeId, now });
|
|
60752
61030
|
const channels = deps.channels ?? createChannelRegistry({
|
|
60753
61031
|
sockets: registry2,
|
|
@@ -60788,7 +61066,7 @@ function createApp(deps) {
|
|
|
60788
61066
|
return new Response(null, { status: 401 });
|
|
60789
61067
|
}
|
|
60790
61068
|
const coordFallback = deps.coordHealthy?.() === false;
|
|
60791
|
-
return new Response(telemetry.scrape(loadRegistry.localCounts?.() ?? new Map, registry2.stats(), coordFallback), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
|
|
61069
|
+
return new Response(telemetry.scrape(loadRegistry.localCounts?.() ?? new Map, registry2.stats(), coordFallback, deps.coordFaults?.() ?? 0), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
|
|
60792
61070
|
})).use(proxyRoutes({
|
|
60793
61071
|
store: deps.store,
|
|
60794
61072
|
snapshots,
|
|
@@ -60811,6 +61089,7 @@ function createApp(deps) {
|
|
|
60811
61089
|
admin,
|
|
60812
61090
|
baseUrl: deps.baseUrl,
|
|
60813
61091
|
bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
|
|
61092
|
+
...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
|
|
60814
61093
|
now,
|
|
60815
61094
|
sessionTtlMs: ADMIN_SESSION_TTL_MS,
|
|
60816
61095
|
logger: logger2,
|
|
@@ -60822,6 +61101,7 @@ function createApp(deps) {
|
|
|
60822
61101
|
store: deps.store,
|
|
60823
61102
|
admin,
|
|
60824
61103
|
sessionTtlMs: ADMIN_SESSION_TTL_MS,
|
|
61104
|
+
...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
|
|
60825
61105
|
now,
|
|
60826
61106
|
logger: logger2
|
|
60827
61107
|
})).use(databaseRoutes({
|
|
@@ -60917,6 +61197,7 @@ var CHANNEL2 = `${NS}pubsub`;
|
|
|
60917
61197
|
var LOCK_POLL_MS = 50;
|
|
60918
61198
|
var FAULT_LOG_INTERVAL_MS = 30000;
|
|
60919
61199
|
var CONNECT_WAIT_MS = 2000;
|
|
61200
|
+
var COMMAND_TIMEOUT_MS = 1000;
|
|
60920
61201
|
var RECONNECTED2 = "coord:reconnected";
|
|
60921
61202
|
var WINDOW_CLAIM = `
|
|
60922
61203
|
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', tonumber(ARGV[1]) - tonumber(ARGV[2]))
|
|
@@ -61030,10 +61311,17 @@ var SCRIPTS = [
|
|
|
61030
61311
|
LEASE_ACQUIRE,
|
|
61031
61312
|
PUBLISH_SEQUENCED
|
|
61032
61313
|
];
|
|
61314
|
+
|
|
61315
|
+
class TimedOut extends Error {
|
|
61316
|
+
constructor() {
|
|
61317
|
+
super("command timed out");
|
|
61318
|
+
}
|
|
61319
|
+
}
|
|
61033
61320
|
function redisCoord(deps) {
|
|
61034
61321
|
const logger2 = deps.logger ?? noopLogger;
|
|
61035
61322
|
const now = deps.now ?? (() => Date.now());
|
|
61036
61323
|
const faultInterval = deps.faultLogIntervalMs ?? FAULT_LOG_INTERVAL_MS;
|
|
61324
|
+
const commandTimeout = deps.commandTimeoutMs ?? COMMAND_TIMEOUT_MS;
|
|
61037
61325
|
const fallback = memoryCoord({ now });
|
|
61038
61326
|
const client = new RedisClient(deps.url, {
|
|
61039
61327
|
connectionTimeout: 2000,
|
|
@@ -61047,17 +61335,15 @@ function redisCoord(deps) {
|
|
|
61047
61335
|
const colon = name.indexOf(":");
|
|
61048
61336
|
return `${NS}gidx:${colon === -1 ? name : name.slice(0, colon + 1)}`;
|
|
61049
61337
|
};
|
|
61338
|
+
let faults = 0;
|
|
61050
61339
|
const fault = (error61) => {
|
|
61051
61340
|
healthy = false;
|
|
61341
|
+
faults++;
|
|
61052
61342
|
const at = now();
|
|
61053
61343
|
if (at - lastFault < faultInterval)
|
|
61054
61344
|
return;
|
|
61055
61345
|
lastFault = at;
|
|
61056
|
-
logger2.warn("coordinator unreachable; serving from memory", {
|
|
61057
|
-
coord: "redis",
|
|
61058
|
-
coordFallback: true,
|
|
61059
|
-
reason: describeError(error61, "unknown")
|
|
61060
|
-
});
|
|
61346
|
+
logger2.warn(error61 instanceof TimedOut ? "coordinator slow; serving from memory" : "coordinator unreachable; serving from memory", { coord: "redis", coordFallback: true, reason: describeError(error61, "unknown") });
|
|
61061
61347
|
};
|
|
61062
61348
|
const shas = new Map;
|
|
61063
61349
|
const loadScripts = async () => {
|
|
@@ -61085,22 +61371,39 @@ function redisCoord(deps) {
|
|
|
61085
61371
|
}, (error61) => fault(error61)),
|
|
61086
61372
|
Bun.sleep(CONNECT_WAIT_MS)
|
|
61087
61373
|
]);
|
|
61374
|
+
let lostDebit = false;
|
|
61088
61375
|
const reseedAfterOutage = async () => {
|
|
61089
61376
|
const keys = await scan(client, `${key("b", "")}*`);
|
|
61090
61377
|
if (keys.length > 0)
|
|
61091
61378
|
await client.send("UNLINK", keys);
|
|
61092
61379
|
};
|
|
61380
|
+
const deadline = async (redis) => {
|
|
61381
|
+
let timer;
|
|
61382
|
+
try {
|
|
61383
|
+
return await Promise.race([
|
|
61384
|
+
redis(),
|
|
61385
|
+
new Promise((_resolve, reject) => {
|
|
61386
|
+
timer = setTimeout(() => reject(new TimedOut), commandTimeout);
|
|
61387
|
+
})
|
|
61388
|
+
]);
|
|
61389
|
+
} finally {
|
|
61390
|
+
clearTimeout(timer);
|
|
61391
|
+
}
|
|
61392
|
+
};
|
|
61093
61393
|
const attempt2 = async (redis, instead) => {
|
|
61094
61394
|
try {
|
|
61095
61395
|
await ready;
|
|
61096
61396
|
if (!client.connected)
|
|
61097
61397
|
throw new Error("not connected");
|
|
61098
|
-
const out = await redis
|
|
61099
|
-
if (!healthy && lastFault !== Number.NEGATIVE_INFINITY) {
|
|
61100
|
-
healthy = true;
|
|
61101
|
-
reseedAfterOutage().catch(fault);
|
|
61102
|
-
}
|
|
61398
|
+
const out = await deadline(redis);
|
|
61103
61399
|
healthy = true;
|
|
61400
|
+
if (lostDebit) {
|
|
61401
|
+
lostDebit = false;
|
|
61402
|
+
reseedAfterOutage().catch((error61) => {
|
|
61403
|
+
lostDebit = true;
|
|
61404
|
+
fault(error61);
|
|
61405
|
+
});
|
|
61406
|
+
}
|
|
61104
61407
|
return out;
|
|
61105
61408
|
} catch (error61) {
|
|
61106
61409
|
fault(error61);
|
|
@@ -61146,6 +61449,7 @@ function redisCoord(deps) {
|
|
|
61146
61449
|
subscriber.connect().catch(fault);
|
|
61147
61450
|
return {
|
|
61148
61451
|
healthy: () => healthy,
|
|
61452
|
+
faults: () => faults,
|
|
61149
61453
|
close() {
|
|
61150
61454
|
subscriber.close();
|
|
61151
61455
|
client.close();
|
|
@@ -61192,7 +61496,10 @@ function redisCoord(deps) {
|
|
|
61192
61496
|
add(name, grainMs, windowMs, at, delta2) {
|
|
61193
61497
|
return attempt2(async () => {
|
|
61194
61498
|
await evalScript(BUCKETS_ADD, [key("b", name)], [at, grainMs, windowMs, delta2.requests, delta2.tokens, delta2.costUsd]);
|
|
61195
|
-
}, () =>
|
|
61499
|
+
}, () => {
|
|
61500
|
+
lostDebit = true;
|
|
61501
|
+
return fallback.buckets.add(name, grainMs, windowMs, at, delta2);
|
|
61502
|
+
});
|
|
61196
61503
|
},
|
|
61197
61504
|
sum(name, grainMs, windowMs, at) {
|
|
61198
61505
|
return attempt2(async () => {
|
|
@@ -61231,14 +61538,15 @@ function redisCoord(deps) {
|
|
|
61231
61538
|
async withLock(name, ttlMs, waitMs, fn) {
|
|
61232
61539
|
const full = key("lock", name);
|
|
61233
61540
|
const token = crypto.randomUUID();
|
|
61234
|
-
const
|
|
61541
|
+
const deadline2 = performance.now() + waitMs;
|
|
61235
61542
|
for (;; ) {
|
|
61236
61543
|
const taken = await attempt2(async () => await client.set(full, token, "PX", String(ttlMs), "NX") === "OK", async () => {
|
|
61544
|
+
evalScript(MUTEX_RELEASE, [full], [token]).catch(() => {});
|
|
61237
61545
|
throw new LockUnavailable(name);
|
|
61238
61546
|
});
|
|
61239
61547
|
if (taken)
|
|
61240
61548
|
break;
|
|
61241
|
-
if (performance.now() >=
|
|
61549
|
+
if (performance.now() >= deadline2)
|
|
61242
61550
|
throw new LockUnavailable(name);
|
|
61243
61551
|
await Bun.sleep(LOCK_POLL_MS);
|
|
61244
61552
|
}
|
|
@@ -61274,17 +61582,17 @@ function redisCoord(deps) {
|
|
|
61274
61582
|
}
|
|
61275
61583
|
},
|
|
61276
61584
|
pubsub: {
|
|
61277
|
-
publish(topic, payload) {
|
|
61585
|
+
async publish(topic, payload) {
|
|
61586
|
+
await listened;
|
|
61278
61587
|
return attempt2(async () => {
|
|
61279
|
-
await listened;
|
|
61280
61588
|
if (!listening)
|
|
61281
61589
|
throw new Error("not listening");
|
|
61282
61590
|
await client.publish(CHANNEL2, JSON.stringify({ topic, payload }));
|
|
61283
61591
|
}, () => fallback.pubsub.publish(topic, payload));
|
|
61284
61592
|
},
|
|
61285
|
-
publishSequenced(topic, payload, seqKey) {
|
|
61593
|
+
async publishSequenced(topic, payload, seqKey) {
|
|
61594
|
+
await listened;
|
|
61286
61595
|
return attempt2(async () => {
|
|
61287
|
-
await listened;
|
|
61288
61596
|
if (!listening)
|
|
61289
61597
|
throw new Error("not listening");
|
|
61290
61598
|
return await evalScript(PUBLISH_SEQUENCED, [key("seq", seqKey)], [CHANNEL2, topic, payload]);
|
|
@@ -61378,13 +61686,13 @@ async function underLease(lease, name, ttlMs, fn) {
|
|
|
61378
61686
|
|
|
61379
61687
|
// apps/gateway/src/maintenance.ts
|
|
61380
61688
|
var SWEEP_INTERVAL_MS2 = 60 * 60 * 1000;
|
|
61381
|
-
var
|
|
61689
|
+
var DAY_MS4 = 24 * 60 * 60 * 1000;
|
|
61382
61690
|
var ROLLUP_RETENTION_DAYS = 400;
|
|
61383
61691
|
async function pruneLogs(store, now) {
|
|
61384
61692
|
const settings = await store.config.getSettings();
|
|
61385
|
-
const rawHorizon = now - settings.logRetentionDays *
|
|
61693
|
+
const rawHorizon = now - settings.logRetentionDays * DAY_MS4;
|
|
61386
61694
|
const raw = await store.usage.prune(rawHorizon);
|
|
61387
|
-
const daily = await store.usage.pruneDaily(now - ROLLUP_RETENTION_DAYS *
|
|
61695
|
+
const daily = await store.usage.pruneDaily(now - ROLLUP_RETENTION_DAYS * DAY_MS4);
|
|
61388
61696
|
const quotaSamples = await store.credentials.pruneQuotaSamples(rawHorizon);
|
|
61389
61697
|
const bodies = await store.bodies.prune(rawHorizon);
|
|
61390
61698
|
const bodiesOverCap = await store.bodies.pruneToCap();
|
|
@@ -61455,7 +61763,7 @@ async function sweep(deps) {
|
|
|
61455
61763
|
let touched = 0;
|
|
61456
61764
|
for (const credential of due(credentials, deps.now())) {
|
|
61457
61765
|
if (!credential.hasRefreshToken) {
|
|
61458
|
-
if (credential
|
|
61766
|
+
if (credentialExpired(credential, deps.now())) {
|
|
61459
61767
|
await deps.store.credentials.update(credential.id, {
|
|
61460
61768
|
enabled: false,
|
|
61461
61769
|
disabledReason: "expiredNoRefresh",
|
|
@@ -62147,6 +62455,10 @@ var DEFINITIONS = {
|
|
|
62147
62455
|
help: "Whether this process is using coordination fallback.",
|
|
62148
62456
|
type: "gauge"
|
|
62149
62457
|
},
|
|
62458
|
+
omni_coord_faults_total: {
|
|
62459
|
+
help: "Coordinator calls this process answered from memory, timeouts included.",
|
|
62460
|
+
type: "counter"
|
|
62461
|
+
},
|
|
62150
62462
|
omni_metrics_series_folded_total: {
|
|
62151
62463
|
help: "Metric series folded into api_key_id other.",
|
|
62152
62464
|
type: "counter"
|
|
@@ -62392,13 +62704,14 @@ function createTelemetry(opts) {
|
|
|
62392
62704
|
if (opts.metricsEnabled)
|
|
62393
62705
|
registry2.add("omni_ratelimit_rejected_total", { dimension, window: window2 ?? "none" });
|
|
62394
62706
|
},
|
|
62395
|
-
scrape(localInflight, streams, coordFallback) {
|
|
62707
|
+
scrape(localInflight, streams, coordFallback, coordFaults) {
|
|
62396
62708
|
for (const [provider, value] of localInflight)
|
|
62397
62709
|
registry2.set("omni_inflight", { provider }, value);
|
|
62398
62710
|
registry2.set("omni_stream_connections", {}, streams.connections);
|
|
62399
62711
|
registry2.set("omni_stream_queued", {}, streams.queued);
|
|
62400
62712
|
registry2.set("omni_stream_dropped_total", {}, streams.dropped);
|
|
62401
62713
|
registry2.set("omni_coord_fallback", {}, coordFallback ? 1 : 0);
|
|
62714
|
+
registry2.set("omni_coord_faults_total", {}, coordFaults);
|
|
62402
62715
|
return renderPrometheus(registry2.snapshot());
|
|
62403
62716
|
},
|
|
62404
62717
|
stop() {
|
|
@@ -62450,7 +62763,12 @@ async function main() {
|
|
|
62450
62763
|
if (config2.logLevelFallbackFrom !== null) {
|
|
62451
62764
|
logger2.info("invalid log level; using info", { reason: config2.logLevelFallbackFrom });
|
|
62452
62765
|
}
|
|
62453
|
-
logger2.info("omnigateway booting",
|
|
62766
|
+
logger2.info(config2.dayOffsetFallbackFrom === null ? "omnigateway booting" : "omnigateway booting; invalid day offset, using host offset", {
|
|
62767
|
+
host: config2.host,
|
|
62768
|
+
port: config2.port,
|
|
62769
|
+
dayOffsetMinutes: config2.dayOffsetMinutes,
|
|
62770
|
+
...config2.dayOffsetFallbackFrom === null ? {} : { reason: config2.dayOffsetFallbackFrom }
|
|
62771
|
+
});
|
|
62454
62772
|
function dashboardDir() {
|
|
62455
62773
|
if (config2.staticDir !== null)
|
|
62456
62774
|
return config2.staticDir;
|
|
@@ -62467,7 +62785,8 @@ async function main() {
|
|
|
62467
62785
|
...config2.clusterMode && config2.databaseUrl !== null ? { url: config2.databaseUrl } : { path: config2.databasePath },
|
|
62468
62786
|
encryptionKey,
|
|
62469
62787
|
logger: logger2,
|
|
62470
|
-
nodeId
|
|
62788
|
+
nodeId,
|
|
62789
|
+
dayOffsetMinutes: config2.dayOffsetMinutes
|
|
62471
62790
|
});
|
|
62472
62791
|
await store.maintenance.heartbeat(now());
|
|
62473
62792
|
const swept = await store.usage.sweepPending(now());
|
|
@@ -62507,7 +62826,7 @@ async function main() {
|
|
|
62507
62826
|
now,
|
|
62508
62827
|
onDetach: (id, topics) => pluginChannelsRef?.closed(id, topics)
|
|
62509
62828
|
});
|
|
62510
|
-
const streamRing = createRing(
|
|
62829
|
+
const streamRing = createRing();
|
|
62511
62830
|
const broadcaster = createBroadcaster({
|
|
62512
62831
|
registry: streamRegistry,
|
|
62513
62832
|
ring: streamRing,
|
|
@@ -62544,7 +62863,7 @@ async function main() {
|
|
|
62544
62863
|
coord,
|
|
62545
62864
|
nodeId,
|
|
62546
62865
|
mode: config2.clusterMode ? "cluster" : "single",
|
|
62547
|
-
...shared === null ? {} : { coordHealthy: shared.healthy },
|
|
62866
|
+
...shared === null ? {} : { coordHealthy: shared.healthy, coordFaults: shared.faults },
|
|
62548
62867
|
baseUrl: config2.baseUrl,
|
|
62549
62868
|
http,
|
|
62550
62869
|
now,
|
|
@@ -62559,6 +62878,7 @@ async function main() {
|
|
|
62559
62878
|
telemetry,
|
|
62560
62879
|
...config2.metricsToken === null ? {} : { metricsToken: config2.metricsToken },
|
|
62561
62880
|
bodyLoggingAllowed: config2.bodyLoggingAllowed,
|
|
62881
|
+
dayOffsetMinutes: config2.dayOffsetMinutes,
|
|
62562
62882
|
plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
|
|
62563
62883
|
pluginUi: loadedPlugins.plugins,
|
|
62564
62884
|
...loadedPlugins.plugins.length === 0 ? {} : { emit: (event) => pluginEvents.emitRequestCompleted(event) },
|