omnigateway 0.1.3 → 0.1.5

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.
Files changed (34) hide show
  1. package/bin/omni.js +101 -37
  2. package/gateway.js +332 -227
  3. package/package.json +1 -1
  4. package/public/assets/{Chip-D0YiqYkz.js → Chip-Ccqsb_Mq.js} +2 -2
  5. package/public/assets/{CopyValue-bzKz2tiJ.js → CopyValue-CvEMy9Zo.js} +5 -5
  6. package/public/assets/{Field-DdscQ9OO.js → Field-Devm4K46.js} +8 -8
  7. package/public/assets/Lamp-BXSUIAtG.js +25 -0
  8. package/public/assets/{Meter-Bm1oQE2U.js → Meter-Fn8X7t60.js} +2 -2
  9. package/public/assets/{Modal-Crr8uyWt.js → Modal-C4y9zALa.js} +8 -8
  10. package/public/assets/{Rack-uDxrjvaz.js → Rack-CRQSwSeB.js} +18 -18
  11. package/public/assets/{Readout-CYg6UOop.js → Readout-BKpOZIGX.js} +5 -5
  12. package/public/assets/{States-DPTTYUHL.js → States-DBzsiHjQ.js} +10 -10
  13. package/public/assets/{Table-LkPaHBsj.js → Table-DPDJeeVc.js} +1 -1
  14. package/public/assets/{Toggle-D0e4C94X.js → Toggle-CBq5XAMU.js} +3 -3
  15. package/public/assets/_app-VCodWes4.js +1 -0
  16. package/public/assets/_app.accounts-BP3L84us.js +51 -0
  17. package/public/assets/_app.index-C5Vd-zlp.js +61 -0
  18. package/public/assets/{_app.keys-BNEOjtbu.js → _app.keys-0e1BHrIZ.js} +8 -8
  19. package/public/assets/_app.logs-DNSND96R.js +33 -0
  20. package/public/assets/_app.models-DzFIbnem.js +144 -0
  21. package/public/assets/{_app.settings-C1gB4w7c.js → _app.settings-Du5Nsp9f.js} +6 -6
  22. package/public/assets/{_app.usage-B0iHKzmI.js → _app.usage-DKq4Hsia.js} +28 -28
  23. package/public/assets/index-BYyeRTQJ.js +170 -0
  24. package/public/assets/{login-D-FsWNX8.js → login-Gyf-1fSa.js} +8 -8
  25. package/public/assets/{queries-vY5lJqBe.js → queries-BMwc6EvM.js} +1 -1
  26. package/public/assets/{trash-2-Cep_aVLx.js → trash-2-DFVxurNH.js} +1 -1
  27. package/public/index.html +3 -3
  28. package/public/assets/Lamp-WOotyOTd.js +0 -15
  29. package/public/assets/_app-OSvB-Dv9.js +0 -1
  30. package/public/assets/_app.accounts-BOE49WyS.js +0 -51
  31. package/public/assets/_app.index-CTRlqxOq.js +0 -61
  32. package/public/assets/_app.logs-BxxqeEhM.js +0 -18
  33. package/public/assets/_app.models-BJi-RGUO.js +0 -144
  34. package/public/assets/index--dA3FFSN.js +0 -170
package/gateway.js CHANGED
@@ -22634,6 +22634,27 @@ ALTER TABLE credentials ADD COLUMN disabled_reason TEXT;
22634
22634
  ALTER TABLE credentials ADD COLUMN disabled_at INTEGER;
22635
22635
  `;
22636
22636
 
22637
+ // packages/store/src/sqlite/migrations/004_request_state.sql
22638
+ var _004_request_state_default = `-- Whether a request is still running.
22639
+ --
22640
+ -- A row used to be written once, after the response stream drained, so a long
22641
+ -- stream was invisible for its whole life and the console showed an idle
22642
+ -- gateway. A row is now written twice: \`pending\` when dispatch starts, \`done\`
22643
+ -- when it finishes.
22644
+ --
22645
+ -- Existing rows default to 'done'. Every row that predates this migration
22646
+ -- describes a request that has already ended, by definition.
22647
+ ALTER TABLE request_logs ADD COLUMN state TEXT NOT NULL DEFAULT 'done';
22648
+
22649
+ -- \`status\` and \`duration_ms\` stay NOT NULL: making them nullable means a table
22650
+ -- rebuild in SQLite, and a pending row's zeros are never read. Readers key off
22651
+ -- \`state\` alone.
22652
+ --
22653
+ -- Partial, because pending rows are a handful at any moment while the table
22654
+ -- holds a month of finished ones.
22655
+ CREATE INDEX idx_request_logs_pending ON request_logs(state) WHERE state = 'pending';
22656
+ `;
22657
+
22637
22658
  // packages/store/src/sqlite/rollup.ts
22638
22659
  function startOfLocalDay(at) {
22639
22660
  const day = new Date(at);
@@ -22740,7 +22761,8 @@ function backfillDaily(db) {
22740
22761
  var MIGRATIONS = [
22741
22762
  { id: 1, sql: _001_init_default },
22742
22763
  { id: 2, sql: _002_usage_daily_default, after: backfillDaily },
22743
- { id: 3, sql: _003_quota_snapshot_default }
22764
+ { id: 3, sql: _003_quota_snapshot_default },
22765
+ { id: 4, sql: _004_request_state_default }
22744
22766
  ];
22745
22767
  function openDb(path) {
22746
22768
  const db = new Database(path, { create: true });
@@ -22812,6 +22834,7 @@ function createKeyRepo(db) {
22812
22834
  // packages/store/src/sqlite/usage.ts
22813
22835
  var toLog = (r) => ({
22814
22836
  id: r.id,
22837
+ state: r.state === "pending" ? "pending" : "done",
22815
22838
  at: r.at,
22816
22839
  apiKeyId: r.api_key_id,
22817
22840
  requestedModel: r.requested_model,
@@ -22870,37 +22893,78 @@ function label(value) {
22870
22893
  const text = String(value);
22871
22894
  return text.length === 0 ? "unknown" : text;
22872
22895
  }
22896
+ var COLUMNS = `(id, state, at, api_key_id, requested_model, resolved_provider, resolved_model,
22897
+ credential_id, attempts, status, error_code, input_tokens, output_tokens,
22898
+ cache_read_tokens, cache_write_tokens, ttft_ms, duration_ms, cost_usd,
22899
+ degradations)`;
22900
+ var PLACEHOLDERS = "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
22901
+ function values(log, state) {
22902
+ return [
22903
+ log.id,
22904
+ state,
22905
+ log.at,
22906
+ log.apiKeyId,
22907
+ log.requestedModel,
22908
+ log.resolvedProvider,
22909
+ log.resolvedModel,
22910
+ log.credentialId,
22911
+ log.attempts,
22912
+ log.status,
22913
+ log.errorCode,
22914
+ log.inputTokens,
22915
+ log.outputTokens,
22916
+ log.cacheReadTokens,
22917
+ log.cacheWriteTokens,
22918
+ log.ttftMs,
22919
+ log.durationMs,
22920
+ log.costUsd,
22921
+ JSON.stringify(log.degradations)
22922
+ ];
22923
+ }
22924
+ var COMPLETE = `INSERT INTO request_logs ${COLUMNS} VALUES ${PLACEHOLDERS}
22925
+ ON CONFLICT(id) DO UPDATE SET
22926
+ state = 'done',
22927
+ requested_model = COALESCE(NULLIF(excluded.requested_model, ''), request_logs.requested_model),
22928
+ api_key_id = COALESCE(excluded.api_key_id, request_logs.api_key_id),
22929
+ resolved_provider = excluded.resolved_provider,
22930
+ resolved_model = excluded.resolved_model,
22931
+ credential_id = excluded.credential_id,
22932
+ attempts = excluded.attempts,
22933
+ status = excluded.status,
22934
+ error_code = excluded.error_code,
22935
+ input_tokens = excluded.input_tokens,
22936
+ output_tokens = excluded.output_tokens,
22937
+ cache_read_tokens = excluded.cache_read_tokens,
22938
+ cache_write_tokens = excluded.cache_write_tokens,
22939
+ ttft_ms = excluded.ttft_ms,
22940
+ duration_ms = excluded.duration_ms,
22941
+ cost_usd = excluded.cost_usd,
22942
+ degradations = excluded.degradations`;
22873
22943
  function createUsageRepo(db) {
22874
- const insert = db.transaction((log) => {
22875
- db.run(`INSERT INTO request_logs
22876
- (id, at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
22877
- attempts, status, error_code, input_tokens, output_tokens, cache_read_tokens,
22878
- cache_write_tokens, ttft_ms, duration_ms, cost_usd, degradations)
22879
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
22880
- log.id,
22881
- log.at,
22882
- log.apiKeyId,
22883
- log.requestedModel,
22884
- log.resolvedProvider,
22885
- log.resolvedModel,
22886
- log.credentialId,
22887
- log.attempts,
22888
- log.status,
22889
- log.errorCode,
22890
- log.inputTokens,
22891
- log.outputTokens,
22892
- log.cacheReadTokens,
22893
- log.cacheWriteTokens,
22894
- log.ttftMs,
22895
- log.durationMs,
22896
- log.costUsd,
22897
- JSON.stringify(log.degradations)
22898
- ]);
22899
- rollupLog(db, log);
22944
+ const complete = db.transaction((log) => {
22945
+ db.run(COMPLETE, values(log, "done"));
22946
+ const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
22947
+ if (stored !== null)
22948
+ rollupLog(db, toLog(stored));
22900
22949
  });
22901
22950
  return {
22951
+ async begin(log) {
22952
+ db.run(`INSERT INTO request_logs ${COLUMNS} VALUES ${PLACEHOLDERS}`, values(log, "pending"));
22953
+ },
22954
+ async route(id, target) {
22955
+ db.run(`UPDATE request_logs
22956
+ SET resolved_provider = ?, resolved_model = ?, credential_id = ?
22957
+ WHERE id = ? AND state = 'pending'`, [target.provider, target.model, target.credentialId, id]);
22958
+ },
22902
22959
  async append(log) {
22903
- insert(log);
22960
+ complete(log);
22961
+ },
22962
+ async sweepPending() {
22963
+ const stale = db.query("SELECT * FROM request_logs WHERE state = 'pending'").all().map(toLog);
22964
+ for (const log of stale) {
22965
+ complete({ ...log, status: 499, errorCode: "interrupted", durationMs: 0 });
22966
+ }
22967
+ return stale.length;
22904
22968
  },
22905
22969
  async recent(limit) {
22906
22970
  return db.query("SELECT * FROM request_logs ORDER BY at DESC LIMIT ?").all(limit).map(toLog);
@@ -22922,7 +22986,7 @@ function createUsageRepo(db) {
22922
22986
  COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
22923
22987
  COALESCE(SUM(cost_usd), 0) AS cost_usd
22924
22988
  FROM ${daily ? "usage_daily" : "request_logs"}
22925
- WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?
22989
+ WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= ? AND ${timeColumn} <= ?
22926
22990
  GROUP BY key${split === null ? "" : ", split"}
22927
22991
  ORDER BY requests DESC`).all(since, until);
22928
22992
  return rows.map((r) => {
@@ -31467,17 +31531,17 @@ var ElysiaType = {
31467
31531
  elysiaMeta: "ArrayString"
31468
31532
  })).Decode((value) => {
31469
31533
  if (Array.isArray(value)) {
31470
- let values = [];
31534
+ let values2 = [];
31471
31535
  for (let i = 0;i < value.length; i++) {
31472
31536
  const v = value[i];
31473
31537
  if (typeof v == "string") {
31474
31538
  const t2 = decode4(v, true);
31475
- Array.isArray(t2) ? values = values.concat(t2) : values.push(t2);
31539
+ Array.isArray(t2) ? values2 = values2.concat(t2) : values2.push(t2);
31476
31540
  continue;
31477
31541
  }
31478
- values.push(v);
31542
+ values2.push(v);
31479
31543
  }
31480
- return values;
31544
+ return values2;
31481
31545
  }
31482
31546
  return typeof value == "string" ? decode4(value) : value;
31483
31547
  }).Encode((value) => {
@@ -31498,17 +31562,17 @@ var ElysiaType = {
31498
31562
  elysiaMeta: "ArrayQuery"
31499
31563
  })).Decode((value) => {
31500
31564
  if (Array.isArray(value)) {
31501
- let values = [];
31565
+ let values2 = [];
31502
31566
  for (let i = 0;i < value.length; i++) {
31503
31567
  const v = value[i];
31504
31568
  if (typeof v == "string") {
31505
31569
  const t2 = decode4(v);
31506
- Array.isArray(t2) ? values = values.concat(t2) : values.push(t2);
31570
+ Array.isArray(t2) ? values2 = values2.concat(t2) : values2.push(t2);
31507
31571
  continue;
31508
31572
  }
31509
- values.push(v);
31573
+ values2.push(v);
31510
31574
  }
31511
- return values;
31575
+ return values2;
31512
31576
  }
31513
31577
  return typeof value == "string" ? decode4(value) : value;
31514
31578
  }).Encode((value) => {
@@ -31552,16 +31616,16 @@ var ElysiaType = {
31552
31616
  sign
31553
31617
  }, v;
31554
31618
  },
31555
- UnionEnum: (values, options = {}) => {
31556
- const type = values.every((value) => typeof value == "string") ? { type: "string" } : values.every((value) => typeof value == "number") ? { type: "number" } : values.every((value) => value === null) ? { type: "null" } : {};
31557
- if (values.some((x) => typeof x == "object" && x !== null))
31619
+ UnionEnum: (values2, options = {}) => {
31620
+ const type = values2.every((value) => typeof value == "string") ? { type: "string" } : values2.every((value) => typeof value == "number") ? { type: "number" } : values2.every((value) => value === null) ? { type: "null" } : {};
31621
+ if (values2.some((x) => typeof x == "object" && x !== null))
31558
31622
  throw new Error("This type does not support objects or arrays");
31559
31623
  return {
31560
- default: values[0],
31624
+ default: values2[0],
31561
31625
  ...options,
31562
31626
  [Kind]: "UnionEnum",
31563
31627
  ...type,
31564
- enum: values
31628
+ enum: values2
31565
31629
  };
31566
31630
  },
31567
31631
  NoValidate: (v, enabled = true) => (v.noValidate = enabled, v),
@@ -34007,33 +34071,33 @@ var getSchemaValidator = (s, {
34007
34071
  return v.then((v2) => Check22(v2, true));
34008
34072
  if (v.issues)
34009
34073
  return v;
34010
- const values = [];
34011
- return v && typeof v == "object" && values.push(v.value), runCheckers2(value, 0, values, v);
34012
- }, runCheckers2 = function(value, startIndex, values, lastV) {
34074
+ const values2 = [];
34075
+ return v && typeof v == "object" && values2.push(v.value), runCheckers2(value, 0, values2, v);
34076
+ }, runCheckers2 = function(value, startIndex, values2, lastV) {
34013
34077
  for (let i = startIndex;i < checkers.length; i++) {
34014
34078
  let v = checkers[i].validate(value);
34015
34079
  if (v instanceof Promise)
34016
34080
  return v.then((resolved) => {
34017
34081
  if (resolved.issues)
34018
34082
  return resolved;
34019
- const nextValues = [...values];
34083
+ const nextValues = [...values2];
34020
34084
  return resolved && typeof resolved == "object" && nextValues.push(resolved.value), runCheckers2(value, i + 1, nextValues, resolved);
34021
34085
  });
34022
34086
  if (v.issues)
34023
34087
  return v;
34024
- v && typeof v == "object" && values.push(v.value), lastV = v;
34088
+ v && typeof v == "object" && values2.push(v.value), lastV = v;
34025
34089
  }
34026
- return mergeValues22(values, lastV);
34027
- }, mergeValues22 = function(values, lastV) {
34028
- if (!values.length)
34090
+ return mergeValues22(values2, lastV);
34091
+ }, mergeValues22 = function(values2, lastV) {
34092
+ if (!values2.length)
34029
34093
  return { value: lastV };
34030
- if (values.length === 1)
34031
- return { value: values[0] };
34032
- if (values.length === 2)
34033
- return { value: mergeDeep(values[0], values[1]) };
34034
- let newValue = mergeDeep(values[0], values[1]);
34035
- for (let i = 2;i < values.length; i++)
34036
- newValue = mergeDeep(newValue, values[i]);
34094
+ if (values2.length === 1)
34095
+ return { value: values2[0] };
34096
+ if (values2.length === 2)
34097
+ return { value: mergeDeep(values2[0], values2[1]) };
34098
+ let newValue = mergeDeep(values2[0], values2[1]);
34099
+ for (let i = 2;i < values2.length; i++)
34100
+ newValue = mergeDeep(newValue, values2[i]);
34037
34101
  return { value: newValue };
34038
34102
  };
34039
34103
  var Check2 = Check22, runCheckers = runCheckers2, mergeValues2 = mergeValues22;
@@ -37924,7 +37988,18 @@ function parseDataUrl(url2) {
37924
37988
  return { mediaType: match[1], data: match[2] };
37925
37989
  }
37926
37990
 
37927
- // apps/gateway/src/routes/admin.ts
37991
+ // apps/gateway/src/routes/http.ts
37992
+ function readCookie(request2, name) {
37993
+ const header = request2.headers.get("cookie");
37994
+ if (header === null)
37995
+ return null;
37996
+ for (const part of header.split(";")) {
37997
+ const [key, ...rest] = part.trim().split("=");
37998
+ if (key === name)
37999
+ return rest.join("=");
38000
+ }
38001
+ return null;
38002
+ }
37928
38003
  function sessionCookie(request2, token, maxAge) {
37929
38004
  const secure = new URL(request2.url).protocol === "https:" ? ["Secure"] : [];
37930
38005
  return [
@@ -37936,38 +38011,36 @@ function sessionCookie(request2, token, maxAge) {
37936
38011
  `Max-Age=${maxAge}`
37937
38012
  ].join("; ");
37938
38013
  }
37939
- function readCookie(request2, name) {
37940
- const header = request2.headers.get("cookie");
37941
- if (header === null)
37942
- return null;
37943
- for (const part of header.split(";")) {
37944
- const [key, ...rest] = part.trim().split("=");
37945
- if (key === name)
37946
- return rest.join("=");
38014
+ async function requireAdmin(request2, admin) {
38015
+ const token = readCookie(request2, ADMIN_COOKIE);
38016
+ if (token === null || !await admin.verify(token)) {
38017
+ throw new GatewayError("AUTH", "admin session required");
37947
38018
  }
37948
- return null;
37949
38019
  }
37950
- async function jsonRecord(request2) {
38020
+ async function readJson(request2) {
37951
38021
  try {
37952
- const body2 = await request2.json();
37953
- return isRecord3(body2) ? body2 : null;
37954
- } catch {
37955
- return null;
38022
+ return await request2.json();
38023
+ } catch (error51) {
38024
+ if (!(error51 instanceof SyntaxError))
38025
+ throw error51;
38026
+ throw new GatewayError("BAD_REQUEST", "invalid JSON body");
37956
38027
  }
37957
38028
  }
38029
+ async function readJsonRecord(request2) {
38030
+ const body2 = await readJson(request2);
38031
+ return isRecord3(body2) ? body2 : null;
38032
+ }
38033
+ function apiErrorResponse(error51) {
38034
+ const gatewayError = error51 instanceof GatewayError ? error51 : new GatewayError("INTERNAL", "internal error");
38035
+ return Response.json({ error: { code: gatewayError.code, message: gatewayError.message } }, { status: HTTP_STATUS[gatewayError.code] });
38036
+ }
38037
+ function apiErrorHandler(input) {
38038
+ return input.code === "NOT_FOUND" ? undefined : apiErrorResponse(input.error);
38039
+ }
38040
+
38041
+ // apps/gateway/src/routes/admin.ts
37958
38042
  function adminRoutes(deps) {
37959
- const app = new Elysia().onError(({ error: error51, set: set2 }) => {
37960
- const gatewayError = error51 instanceof GatewayError ? error51 : new GatewayError("INTERNAL", "internal error");
37961
- set2.status = HTTP_STATUS[gatewayError.code];
37962
- return { error: { code: gatewayError.code, message: gatewayError.message } };
37963
- });
37964
- async function requireAdmin(request2) {
37965
- const token = readCookie(request2, ADMIN_COOKIE);
37966
- if (token === null || !await deps.admin.verify(token)) {
37967
- throw new GatewayError("AUTH", "admin session required");
37968
- }
37969
- }
37970
- return app.get("/api/status", async ({ request: request2 }) => {
38043
+ return new Elysia().onError(apiErrorHandler).get("/api/status", async ({ request: request2 }) => {
37971
38044
  const token = readCookie(request2, ADMIN_COOKIE);
37972
38045
  return {
37973
38046
  configured: await deps.admin.isConfigured(),
@@ -37978,7 +38051,7 @@ function adminRoutes(deps) {
37978
38051
  set2.status = 409;
37979
38052
  return { error: { code: "CONFLICT", message: "an admin password is already configured" } };
37980
38053
  }
37981
- const body2 = await jsonRecord(request2);
38054
+ const body2 = await readJsonRecord(request2);
37982
38055
  if (typeof body2?.password !== "string") {
37983
38056
  throw new GatewayError("BAD_REQUEST", "password is required");
37984
38057
  }
@@ -37998,7 +38071,7 @@ function adminRoutes(deps) {
37998
38071
  set2.headers["set-cookie"] = sessionCookie(request2, token, Math.floor(deps.sessionTtlMs / 1000));
37999
38072
  return { ok: true };
38000
38073
  }).post("/api/login", async ({ request: request2, set: set2 }) => {
38001
- const body2 = await jsonRecord(request2);
38074
+ const body2 = await readJsonRecord(request2);
38002
38075
  const token = typeof body2?.password === "string" ? await deps.admin.login(body2.password) : null;
38003
38076
  if (token === null)
38004
38077
  throw new GatewayError("AUTH", "invalid password");
@@ -38011,52 +38084,52 @@ function adminRoutes(deps) {
38011
38084
  set2.headers["set-cookie"] = sessionCookie(request2, "", 0);
38012
38085
  return { ok: true };
38013
38086
  }).get("/api/credentials", async ({ request: request2 }) => {
38014
- await requireAdmin(request2);
38087
+ await requireAdmin(request2, deps.admin);
38015
38088
  return { credentials: await listCredentials(deps.store) };
38016
38089
  }).get("/api/credentials/health", async ({ request: request2 }) => {
38017
- await requireAdmin(request2);
38090
+ await requireAdmin(request2, deps.admin);
38018
38091
  return credentialHealth(deps.store);
38019
38092
  }).patch("/api/credentials/:id", async ({ request: request2, params }) => {
38020
- await requireAdmin(request2);
38021
- await patchCredential(deps, params.id, await request2.json());
38093
+ await requireAdmin(request2, deps.admin);
38094
+ await patchCredential(deps, params.id, await readJson(request2));
38022
38095
  return { ok: true };
38023
38096
  }).delete("/api/credentials/:id", async ({ request: request2, params }) => {
38024
- await requireAdmin(request2);
38097
+ await requireAdmin(request2, deps.admin);
38025
38098
  await removeCredential(deps.store, params.id);
38026
38099
  return { ok: true };
38027
38100
  }).get("/api/models", async ({ request: request2 }) => {
38028
- await requireAdmin(request2);
38101
+ await requireAdmin(request2, deps.admin);
38029
38102
  return { models: await listModels(deps.store) };
38030
38103
  }).put("/api/models/:id", async ({ request: request2, params }) => {
38031
- await requireAdmin(request2);
38032
- await putModel(deps.store, params.id, await request2.json());
38104
+ await requireAdmin(request2, deps.admin);
38105
+ await putModel(deps.store, params.id, await readJson(request2));
38033
38106
  return { ok: true };
38034
38107
  }).delete("/api/models/:id", async ({ request: request2, params }) => {
38035
- await requireAdmin(request2);
38108
+ await requireAdmin(request2, deps.admin);
38036
38109
  await removeModel(deps.store, params.id);
38037
38110
  return { ok: true };
38038
38111
  }).get("/api/keys", async ({ request: request2 }) => {
38039
- await requireAdmin(request2);
38112
+ await requireAdmin(request2, deps.admin);
38040
38113
  return { keys: await listKeys(deps.store) };
38041
38114
  }).post("/api/keys", async ({ request: request2 }) => {
38042
- await requireAdmin(request2);
38043
- return createKey(deps.store, await request2.json());
38115
+ await requireAdmin(request2, deps.admin);
38116
+ return createKey(deps.store, await readJson(request2));
38044
38117
  }).delete("/api/keys/:id", async ({ request: request2, params }) => {
38045
- await requireAdmin(request2);
38118
+ await requireAdmin(request2, deps.admin);
38046
38119
  await revokeKey(deps.store, params.id);
38047
38120
  return { ok: true };
38048
38121
  }).get("/api/settings", async ({ request: request2 }) => {
38049
- await requireAdmin(request2);
38122
+ await requireAdmin(request2, deps.admin);
38050
38123
  return { settings: await getSettings(deps.store) };
38051
38124
  }).put("/api/settings", async ({ request: request2 }) => {
38052
- await requireAdmin(request2);
38053
- await putSettings(deps.store, await request2.json());
38125
+ await requireAdmin(request2, deps.admin);
38126
+ await putSettings(deps.store, await readJson(request2));
38054
38127
  return { ok: true };
38055
38128
  }).post("/api/models/:id/dry-run", async ({ request: request2, params }) => {
38056
- await requireAdmin(request2);
38057
- return dryRun(deps, params.id, await request2.json());
38129
+ await requireAdmin(request2, deps.admin);
38130
+ return dryRun(deps, params.id, await readJson(request2));
38058
38131
  }).get("/api/usage", async ({ request: request2, query }) => {
38059
- await requireAdmin(request2);
38132
+ await requireAdmin(request2, deps.admin);
38060
38133
  return {
38061
38134
  rows: await queryUsage(deps, {
38062
38135
  grain: query.grain,
@@ -38067,70 +38140,29 @@ function adminRoutes(deps) {
38067
38140
  })
38068
38141
  };
38069
38142
  }).get("/api/logs", async ({ request: request2, query }) => {
38070
- await requireAdmin(request2);
38143
+ await requireAdmin(request2, deps.admin);
38071
38144
  return { logs: await recentLogs(deps.store, query.limit) };
38072
38145
  });
38073
38146
  }
38074
38147
 
38075
38148
  // apps/gateway/src/routes/connect.ts
38076
- function asGatewayError(error51) {
38077
- if (error51 instanceof GatewayError)
38078
- return error51;
38079
- return new GatewayError("INTERNAL", error51 instanceof Error ? error51.message : "internal error");
38080
- }
38081
- function errorResponse(error51) {
38082
- const gatewayError = asGatewayError(error51);
38083
- return new Response(JSON.stringify({ error: { code: gatewayError.code, message: gatewayError.message } }), {
38084
- status: HTTP_STATUS[gatewayError.code],
38085
- headers: { "content-type": "application/json" }
38086
- });
38087
- }
38088
- function readCookie2(request2, name) {
38089
- const header = request2.headers.get("cookie");
38090
- if (header === null)
38091
- return null;
38092
- for (const part of header.split(";")) {
38093
- const [key, ...rest] = part.trim().split("=");
38094
- if (key === name)
38095
- return rest.join("=");
38096
- }
38097
- return null;
38098
- }
38099
38149
  function connectRoutes(deps) {
38100
38150
  const flows = createConnectFlows(deps);
38101
- async function requireAdmin(request2) {
38102
- const token = readCookie2(request2, ADMIN_COOKIE);
38103
- if (token === null || !await deps.admin.verify(token)) {
38104
- throw new GatewayError("AUTH", "admin session required");
38105
- }
38106
- }
38107
- return new Elysia().post("/api/connect/start", async ({ request: request2 }) => {
38108
- try {
38109
- await requireAdmin(request2);
38110
- const body2 = await request2.json();
38111
- return await flows.start(isRecord3(body2) ? body2.provider : undefined, isRecord3(body2) ? body2.label : undefined);
38112
- } catch (error51) {
38113
- return errorResponse(error51);
38114
- }
38151
+ return new Elysia().onError(apiErrorHandler).post("/api/connect/start", async ({ request: request2 }) => {
38152
+ await requireAdmin(request2, deps.admin);
38153
+ const body2 = await readJsonRecord(request2);
38154
+ return flows.start(body2?.provider, body2?.label);
38115
38155
  }).post("/api/connect/finish", async ({ request: request2 }) => {
38116
- try {
38117
- await requireAdmin(request2);
38118
- const body2 = await request2.json();
38119
- return await flows.finish(isRecord3(body2) ? body2.flowId : undefined, isRecord3(body2) ? body2.code : undefined);
38120
- } catch (error51) {
38121
- return errorResponse(error51);
38122
- }
38156
+ await requireAdmin(request2, deps.admin);
38157
+ const body2 = await readJsonRecord(request2);
38158
+ return flows.finish(body2?.flowId, body2?.code);
38123
38159
  }).post("/api/connect/poll", async ({ request: request2, set: set2 }) => {
38124
- try {
38125
- await requireAdmin(request2);
38126
- const body2 = await request2.json();
38127
- const outcome = await flows.poll(isRecord3(body2) ? body2.flowId : undefined);
38128
- if (outcome.status === "pending")
38129
- set2.status = 202;
38130
- return outcome;
38131
- } catch (error51) {
38132
- return errorResponse(error51);
38133
- }
38160
+ await requireAdmin(request2, deps.admin);
38161
+ const body2 = await readJsonRecord(request2);
38162
+ const outcome = await flows.poll(body2?.flowId);
38163
+ if (outcome.status === "pending")
38164
+ set2.status = 202;
38165
+ return outcome;
38134
38166
  });
38135
38167
  }
38136
38168
 
@@ -38169,6 +38201,73 @@ function extractToken(header) {
38169
38201
  return match === null ? value : match[1].trim();
38170
38202
  }
38171
38203
 
38204
+ // apps/gateway/src/logging.ts
38205
+ function requestLogDefaults(id, at) {
38206
+ return {
38207
+ id,
38208
+ state: "done",
38209
+ at,
38210
+ apiKeyId: null,
38211
+ requestedModel: "",
38212
+ resolvedProvider: null,
38213
+ resolvedModel: null,
38214
+ credentialId: null,
38215
+ attempts: 0,
38216
+ status: 0,
38217
+ errorCode: null,
38218
+ inputTokens: 0,
38219
+ outputTokens: 0,
38220
+ cacheReadTokens: 0,
38221
+ cacheWriteTokens: 0,
38222
+ ttftMs: null,
38223
+ durationMs: 0,
38224
+ costUsd: 0,
38225
+ degradations: []
38226
+ };
38227
+ }
38228
+ function newCompletedRequestLog(id, at, overrides) {
38229
+ return { ...requestLogDefaults(id, at), ...overrides, id, state: "done", at };
38230
+ }
38231
+ function newPendingRequestLog(input) {
38232
+ const { id, at, requestedModel, resolvedProvider, resolvedModel, credentialId } = input;
38233
+ return {
38234
+ ...requestLogDefaults(id, at),
38235
+ requestedModel,
38236
+ resolvedProvider,
38237
+ resolvedModel,
38238
+ credentialId,
38239
+ state: "pending",
38240
+ status: 0
38241
+ };
38242
+ }
38243
+ function report(what, requestId, error51) {
38244
+ console.error(what, {
38245
+ requestId,
38246
+ reason: error51 instanceof Error ? error51.message : "unknown"
38247
+ });
38248
+ }
38249
+ async function beginLog(store, log, keyId) {
38250
+ try {
38251
+ await store.usage.begin({ ...log, state: "pending", apiKeyId: keyId });
38252
+ } catch (error51) {
38253
+ report("failed to record request start", log.id, error51);
38254
+ }
38255
+ }
38256
+ async function routeLog(store, requestId, target) {
38257
+ try {
38258
+ await store.usage.route(requestId, target);
38259
+ } catch (error51) {
38260
+ report("failed to record request route", requestId, error51);
38261
+ }
38262
+ }
38263
+ async function finishLog(store, log, keyId) {
38264
+ try {
38265
+ await store.usage.append({ ...log, apiKeyId: keyId });
38266
+ } catch (error51) {
38267
+ report("failed to persist request log", log.id, error51);
38268
+ }
38269
+ }
38270
+
38172
38271
  // apps/gateway/src/dispatch/attempt.ts
38173
38272
  async function attempt(opts) {
38174
38273
  const { candidate, adapter, http, now, signal, refresh, refreshLeadMs } = opts;
@@ -38222,30 +38321,14 @@ function classify(error51) {
38222
38321
  }
38223
38322
 
38224
38323
  // apps/gateway/src/dispatch/index.ts
38225
- async function dispatch(request2, deps, signal) {
38324
+ async function dispatch(request2, deps, signal, requestId) {
38226
38325
  const startedAt = deps.now();
38227
38326
  const snapshot = await deps.snapshots.get(startedAt);
38228
38327
  const deadlineAt = startedAt + snapshot.settings.requestDeadlineMs;
38229
- const log = {
38230
- id: crypto.randomUUID(),
38231
- at: startedAt,
38232
- apiKeyId: null,
38328
+ const log = newCompletedRequestLog(requestId, startedAt, {
38233
38329
  requestedModel: request2.model,
38234
- resolvedProvider: null,
38235
- resolvedModel: null,
38236
- credentialId: null,
38237
- attempts: 0,
38238
- status: 200,
38239
- errorCode: null,
38240
- inputTokens: 0,
38241
- outputTokens: 0,
38242
- cacheReadTokens: 0,
38243
- cacheWriteTokens: 0,
38244
- ttftMs: null,
38245
- durationMs: 0,
38246
- costUsd: 0,
38247
- degradations: []
38248
- };
38330
+ status: 0
38331
+ });
38249
38332
  const fail = (code, message) => {
38250
38333
  log.errorCode = code;
38251
38334
  log.status = HTTP_STATUS[code];
@@ -38319,6 +38402,11 @@ async function dispatch(request2, deps, signal) {
38319
38402
  log.credentialId = candidate.credential.id;
38320
38403
  log.resolvedProvider = candidate.target.provider;
38321
38404
  log.resolvedModel = candidate.target.model;
38405
+ await deps.onRoute?.({
38406
+ provider: candidate.target.provider,
38407
+ model: candidate.target.model,
38408
+ credentialId: candidate.credential.id
38409
+ });
38322
38410
  log.inputTokens = 0;
38323
38411
  log.outputTokens = 0;
38324
38412
  log.cacheReadTokens = 0;
@@ -39115,38 +39203,35 @@ function parseOpenAIRequest(body2) {
39115
39203
  return validateRequest(request2);
39116
39204
  }
39117
39205
 
39118
- // apps/gateway/src/logging.ts
39119
- async function finishLog(store, log, keyId) {
39120
- try {
39121
- await store.usage.append({ ...log, apiKeyId: keyId });
39122
- } catch (error51) {
39123
- console.error("failed to persist request log", {
39124
- requestId: log.id,
39125
- reason: error51 instanceof Error ? error51.message : "unknown"
39126
- });
39127
- }
39128
- }
39129
-
39130
39206
  // apps/gateway/src/routes/proxy.ts
39207
+ var KEEPALIVE_MS = 1e4;
39208
+ var KEEPALIVE = Symbol("keepalive");
39209
+ function withKeepalive(pending, ms) {
39210
+ let timer;
39211
+ const tick = new Promise((resolve) => {
39212
+ timer = setTimeout(() => resolve(KEEPALIVE), ms);
39213
+ });
39214
+ return Promise.race([pending, tick]).finally(() => clearTimeout(timer));
39215
+ }
39131
39216
  var SSE_HEADERS = {
39132
39217
  "content-type": "text/event-stream; charset=utf-8",
39133
39218
  "cache-control": "no-cache, no-transform",
39134
39219
  connection: "keep-alive",
39135
39220
  "x-accel-buffering": "no"
39136
39221
  };
39137
- function errorResponse2(surface, code, message3) {
39222
+ function errorResponse(surface, code, message3) {
39138
39223
  const body2 = surface === "anthropic" ? anthropicErrorBody(code, message3) : openaiErrorBody(code, message3);
39139
39224
  return new Response(JSON.stringify(body2), {
39140
39225
  status: HTTP_STATUS[code],
39141
39226
  headers: { "content-type": "application/json" }
39142
39227
  });
39143
39228
  }
39144
- function asGatewayError2(error51) {
39229
+ function asGatewayError(error51) {
39145
39230
  if (error51 instanceof GatewayError)
39146
39231
  return error51;
39147
39232
  return new GatewayError("INTERNAL", error51 instanceof Error ? error51.message : "internal error");
39148
39233
  }
39149
- function sseResponse(frames, onDone) {
39234
+ function sseResponse(frames, onDone, keepaliveMs) {
39150
39235
  const encoder2 = new TextEncoder;
39151
39236
  let done = null;
39152
39237
  const runOnce = () => {
@@ -39154,10 +39239,19 @@ function sseResponse(frames, onDone) {
39154
39239
  done = onDone();
39155
39240
  return done;
39156
39241
  };
39242
+ let inflight = null;
39157
39243
  const stream2 = new ReadableStream({
39158
39244
  async pull(controller) {
39159
39245
  try {
39160
- const next = await frames.next();
39246
+ inflight ??= frames.next();
39247
+ const next = await withKeepalive(inflight, keepaliveMs);
39248
+ if (next === KEEPALIVE) {
39249
+ controller.enqueue(encoder2.encode(`: keepalive
39250
+
39251
+ `));
39252
+ return;
39253
+ }
39254
+ inflight = null;
39161
39255
  if (next.done === true) {
39162
39256
  controller.close();
39163
39257
  await runOnce();
@@ -39182,7 +39276,9 @@ data: ${data}
39182
39276
  }
39183
39277
  async function handle(deps, rateLimiter, surface, request2) {
39184
39278
  const requestId = deps.requestId();
39279
+ const startedAt = deps.now();
39185
39280
  let keyId = null;
39281
+ let requestedModel = "";
39186
39282
  try {
39187
39283
  const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers));
39188
39284
  keyId = key.id;
@@ -39192,11 +39288,31 @@ async function handle(deps, rateLimiter, surface, request2) {
39192
39288
  if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
39193
39289
  throw new GatewayError("AUTH", `model "${chatRequest.model}" is not allowed for this API key`);
39194
39290
  }
39195
- const outcome = await dispatch(chatRequest, deps, request2.signal);
39196
- const log = () => finishLog(deps.store, { ...outcome.log(), id: requestId }, keyId);
39291
+ requestedModel = chatRequest.model;
39292
+ let began = false;
39293
+ const outcome = await dispatch(chatRequest, {
39294
+ ...deps,
39295
+ async onRoute(target) {
39296
+ if (began) {
39297
+ await routeLog(deps.store, requestId, target);
39298
+ return;
39299
+ }
39300
+ began = true;
39301
+ const log2 = newPendingRequestLog({
39302
+ id: requestId,
39303
+ at: startedAt,
39304
+ requestedModel,
39305
+ resolvedProvider: target.provider,
39306
+ resolvedModel: target.model,
39307
+ credentialId: target.credentialId
39308
+ });
39309
+ await beginLog(deps.store, log2, keyId);
39310
+ }
39311
+ }, request2.signal, requestId);
39312
+ const log = () => finishLog(deps.store, outcome.log(), keyId);
39197
39313
  if (chatRequest.stream) {
39198
39314
  const frames = surface === "anthropic" ? anthropicStream(outcome.events, requestId) : openaiStream(outcome.events, requestId, Math.floor(deps.now() / 1000));
39199
- return sseResponse(frames, log);
39315
+ return sseResponse(frames, log, deps.keepaliveMs);
39200
39316
  }
39201
39317
  const events = [];
39202
39318
  for await (const event of outcome.events)
@@ -39204,7 +39320,7 @@ async function handle(deps, rateLimiter, surface, request2) {
39204
39320
  await log();
39205
39321
  const failure = events.find((e) => e.type === "error");
39206
39322
  if (failure !== undefined)
39207
- return errorResponse2(surface, failure.code, failure.message);
39323
+ return errorResponse(surface, failure.code, failure.message);
39208
39324
  const collected = collect(events);
39209
39325
  const responseBody = surface === "anthropic" ? anthropicResponse(collected, requestId) : openaiResponse(collected, requestId, Math.floor(deps.now() / 1000));
39210
39326
  return new Response(JSON.stringify(responseBody), {
@@ -39212,35 +39328,21 @@ async function handle(deps, rateLimiter, surface, request2) {
39212
39328
  headers: { "content-type": "application/json" }
39213
39329
  });
39214
39330
  } catch (error51) {
39215
- const gatewayError = asGatewayError2(error51);
39216
- await finishLog(deps.store, {
39217
- id: requestId,
39218
- at: deps.now(),
39219
- apiKeyId: keyId,
39220
- requestedModel: "",
39221
- resolvedProvider: null,
39222
- resolvedModel: null,
39223
- credentialId: null,
39224
- attempts: 0,
39331
+ const gatewayError = asGatewayError(error51);
39332
+ await finishLog(deps.store, newCompletedRequestLog(requestId, startedAt, {
39333
+ requestedModel,
39225
39334
  status: HTTP_STATUS[gatewayError.code],
39226
- errorCode: gatewayError.code,
39227
- inputTokens: 0,
39228
- outputTokens: 0,
39229
- cacheReadTokens: 0,
39230
- cacheWriteTokens: 0,
39231
- ttftMs: null,
39232
- durationMs: 0,
39233
- costUsd: 0,
39234
- degradations: []
39235
- }, keyId);
39236
- return errorResponse2(surface, gatewayError.code, gatewayError.message);
39335
+ errorCode: gatewayError.code
39336
+ }), keyId);
39337
+ return errorResponse(surface, gatewayError.code, gatewayError.message);
39237
39338
  }
39238
39339
  }
39239
39340
  function proxyRoutes(deps) {
39240
39341
  const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter(deps.now);
39241
39342
  const dispatchDeps = {
39242
39343
  ...deps,
39243
- snapshots: deps.snapshots ?? createRoutingSnapshotCache(deps.store)
39344
+ snapshots: deps.snapshots ?? createRoutingSnapshotCache(deps.store),
39345
+ keepaliveMs: deps.keepaliveMs ?? KEEPALIVE_MS
39244
39346
  };
39245
39347
  return new Elysia().post("/v1/messages", ({ request: request2 }) => handle(dispatchDeps, rateLimiter, "anthropic", request2)).post("/v1/chat/completions", ({ request: request2 }) => handle(dispatchDeps, rateLimiter, "openai", request2)).get("/v1/models", async ({ request: request2 }) => {
39246
39348
  try {
@@ -39257,12 +39359,12 @@ function proxyRoutes(deps) {
39257
39359
  }))
39258
39360
  });
39259
39361
  } catch (error51) {
39260
- const gatewayError = asGatewayError2(error51);
39261
- return errorResponse2("anthropic", gatewayError.code, gatewayError.message);
39362
+ const gatewayError = asGatewayError(error51);
39363
+ return errorResponse("anthropic", gatewayError.code, gatewayError.message);
39262
39364
  }
39263
39365
  }).onError(({ error: error51 }) => {
39264
- const gatewayError = asGatewayError2(error51);
39265
- return errorResponse2("anthropic", gatewayError.code, gatewayError.message);
39366
+ const gatewayError = asGatewayError(error51);
39367
+ return errorResponse("anthropic", gatewayError.code, gatewayError.message);
39266
39368
  });
39267
39369
  }
39268
39370
 
@@ -39469,6 +39571,9 @@ var store = await createStore({
39469
39571
  path: config2.databasePath,
39470
39572
  encryptionKey
39471
39573
  });
39574
+ var swept = await store.usage.sweepPending();
39575
+ if (swept > 0)
39576
+ console.log(`retired ${swept} request(s) interrupted by the last shutdown`);
39472
39577
  var now = () => Date.now();
39473
39578
  var http = nodeHttpClient();
39474
39579
  var refresh = createRefresher({ store, providers: OAUTH_PROVIDERS, http, now });
@@ -39489,7 +39594,7 @@ var stopQuotaPoller = await startQuotaPoller({
39489
39594
  refresh,
39490
39595
  now
39491
39596
  });
39492
- app.listen({ port: config2.port, hostname: config2.host });
39597
+ app.listen({ port: config2.port, hostname: config2.host, idleTimeout: 255 });
39493
39598
  console.log(`omnigateway listening on http://${config2.host}:${config2.port}`);
39494
39599
  var shuttingDown = false;
39495
39600
  function exitAfterClosingStore(code) {