omnigateway 0.1.2 → 0.1.3

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 (3) hide show
  1. package/bin/omni.js +39 -14
  2. package/gateway.js +77 -18
  3. package/package.json +1 -1
package/bin/omni.js CHANGED
@@ -608,6 +608,7 @@ async function* decodeAnthropic(messages) {
608
608
  let cacheWriteTokens = 0;
609
609
  let outputTokens = 0;
610
610
  let stopReason = "endTurn";
611
+ let terminal = false;
611
612
  for await (const msg of messages) {
612
613
  const d = json(msg.data);
613
614
  if (d === null)
@@ -673,6 +674,7 @@ async function* decodeAnthropic(messages) {
673
674
  break;
674
675
  }
675
676
  case "message_stop":
677
+ terminal = true;
676
678
  yield {
677
679
  type: "end",
678
680
  stopReason,
@@ -680,6 +682,7 @@ async function* decodeAnthropic(messages) {
680
682
  };
681
683
  break;
682
684
  case "error": {
685
+ terminal = true;
683
686
  const code = ERROR_TYPE[String(d.error?.type)] ?? "UPSTREAM";
684
687
  yield {
685
688
  type: "error",
@@ -693,6 +696,14 @@ async function* decodeAnthropic(messages) {
693
696
  break;
694
697
  }
695
698
  }
699
+ if (!terminal) {
700
+ yield {
701
+ type: "error",
702
+ code: "UPSTREAM",
703
+ message: "upstream stream ended before message_stop",
704
+ retryable: RETRYABLE.UPSTREAM
705
+ };
706
+ }
696
707
  }
697
708
 
698
709
  // packages/providers/src/anthropic/wire.ts
@@ -805,9 +816,10 @@ var anthropicAdapter = {
805
816
  ["anthropic-version", API_VERSION],
806
817
  ["Accept", req.request.stream ? "text/event-stream" : "application/json"]
807
818
  ];
819
+ const betas = new Set(req.request.betas ?? []);
808
820
  if (oauth) {
809
821
  protocol.push(["Authorization", `Bearer ${req.credentials.accessToken}`]);
810
- protocol.push(["anthropic-beta", OAUTH_BETA]);
822
+ betas.add(OAUTH_BETA);
811
823
  } else if (req.credentials.apiKey !== null) {
812
824
  protocol.push(["x-api-key", req.credentials.apiKey]);
813
825
  } else {
@@ -815,6 +827,8 @@ var anthropicAdapter = {
815
827
  provider: "anthropic"
816
828
  });
817
829
  }
830
+ if (betas.size > 0)
831
+ protocol.push(["anthropic-beta", [...betas].join(",")]);
818
832
  const profile = PROFILES.anthropic;
819
833
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
820
834
  const bodyString = signAnthropicBody(JSON.stringify(orderFields(withSystem, BODY_ORDER.anthropic)));
@@ -1032,18 +1046,16 @@ async function* decodeChat(messages) {
1032
1046
  let started = false;
1033
1047
  let textOpen = false;
1034
1048
  let textIndex;
1035
- let ended = false;
1049
+ let done = false;
1036
1050
  let stopReason = "endTurn";
1037
1051
  let usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
1038
1052
  const toolIndex = new Map;
1039
1053
  let nextIndex = 0;
1040
- const emitEnd = () => {
1041
- ended = true;
1042
- return { type: "end", stopReason, usage };
1043
- };
1044
1054
  for await (const msg of messages) {
1045
- if (msg.data === "[DONE]")
1055
+ if (msg.data === "[DONE]") {
1056
+ done = true;
1046
1057
  break;
1058
+ }
1047
1059
  const d = json2(msg.data);
1048
1060
  if (d === null)
1049
1061
  continue;
@@ -1098,19 +1110,21 @@ async function* decodeChat(messages) {
1098
1110
  }
1099
1111
  if (typeof choice.finish_reason === "string") {
1100
1112
  stopReason = FINISH[choice.finish_reason] ?? "endTurn";
1101
- if (textOpen)
1102
- yield { type: "blockEnd", index: textIndex ?? 0 };
1103
- for (const index of toolIndex.values())
1104
- yield { type: "blockEnd", index };
1105
- yield emitEnd();
1106
1113
  }
1107
1114
  }
1108
- if (!ended) {
1115
+ if (done) {
1109
1116
  if (textOpen)
1110
1117
  yield { type: "blockEnd", index: textIndex ?? 0 };
1111
1118
  for (const index of toolIndex.values())
1112
1119
  yield { type: "blockEnd", index };
1113
- yield emitEnd();
1120
+ yield { type: "end", stopReason, usage };
1121
+ } else {
1122
+ yield {
1123
+ type: "error",
1124
+ code: "UPSTREAM",
1125
+ message: "upstream stream ended before [DONE]",
1126
+ retryable: RETRYABLE.UPSTREAM
1127
+ };
1114
1128
  }
1115
1129
  }
1116
1130
 
@@ -1265,6 +1279,7 @@ async function* decodeResponses(messages) {
1265
1279
  return assigned;
1266
1280
  };
1267
1281
  let sawToolCall = false;
1282
+ let terminal = false;
1268
1283
  const ownsBlock = new Set;
1269
1284
  for await (const msg of messages) {
1270
1285
  const d = json3(msg.data);
@@ -1340,6 +1355,7 @@ async function* decodeResponses(messages) {
1340
1355
  }
1341
1356
  case "response.completed":
1342
1357
  case "response.incomplete": {
1358
+ terminal = true;
1343
1359
  const r = d.response ?? {};
1344
1360
  const reason = r.incomplete_details?.reason;
1345
1361
  let stopReason = sawToolCall ? "toolUse" : "endTurn";
@@ -1361,6 +1377,7 @@ async function* decodeResponses(messages) {
1361
1377
  }
1362
1378
  case "response.failed":
1363
1379
  case "error": {
1380
+ terminal = true;
1364
1381
  const err = d.response?.error ?? d.error ?? {};
1365
1382
  const code = ERROR_CODE[String(err.code ?? err.type)] ?? "UPSTREAM";
1366
1383
  yield {
@@ -1375,6 +1392,14 @@ async function* decodeResponses(messages) {
1375
1392
  break;
1376
1393
  }
1377
1394
  }
1395
+ if (!terminal) {
1396
+ yield {
1397
+ type: "error",
1398
+ code: "UPSTREAM",
1399
+ message: "upstream stream ended before response completion",
1400
+ retryable: RETRYABLE.UPSTREAM
1401
+ };
1402
+ }
1378
1403
  }
1379
1404
 
1380
1405
  // packages/providers/src/openai/wire.ts
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
@@ -38319,12 +38344,18 @@ async function dispatch(request2, deps, signal) {
38319
38344
  }), dispatchSignal);
38320
38345
  for (const d of result.degradations)
38321
38346
  log.degradations.push(d);
38347
+ let terminal = false;
38348
+ const pending = [];
38322
38349
  for await (const event of result.events) {
38323
38350
  if (event.type === "blockDelta" && !committed) {
38324
38351
  committed = true;
38325
38352
  log.ttftMs = deps.now() - startedAt;
38353
+ for (const buffered of pending)
38354
+ yield buffered;
38355
+ pending.length = 0;
38326
38356
  }
38327
38357
  if (event.type === "end") {
38358
+ terminal = true;
38328
38359
  log.inputTokens = event.usage.inputTokens;
38329
38360
  log.outputTokens = event.usage.outputTokens;
38330
38361
  log.cacheReadTokens = event.usage.cacheReadTokens;
@@ -38332,6 +38363,7 @@ async function dispatch(request2, deps, signal) {
38332
38363
  log.costUsd = priceOf(candidate, event.usage);
38333
38364
  }
38334
38365
  if (event.type === "error") {
38366
+ terminal = true;
38335
38367
  if (!committed && RETRYABLE[event.code]) {
38336
38368
  throw new GatewayError(event.code, event.message);
38337
38369
  }
@@ -38348,7 +38380,21 @@ async function dispatch(request2, deps, signal) {
38348
38380
  log.durationMs = deps.now() - startedAt;
38349
38381
  return;
38350
38382
  }
38351
- yield event;
38383
+ if (!committed && event.type !== "end") {
38384
+ pending.push(event);
38385
+ } else {
38386
+ if (!committed) {
38387
+ for (const buffered of pending)
38388
+ yield buffered;
38389
+ pending.length = 0;
38390
+ }
38391
+ yield event;
38392
+ }
38393
+ if (event.type === "end")
38394
+ break;
38395
+ }
38396
+ if (!terminal) {
38397
+ throw new GatewayError("UPSTREAM", "upstream stream ended without a terminal event");
38352
38398
  }
38353
38399
  await persistHealth(recordSuccess(healthFor(candidate), {
38354
38400
  settings: snapshot.settings,
@@ -38602,6 +38648,7 @@ var chunk = (id, created, model, choice, usage) => ({
38602
38648
  async function* openaiStream(events, requestId, created) {
38603
38649
  let model = "";
38604
38650
  let roleSent = false;
38651
+ let completed = false;
38605
38652
  const toolIndex = new Map;
38606
38653
  const emit = (data) => ({ event: "message", data: JSON.stringify(data) });
38607
38654
  for await (const event of events) {
@@ -38663,6 +38710,7 @@ async function* openaiStream(events, requestId, created) {
38663
38710
  case "blockEnd":
38664
38711
  break;
38665
38712
  case "end":
38713
+ completed = true;
38666
38714
  yield emit(chunk(requestId, created, model, { index: 0, delta: {}, finish_reason: FINISH2[event.stopReason] }, {
38667
38715
  prompt_tokens: event.usage.inputTokens,
38668
38716
  completion_tokens: event.usage.outputTokens,
@@ -38676,7 +38724,8 @@ async function* openaiStream(events, requestId, created) {
38676
38724
  }
38677
38725
  }
38678
38726
  }
38679
- yield { event: "message", data: "[DONE]" };
38727
+ if (completed)
38728
+ yield { event: "message", data: "[DONE]" };
38680
38729
  }
38681
38730
  function openaiResponse(collected, requestId, created) {
38682
38731
  const text = collected.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("");
@@ -38855,7 +38904,14 @@ function toIrReasoning(thinking, effort) {
38855
38904
  return { mode: "off" };
38856
38905
  }
38857
38906
  }
38858
- function parseAnthropicRequest(body2) {
38907
+ function readBetas(headers) {
38908
+ const raw = headers?.get("anthropic-beta");
38909
+ if (raw === undefined || raw === null)
38910
+ return [];
38911
+ const names = raw.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
38912
+ return [...new Set(names)];
38913
+ }
38914
+ function parseAnthropicRequest(body2, headers) {
38859
38915
  if (typeof body2 !== "object" || body2 === null) {
38860
38916
  throw new GatewayError("BAD_REQUEST", "request body must be a JSON object");
38861
38917
  }
@@ -38896,6 +38952,9 @@ function parseAnthropicRequest(body2) {
38896
38952
  const extras = extraFields(body2, KNOWN);
38897
38953
  if (extras !== undefined)
38898
38954
  request2.vendor = { anthropic: extras };
38955
+ const betas = readBetas(headers);
38956
+ if (betas.length > 0)
38957
+ request2.betas = betas;
38899
38958
  return validateRequest(request2);
38900
38959
  }
38901
38960
 
@@ -39129,7 +39188,7 @@ async function handle(deps, rateLimiter, surface, request2) {
39129
39188
  keyId = key.id;
39130
39189
  rateLimiter.consume(key.id, key.rateLimitPerMin);
39131
39190
  const body2 = await request2.json();
39132
- const chatRequest = surface === "anthropic" ? parseAnthropicRequest(body2) : parseOpenAIRequest(body2);
39191
+ const chatRequest = surface === "anthropic" ? parseAnthropicRequest(body2, request2.headers) : parseOpenAIRequest(body2);
39133
39192
  if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
39134
39193
  throw new GatewayError("AUTH", `model "${chatRequest.model}" is not allowed for this API key`);
39135
39194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnigateway",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Self-hosted AI gateway with Anthropic- and OpenAI-compatible APIs, an admin console, and a CLI",
5
5
  "license": "MIT",
6
6
  "author": "Harismawan <mail@harismawan.com>",