omnigateway 0.1.2 → 0.1.4

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 +140 -51
  2. package/gateway.js +289 -87
  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-BhGx7Gy0.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-DOQGdhoM.js} +8 -8
  10. package/public/assets/{Rack-uDxrjvaz.js → Rack-DnbGnBvz.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-CP3RvAAB.js} +3 -3
  15. package/public/assets/_app-D83powx4.js +1 -0
  16. package/public/assets/_app.accounts-DR-2t9-7.js +51 -0
  17. package/public/assets/_app.index-DimCB0Bo.js +61 -0
  18. package/public/assets/{_app.keys-BNEOjtbu.js → _app.keys-B7kl3CSe.js} +8 -8
  19. package/public/assets/_app.logs-BjtdzcDY.js +18 -0
  20. package/public/assets/_app.models-BEECjcLM.js +144 -0
  21. package/public/assets/{_app.settings-C1gB4w7c.js → _app.settings-DtDIhAAr.js} +6 -6
  22. package/public/assets/{_app.usage-B0iHKzmI.js → _app.usage-BChnSBbS.js} +28 -28
  23. package/public/assets/index-C53PiRfb.js +170 -0
  24. package/public/assets/{login-D-FsWNX8.js → login-BuvJY1Qd.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-Cq7ldH9D.js} +1 -1
  27. package/public/index.html +2 -2
  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
@@ -5866,6 +5866,7 @@ async function* decodeAnthropic(messages) {
5866
5866
  let cacheWriteTokens = 0;
5867
5867
  let outputTokens = 0;
5868
5868
  let stopReason = "endTurn";
5869
+ let terminal = false;
5869
5870
  for await (const msg of messages) {
5870
5871
  const d = json(msg.data);
5871
5872
  if (d === null)
@@ -5931,6 +5932,7 @@ async function* decodeAnthropic(messages) {
5931
5932
  break;
5932
5933
  }
5933
5934
  case "message_stop":
5935
+ terminal = true;
5934
5936
  yield {
5935
5937
  type: "end",
5936
5938
  stopReason,
@@ -5938,6 +5940,7 @@ async function* decodeAnthropic(messages) {
5938
5940
  };
5939
5941
  break;
5940
5942
  case "error": {
5943
+ terminal = true;
5941
5944
  const code = ERROR_TYPE[String(d.error?.type)] ?? "UPSTREAM";
5942
5945
  yield {
5943
5946
  type: "error",
@@ -5951,6 +5954,14 @@ async function* decodeAnthropic(messages) {
5951
5954
  break;
5952
5955
  }
5953
5956
  }
5957
+ if (!terminal) {
5958
+ yield {
5959
+ type: "error",
5960
+ code: "UPSTREAM",
5961
+ message: "upstream stream ended before message_stop",
5962
+ retryable: RETRYABLE.UPSTREAM
5963
+ };
5964
+ }
5954
5965
  }
5955
5966
 
5956
5967
  // packages/providers/src/anthropic/wire.ts
@@ -6063,9 +6074,10 @@ var anthropicAdapter = {
6063
6074
  ["anthropic-version", API_VERSION],
6064
6075
  ["Accept", req.request.stream ? "text/event-stream" : "application/json"]
6065
6076
  ];
6077
+ const betas = new Set(req.request.betas ?? []);
6066
6078
  if (oauth) {
6067
6079
  protocol.push(["Authorization", `Bearer ${req.credentials.accessToken}`]);
6068
- protocol.push(["anthropic-beta", OAUTH_BETA]);
6080
+ betas.add(OAUTH_BETA);
6069
6081
  } else if (req.credentials.apiKey !== null) {
6070
6082
  protocol.push(["x-api-key", req.credentials.apiKey]);
6071
6083
  } else {
@@ -6073,6 +6085,8 @@ var anthropicAdapter = {
6073
6085
  provider: "anthropic"
6074
6086
  });
6075
6087
  }
6088
+ if (betas.size > 0)
6089
+ protocol.push(["anthropic-beta", [...betas].join(",")]);
6076
6090
  const profile = PROFILES.anthropic;
6077
6091
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
6078
6092
  const bodyString = signAnthropicBody(JSON.stringify(orderFields(withSystem, BODY_ORDER.anthropic)));
@@ -6290,18 +6304,16 @@ async function* decodeChat(messages) {
6290
6304
  let started = false;
6291
6305
  let textOpen = false;
6292
6306
  let textIndex;
6293
- let ended = false;
6307
+ let done = false;
6294
6308
  let stopReason = "endTurn";
6295
6309
  let usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
6296
6310
  const toolIndex = new Map;
6297
6311
  let nextIndex = 0;
6298
- const emitEnd = () => {
6299
- ended = true;
6300
- return { type: "end", stopReason, usage };
6301
- };
6302
6312
  for await (const msg of messages) {
6303
- if (msg.data === "[DONE]")
6313
+ if (msg.data === "[DONE]") {
6314
+ done = true;
6304
6315
  break;
6316
+ }
6305
6317
  const d = json2(msg.data);
6306
6318
  if (d === null)
6307
6319
  continue;
@@ -6356,19 +6368,21 @@ async function* decodeChat(messages) {
6356
6368
  }
6357
6369
  if (typeof choice.finish_reason === "string") {
6358
6370
  stopReason = FINISH[choice.finish_reason] ?? "endTurn";
6359
- if (textOpen)
6360
- yield { type: "blockEnd", index: textIndex ?? 0 };
6361
- for (const index of toolIndex.values())
6362
- yield { type: "blockEnd", index };
6363
- yield emitEnd();
6364
6371
  }
6365
6372
  }
6366
- if (!ended) {
6373
+ if (done) {
6367
6374
  if (textOpen)
6368
6375
  yield { type: "blockEnd", index: textIndex ?? 0 };
6369
6376
  for (const index of toolIndex.values())
6370
6377
  yield { type: "blockEnd", index };
6371
- yield emitEnd();
6378
+ yield { type: "end", stopReason, usage };
6379
+ } else {
6380
+ yield {
6381
+ type: "error",
6382
+ code: "UPSTREAM",
6383
+ message: "upstream stream ended before [DONE]",
6384
+ retryable: RETRYABLE.UPSTREAM
6385
+ };
6372
6386
  }
6373
6387
  }
6374
6388
 
@@ -6523,6 +6537,7 @@ async function* decodeResponses(messages) {
6523
6537
  return assigned;
6524
6538
  };
6525
6539
  let sawToolCall = false;
6540
+ let terminal = false;
6526
6541
  const ownsBlock = new Set;
6527
6542
  for await (const msg of messages) {
6528
6543
  const d = json3(msg.data);
@@ -6598,6 +6613,7 @@ async function* decodeResponses(messages) {
6598
6613
  }
6599
6614
  case "response.completed":
6600
6615
  case "response.incomplete": {
6616
+ terminal = true;
6601
6617
  const r = d.response ?? {};
6602
6618
  const reason = r.incomplete_details?.reason;
6603
6619
  let stopReason = sawToolCall ? "toolUse" : "endTurn";
@@ -6619,6 +6635,7 @@ async function* decodeResponses(messages) {
6619
6635
  }
6620
6636
  case "response.failed":
6621
6637
  case "error": {
6638
+ terminal = true;
6622
6639
  const err = d.response?.error ?? d.error ?? {};
6623
6640
  const code = ERROR_CODE[String(err.code ?? err.type)] ?? "UPSTREAM";
6624
6641
  yield {
@@ -6633,6 +6650,14 @@ async function* decodeResponses(messages) {
6633
6650
  break;
6634
6651
  }
6635
6652
  }
6653
+ if (!terminal) {
6654
+ yield {
6655
+ type: "error",
6656
+ code: "UPSTREAM",
6657
+ message: "upstream stream ended before response completion",
6658
+ retryable: RETRYABLE.UPSTREAM
6659
+ };
6660
+ }
6636
6661
  }
6637
6662
 
6638
6663
  // packages/providers/src/openai/wire.ts
@@ -22609,6 +22634,27 @@ ALTER TABLE credentials ADD COLUMN disabled_reason TEXT;
22609
22634
  ALTER TABLE credentials ADD COLUMN disabled_at INTEGER;
22610
22635
  `;
22611
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
+
22612
22658
  // packages/store/src/sqlite/rollup.ts
22613
22659
  function startOfLocalDay(at) {
22614
22660
  const day = new Date(at);
@@ -22715,7 +22761,8 @@ function backfillDaily(db) {
22715
22761
  var MIGRATIONS = [
22716
22762
  { id: 1, sql: _001_init_default },
22717
22763
  { id: 2, sql: _002_usage_daily_default, after: backfillDaily },
22718
- { id: 3, sql: _003_quota_snapshot_default }
22764
+ { id: 3, sql: _003_quota_snapshot_default },
22765
+ { id: 4, sql: _004_request_state_default }
22719
22766
  ];
22720
22767
  function openDb(path) {
22721
22768
  const db = new Database(path, { create: true });
@@ -22787,6 +22834,7 @@ function createKeyRepo(db) {
22787
22834
  // packages/store/src/sqlite/usage.ts
22788
22835
  var toLog = (r) => ({
22789
22836
  id: r.id,
22837
+ state: r.state === "pending" ? "pending" : "done",
22790
22838
  at: r.at,
22791
22839
  apiKeyId: r.api_key_id,
22792
22840
  requestedModel: r.requested_model,
@@ -22845,37 +22893,78 @@ function label(value) {
22845
22893
  const text = String(value);
22846
22894
  return text.length === 0 ? "unknown" : text;
22847
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`;
22848
22943
  function createUsageRepo(db) {
22849
- const insert = db.transaction((log) => {
22850
- db.run(`INSERT INTO request_logs
22851
- (id, at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
22852
- attempts, status, error_code, input_tokens, output_tokens, cache_read_tokens,
22853
- cache_write_tokens, ttft_ms, duration_ms, cost_usd, degradations)
22854
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
22855
- log.id,
22856
- log.at,
22857
- log.apiKeyId,
22858
- log.requestedModel,
22859
- log.resolvedProvider,
22860
- log.resolvedModel,
22861
- log.credentialId,
22862
- log.attempts,
22863
- log.status,
22864
- log.errorCode,
22865
- log.inputTokens,
22866
- log.outputTokens,
22867
- log.cacheReadTokens,
22868
- log.cacheWriteTokens,
22869
- log.ttftMs,
22870
- log.durationMs,
22871
- log.costUsd,
22872
- JSON.stringify(log.degradations)
22873
- ]);
22874
- 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));
22875
22949
  });
22876
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
+ },
22877
22959
  async append(log) {
22878
- 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;
22879
22968
  },
22880
22969
  async recent(limit) {
22881
22970
  return db.query("SELECT * FROM request_logs ORDER BY at DESC LIMIT ?").all(limit).map(toLog);
@@ -22897,7 +22986,7 @@ function createUsageRepo(db) {
22897
22986
  COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
22898
22987
  COALESCE(SUM(cost_usd), 0) AS cost_usd
22899
22988
  FROM ${daily ? "usage_daily" : "request_logs"}
22900
- WHERE ${timeColumn} >= ? AND ${timeColumn} <= ?
22989
+ WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= ? AND ${timeColumn} <= ?
22901
22990
  GROUP BY key${split === null ? "" : ", split"}
22902
22991
  ORDER BY requests DESC`).all(since, until);
22903
22992
  return rows.map((r) => {
@@ -31442,17 +31531,17 @@ var ElysiaType = {
31442
31531
  elysiaMeta: "ArrayString"
31443
31532
  })).Decode((value) => {
31444
31533
  if (Array.isArray(value)) {
31445
- let values = [];
31534
+ let values2 = [];
31446
31535
  for (let i = 0;i < value.length; i++) {
31447
31536
  const v = value[i];
31448
31537
  if (typeof v == "string") {
31449
31538
  const t2 = decode4(v, true);
31450
- Array.isArray(t2) ? values = values.concat(t2) : values.push(t2);
31539
+ Array.isArray(t2) ? values2 = values2.concat(t2) : values2.push(t2);
31451
31540
  continue;
31452
31541
  }
31453
- values.push(v);
31542
+ values2.push(v);
31454
31543
  }
31455
- return values;
31544
+ return values2;
31456
31545
  }
31457
31546
  return typeof value == "string" ? decode4(value) : value;
31458
31547
  }).Encode((value) => {
@@ -31473,17 +31562,17 @@ var ElysiaType = {
31473
31562
  elysiaMeta: "ArrayQuery"
31474
31563
  })).Decode((value) => {
31475
31564
  if (Array.isArray(value)) {
31476
- let values = [];
31565
+ let values2 = [];
31477
31566
  for (let i = 0;i < value.length; i++) {
31478
31567
  const v = value[i];
31479
31568
  if (typeof v == "string") {
31480
31569
  const t2 = decode4(v);
31481
- Array.isArray(t2) ? values = values.concat(t2) : values.push(t2);
31570
+ Array.isArray(t2) ? values2 = values2.concat(t2) : values2.push(t2);
31482
31571
  continue;
31483
31572
  }
31484
- values.push(v);
31573
+ values2.push(v);
31485
31574
  }
31486
- return values;
31575
+ return values2;
31487
31576
  }
31488
31577
  return typeof value == "string" ? decode4(value) : value;
31489
31578
  }).Encode((value) => {
@@ -31527,16 +31616,16 @@ var ElysiaType = {
31527
31616
  sign
31528
31617
  }, v;
31529
31618
  },
31530
- UnionEnum: (values, options = {}) => {
31531
- 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" } : {};
31532
- 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))
31533
31622
  throw new Error("This type does not support objects or arrays");
31534
31623
  return {
31535
- default: values[0],
31624
+ default: values2[0],
31536
31625
  ...options,
31537
31626
  [Kind]: "UnionEnum",
31538
31627
  ...type,
31539
- enum: values
31628
+ enum: values2
31540
31629
  };
31541
31630
  },
31542
31631
  NoValidate: (v, enabled = true) => (v.noValidate = enabled, v),
@@ -33982,33 +34071,33 @@ var getSchemaValidator = (s, {
33982
34071
  return v.then((v2) => Check22(v2, true));
33983
34072
  if (v.issues)
33984
34073
  return v;
33985
- const values = [];
33986
- return v && typeof v == "object" && values.push(v.value), runCheckers2(value, 0, values, v);
33987
- }, 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) {
33988
34077
  for (let i = startIndex;i < checkers.length; i++) {
33989
34078
  let v = checkers[i].validate(value);
33990
34079
  if (v instanceof Promise)
33991
34080
  return v.then((resolved) => {
33992
34081
  if (resolved.issues)
33993
34082
  return resolved;
33994
- const nextValues = [...values];
34083
+ const nextValues = [...values2];
33995
34084
  return resolved && typeof resolved == "object" && nextValues.push(resolved.value), runCheckers2(value, i + 1, nextValues, resolved);
33996
34085
  });
33997
34086
  if (v.issues)
33998
34087
  return v;
33999
- v && typeof v == "object" && values.push(v.value), lastV = v;
34088
+ v && typeof v == "object" && values2.push(v.value), lastV = v;
34000
34089
  }
34001
- return mergeValues22(values, lastV);
34002
- }, mergeValues22 = function(values, lastV) {
34003
- if (!values.length)
34090
+ return mergeValues22(values2, lastV);
34091
+ }, mergeValues22 = function(values2, lastV) {
34092
+ if (!values2.length)
34004
34093
  return { value: lastV };
34005
- if (values.length === 1)
34006
- return { value: values[0] };
34007
- if (values.length === 2)
34008
- return { value: mergeDeep(values[0], values[1]) };
34009
- let newValue = mergeDeep(values[0], values[1]);
34010
- for (let i = 2;i < values.length; i++)
34011
- 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]);
34012
34101
  return { value: newValue };
34013
34102
  };
34014
34103
  var Check2 = Check22, runCheckers = runCheckers2, mergeValues2 = mergeValues22;
@@ -38203,6 +38292,7 @@ async function dispatch(request2, deps, signal) {
38203
38292
  const deadlineAt = startedAt + snapshot.settings.requestDeadlineMs;
38204
38293
  const log = {
38205
38294
  id: crypto.randomUUID(),
38295
+ state: "done",
38206
38296
  at: startedAt,
38207
38297
  apiKeyId: null,
38208
38298
  requestedModel: request2.model,
@@ -38294,6 +38384,11 @@ async function dispatch(request2, deps, signal) {
38294
38384
  log.credentialId = candidate.credential.id;
38295
38385
  log.resolvedProvider = candidate.target.provider;
38296
38386
  log.resolvedModel = candidate.target.model;
38387
+ await deps.onRoute?.({
38388
+ provider: candidate.target.provider,
38389
+ model: candidate.target.model,
38390
+ credentialId: candidate.credential.id
38391
+ });
38297
38392
  log.inputTokens = 0;
38298
38393
  log.outputTokens = 0;
38299
38394
  log.cacheReadTokens = 0;
@@ -38319,12 +38414,18 @@ async function dispatch(request2, deps, signal) {
38319
38414
  }), dispatchSignal);
38320
38415
  for (const d of result.degradations)
38321
38416
  log.degradations.push(d);
38417
+ let terminal = false;
38418
+ const pending = [];
38322
38419
  for await (const event of result.events) {
38323
38420
  if (event.type === "blockDelta" && !committed) {
38324
38421
  committed = true;
38325
38422
  log.ttftMs = deps.now() - startedAt;
38423
+ for (const buffered of pending)
38424
+ yield buffered;
38425
+ pending.length = 0;
38326
38426
  }
38327
38427
  if (event.type === "end") {
38428
+ terminal = true;
38328
38429
  log.inputTokens = event.usage.inputTokens;
38329
38430
  log.outputTokens = event.usage.outputTokens;
38330
38431
  log.cacheReadTokens = event.usage.cacheReadTokens;
@@ -38332,6 +38433,7 @@ async function dispatch(request2, deps, signal) {
38332
38433
  log.costUsd = priceOf(candidate, event.usage);
38333
38434
  }
38334
38435
  if (event.type === "error") {
38436
+ terminal = true;
38335
38437
  if (!committed && RETRYABLE[event.code]) {
38336
38438
  throw new GatewayError(event.code, event.message);
38337
38439
  }
@@ -38348,7 +38450,21 @@ async function dispatch(request2, deps, signal) {
38348
38450
  log.durationMs = deps.now() - startedAt;
38349
38451
  return;
38350
38452
  }
38351
- yield event;
38453
+ if (!committed && event.type !== "end") {
38454
+ pending.push(event);
38455
+ } else {
38456
+ if (!committed) {
38457
+ for (const buffered of pending)
38458
+ yield buffered;
38459
+ pending.length = 0;
38460
+ }
38461
+ yield event;
38462
+ }
38463
+ if (event.type === "end")
38464
+ break;
38465
+ }
38466
+ if (!terminal) {
38467
+ throw new GatewayError("UPSTREAM", "upstream stream ended without a terminal event");
38352
38468
  }
38353
38469
  await persistHealth(recordSuccess(healthFor(candidate), {
38354
38470
  settings: snapshot.settings,
@@ -38602,6 +38718,7 @@ var chunk = (id, created, model, choice, usage) => ({
38602
38718
  async function* openaiStream(events, requestId, created) {
38603
38719
  let model = "";
38604
38720
  let roleSent = false;
38721
+ let completed = false;
38605
38722
  const toolIndex = new Map;
38606
38723
  const emit = (data) => ({ event: "message", data: JSON.stringify(data) });
38607
38724
  for await (const event of events) {
@@ -38663,6 +38780,7 @@ async function* openaiStream(events, requestId, created) {
38663
38780
  case "blockEnd":
38664
38781
  break;
38665
38782
  case "end":
38783
+ completed = true;
38666
38784
  yield emit(chunk(requestId, created, model, { index: 0, delta: {}, finish_reason: FINISH2[event.stopReason] }, {
38667
38785
  prompt_tokens: event.usage.inputTokens,
38668
38786
  completion_tokens: event.usage.outputTokens,
@@ -38676,7 +38794,8 @@ async function* openaiStream(events, requestId, created) {
38676
38794
  }
38677
38795
  }
38678
38796
  }
38679
- yield { event: "message", data: "[DONE]" };
38797
+ if (completed)
38798
+ yield { event: "message", data: "[DONE]" };
38680
38799
  }
38681
38800
  function openaiResponse(collected, requestId, created) {
38682
38801
  const text = collected.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("");
@@ -38855,7 +38974,14 @@ function toIrReasoning(thinking, effort) {
38855
38974
  return { mode: "off" };
38856
38975
  }
38857
38976
  }
38858
- function parseAnthropicRequest(body2) {
38977
+ function readBetas(headers) {
38978
+ const raw = headers?.get("anthropic-beta");
38979
+ if (raw === undefined || raw === null)
38980
+ return [];
38981
+ const names = raw.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
38982
+ return [...new Set(names)];
38983
+ }
38984
+ function parseAnthropicRequest(body2, headers) {
38859
38985
  if (typeof body2 !== "object" || body2 === null) {
38860
38986
  throw new GatewayError("BAD_REQUEST", "request body must be a JSON object");
38861
38987
  }
@@ -38896,6 +39022,9 @@ function parseAnthropicRequest(body2) {
38896
39022
  const extras = extraFields(body2, KNOWN);
38897
39023
  if (extras !== undefined)
38898
39024
  request2.vendor = { anthropic: extras };
39025
+ const betas = readBetas(headers);
39026
+ if (betas.length > 0)
39027
+ request2.betas = betas;
38899
39028
  return validateRequest(request2);
38900
39029
  }
38901
39030
 
@@ -39057,18 +39186,44 @@ function parseOpenAIRequest(body2) {
39057
39186
  }
39058
39187
 
39059
39188
  // apps/gateway/src/logging.ts
39189
+ function report(what, requestId, error51) {
39190
+ console.error(what, {
39191
+ requestId,
39192
+ reason: error51 instanceof Error ? error51.message : "unknown"
39193
+ });
39194
+ }
39195
+ async function beginLog(store, log, keyId) {
39196
+ try {
39197
+ await store.usage.begin({ ...log, state: "pending", apiKeyId: keyId });
39198
+ } catch (error51) {
39199
+ report("failed to record request start", log.id, error51);
39200
+ }
39201
+ }
39202
+ async function routeLog(store, requestId, target) {
39203
+ try {
39204
+ await store.usage.route(requestId, target);
39205
+ } catch (error51) {
39206
+ report("failed to record request route", requestId, error51);
39207
+ }
39208
+ }
39060
39209
  async function finishLog(store, log, keyId) {
39061
39210
  try {
39062
39211
  await store.usage.append({ ...log, apiKeyId: keyId });
39063
39212
  } catch (error51) {
39064
- console.error("failed to persist request log", {
39065
- requestId: log.id,
39066
- reason: error51 instanceof Error ? error51.message : "unknown"
39067
- });
39213
+ report("failed to persist request log", log.id, error51);
39068
39214
  }
39069
39215
  }
39070
39216
 
39071
39217
  // apps/gateway/src/routes/proxy.ts
39218
+ var KEEPALIVE_MS = 1e4;
39219
+ var KEEPALIVE = Symbol("keepalive");
39220
+ function withKeepalive(pending, ms) {
39221
+ let timer;
39222
+ const tick = new Promise((resolve) => {
39223
+ timer = setTimeout(() => resolve(KEEPALIVE), ms);
39224
+ });
39225
+ return Promise.race([pending, tick]).finally(() => clearTimeout(timer));
39226
+ }
39072
39227
  var SSE_HEADERS = {
39073
39228
  "content-type": "text/event-stream; charset=utf-8",
39074
39229
  "cache-control": "no-cache, no-transform",
@@ -39087,7 +39242,7 @@ function asGatewayError2(error51) {
39087
39242
  return error51;
39088
39243
  return new GatewayError("INTERNAL", error51 instanceof Error ? error51.message : "internal error");
39089
39244
  }
39090
- function sseResponse(frames, onDone) {
39245
+ function sseResponse(frames, onDone, keepaliveMs) {
39091
39246
  const encoder2 = new TextEncoder;
39092
39247
  let done = null;
39093
39248
  const runOnce = () => {
@@ -39095,10 +39250,19 @@ function sseResponse(frames, onDone) {
39095
39250
  done = onDone();
39096
39251
  return done;
39097
39252
  };
39253
+ let inflight = null;
39098
39254
  const stream2 = new ReadableStream({
39099
39255
  async pull(controller) {
39100
39256
  try {
39101
- const next = await frames.next();
39257
+ inflight ??= frames.next();
39258
+ const next = await withKeepalive(inflight, keepaliveMs);
39259
+ if (next === KEEPALIVE) {
39260
+ controller.enqueue(encoder2.encode(`: keepalive
39261
+
39262
+ `));
39263
+ return;
39264
+ }
39265
+ inflight = null;
39102
39266
  if (next.done === true) {
39103
39267
  controller.close();
39104
39268
  await runOnce();
@@ -39123,21 +39287,54 @@ data: ${data}
39123
39287
  }
39124
39288
  async function handle(deps, rateLimiter, surface, request2) {
39125
39289
  const requestId = deps.requestId();
39290
+ const startedAt = deps.now();
39126
39291
  let keyId = null;
39292
+ let requestedModel = "";
39127
39293
  try {
39128
39294
  const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers));
39129
39295
  keyId = key.id;
39130
39296
  rateLimiter.consume(key.id, key.rateLimitPerMin);
39131
39297
  const body2 = await request2.json();
39132
- const chatRequest = surface === "anthropic" ? parseAnthropicRequest(body2) : parseOpenAIRequest(body2);
39298
+ const chatRequest = surface === "anthropic" ? parseAnthropicRequest(body2, request2.headers) : parseOpenAIRequest(body2);
39133
39299
  if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
39134
39300
  throw new GatewayError("AUTH", `model "${chatRequest.model}" is not allowed for this API key`);
39135
39301
  }
39136
- const outcome = await dispatch(chatRequest, deps, request2.signal);
39302
+ requestedModel = chatRequest.model;
39303
+ let began = false;
39304
+ const outcome = await dispatch(chatRequest, {
39305
+ ...deps,
39306
+ async onRoute(target) {
39307
+ if (began) {
39308
+ await routeLog(deps.store, requestId, target);
39309
+ return;
39310
+ }
39311
+ began = true;
39312
+ await beginLog(deps.store, {
39313
+ id: requestId,
39314
+ at: startedAt,
39315
+ apiKeyId: keyId,
39316
+ requestedModel,
39317
+ resolvedProvider: target.provider,
39318
+ resolvedModel: target.model,
39319
+ credentialId: target.credentialId,
39320
+ attempts: 0,
39321
+ status: 0,
39322
+ errorCode: null,
39323
+ inputTokens: 0,
39324
+ outputTokens: 0,
39325
+ cacheReadTokens: 0,
39326
+ cacheWriteTokens: 0,
39327
+ ttftMs: null,
39328
+ durationMs: 0,
39329
+ costUsd: 0,
39330
+ degradations: []
39331
+ }, keyId);
39332
+ }
39333
+ }, request2.signal);
39137
39334
  const log = () => finishLog(deps.store, { ...outcome.log(), id: requestId }, keyId);
39138
39335
  if (chatRequest.stream) {
39139
39336
  const frames = surface === "anthropic" ? anthropicStream(outcome.events, requestId) : openaiStream(outcome.events, requestId, Math.floor(deps.now() / 1000));
39140
- return sseResponse(frames, log);
39337
+ return sseResponse(frames, log, deps.keepaliveMs);
39141
39338
  }
39142
39339
  const events = [];
39143
39340
  for await (const event of outcome.events)
@@ -39156,9 +39353,10 @@ async function handle(deps, rateLimiter, surface, request2) {
39156
39353
  const gatewayError = asGatewayError2(error51);
39157
39354
  await finishLog(deps.store, {
39158
39355
  id: requestId,
39159
- at: deps.now(),
39356
+ state: "done",
39357
+ at: startedAt,
39160
39358
  apiKeyId: keyId,
39161
- requestedModel: "",
39359
+ requestedModel,
39162
39360
  resolvedProvider: null,
39163
39361
  resolvedModel: null,
39164
39362
  credentialId: null,
@@ -39181,7 +39379,8 @@ function proxyRoutes(deps) {
39181
39379
  const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter(deps.now);
39182
39380
  const dispatchDeps = {
39183
39381
  ...deps,
39184
- snapshots: deps.snapshots ?? createRoutingSnapshotCache(deps.store)
39382
+ snapshots: deps.snapshots ?? createRoutingSnapshotCache(deps.store),
39383
+ keepaliveMs: deps.keepaliveMs ?? KEEPALIVE_MS
39185
39384
  };
39186
39385
  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 }) => {
39187
39386
  try {
@@ -39410,6 +39609,9 @@ var store = await createStore({
39410
39609
  path: config2.databasePath,
39411
39610
  encryptionKey
39412
39611
  });
39612
+ var swept = await store.usage.sweepPending();
39613
+ if (swept > 0)
39614
+ console.log(`retired ${swept} request(s) interrupted by the last shutdown`);
39413
39615
  var now = () => Date.now();
39414
39616
  var http = nodeHttpClient();
39415
39617
  var refresh = createRefresher({ store, providers: OAUTH_PROVIDERS, http, now });
@@ -39430,7 +39632,7 @@ var stopQuotaPoller = await startQuotaPoller({
39430
39632
  refresh,
39431
39633
  now
39432
39634
  });
39433
- app.listen({ port: config2.port, hostname: config2.host });
39635
+ app.listen({ port: config2.port, hostname: config2.host, idleTimeout: 255 });
39434
39636
  console.log(`omnigateway listening on http://${config2.host}:${config2.port}`);
39435
39637
  var shuttingDown = false;
39436
39638
  function exitAfterClosingStore(code) {