omnigateway 0.6.0 → 0.7.0

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 CHANGED
@@ -182,6 +182,9 @@ Connect an account. The CLI prints a URL to open, and waits:
182
182
  omni connect anthropic # or: openai, kimi, kilo, grok
183
183
  ```
184
184
 
185
+ An installed [plugin](#plugins) that supplies its own provider is connected the
186
+ same way, under its plugin id.
187
+
185
188
  Every provider also takes a plain API key, if that is what you hold rather than
186
189
  a subscription:
187
190
 
@@ -741,13 +744,26 @@ and whether the gateway would load it:
741
744
 
742
745
  ```
743
746
  ID NAME VERSION API SDK CAPABILITIES STATE
744
- pokemon Pokémon Companion 1.0.0 1 ^1.0.0 storage,files,net:outbound,… ok
747
+ pokemon Pokémon Companion 1.0.0 2 ^1.0.0 storage,files,net:outbound,… ok
745
748
  ```
746
749
 
750
+ The API column is the plugin API generation, and it is matched **exactly** — a
751
+ plugin built against an older one is listed with that as its reason rather than
752
+ loaded.
753
+
747
754
  The capabilities a manifest may declare are `storage`, `files`, `net:outbound`,
748
- `events:request`, `events:limit` and `channels` — the last being namespaced
749
- topics on the gateway's push socket, which a plugin owns without ever touching a
750
- connection. Anything a plugin did not declare is absent from what it is handed.
755
+ `events:request`, `events:limit`, `channels` and `provider`. `channels` is
756
+ namespaced topics on the gateway's push socket, which a plugin owns without ever
757
+ touching a connection. `provider` lets a plugin supply a provider of its own
758
+ the models, the wire format, and optionally the OAuth flow, so
759
+ `omni connect <plugin-id>` works for it exactly as it does for a built-in.
760
+ Anything a plugin did not declare is absent from what it is handed.
761
+
762
+ `net:outbound` and `provider` each require the manifest to declare `origins`,
763
+ and both are enforced: a plugin's own `fetch` and the requests the gateway makes
764
+ on a provider plugin's behalf are both refused outside them. That is what makes
765
+ `omni plugin verify <id>` worth reading before you install one — where your
766
+ prompts can be sent is in the manifest, not only in the code.
751
767
 
752
768
  A plugin that would *not* load is listed with the reason rather than hidden,
753
769
  because a plugin missing from the console is exactly what you are trying to
package/bin/omni.js CHANGED
@@ -1687,6 +1687,7 @@ var MIGRATIONS = [
1687
1687
  function openDb(path) {
1688
1688
  const db = new Database(path, { create: true });
1689
1689
  db.run("PRAGMA journal_mode = WAL");
1690
+ db.run("PRAGMA synchronous = NORMAL");
1690
1691
  db.run("PRAGMA foreign_keys = ON");
1691
1692
  db.run("PRAGMA busy_timeout = 5000");
1692
1693
  db.run("CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL)");
@@ -1848,6 +1849,18 @@ function estimateInputTokens(request) {
1848
1849
  total += toolTokens(tool);
1849
1850
  return total;
1850
1851
  }
1852
+ function estimateInputPrefixes(request) {
1853
+ let tools = 0;
1854
+ for (const tool of request.tools ?? [])
1855
+ tools += toolTokens(tool);
1856
+ let toolsAndSystem = tools;
1857
+ for (const block of request.system ?? [])
1858
+ toolsAndSystem += blockTokens(block);
1859
+ let total = toolsAndSystem;
1860
+ for (const message of request.messages)
1861
+ total += messageTokens(message);
1862
+ return { tools, toolsAndSystem, total };
1863
+ }
1851
1864
  function estimateCachedInputTokens(request) {
1852
1865
  let running = 0;
1853
1866
  let cached = 0;
@@ -19054,7 +19067,7 @@ import { basename as basename2, join as join3, resolve as resolve2, sep as sep2
19054
19067
 
19055
19068
  // packages/plugin-api/src/version.ts
19056
19069
  var PLUGIN_API_VERSION = 2;
19057
- var DASHBOARD_SDK_VERSION = "0.1.3";
19070
+ var DASHBOARD_SDK_VERSION = "0.1.4";
19058
19071
  // packages/plugin-api/src/manifest.ts
19059
19072
  var CAPABILITIES = [
19060
19073
  "storage",
@@ -20375,47 +20388,77 @@ var PROFILES = {
20375
20388
  Object.setPrototypeOf(PROFILES, null);
20376
20389
 
20377
20390
  // packages/providers/src/sse.ts
20391
+ var HOLDBACK = 2;
20392
+ var MAX_RECORD_CHARS = 10 * 1024 * 1024;
20378
20393
  async function* parseSse(body) {
20379
20394
  const reader = body.getReader();
20380
20395
  const decoder3 = new TextDecoder;
20381
- let buf = "";
20396
+ const pending = [];
20397
+ let pendingChars = 0;
20398
+ let tail = "";
20382
20399
  try {
20383
20400
  while (true) {
20384
20401
  const { done, value } = await reader.read();
20385
20402
  if (done)
20386
20403
  break;
20387
- buf += decoder3.decode(value, { stream: true });
20388
- buf = buf.replaceAll(`\r
20389
- `, `
20390
- `);
20391
- let sep3 = buf.indexOf(`
20392
-
20404
+ const probe = tail + decoder3.decode(value, { stream: true });
20405
+ let start = 0;
20406
+ let nl = probe.indexOf(`
20393
20407
  `);
20394
- while (sep3 !== -1) {
20395
- const record2 = buf.slice(0, sep3);
20396
- buf = buf.slice(sep3 + 2);
20408
+ while (nl !== -1) {
20409
+ const end = separatorEnd(probe, nl);
20410
+ if (end === -1) {
20411
+ nl = probe.indexOf(`
20412
+ `, nl + 1);
20413
+ continue;
20414
+ }
20415
+ refuseIfOversized(pendingChars + (nl - start));
20416
+ pending.push(probe.slice(start, nl));
20417
+ const record2 = pending.join("");
20418
+ pending.length = 0;
20419
+ pendingChars = 0;
20397
20420
  const msg = parseRecord(record2);
20398
20421
  if (msg)
20399
20422
  yield msg;
20400
- sep3 = buf.indexOf(`
20401
-
20402
- `);
20403
- }
20404
- }
20405
- const tail = parseRecord(buf.replaceAll(`\r
20406
- `, `
20407
- `));
20408
- if (tail)
20409
- yield tail;
20423
+ start = end;
20424
+ nl = probe.indexOf(`
20425
+ `, end);
20426
+ }
20427
+ const keep2 = Math.min(HOLDBACK, probe.length - start);
20428
+ const settled = probe.slice(start, probe.length - keep2);
20429
+ if (settled.length > 0) {
20430
+ pending.push(settled);
20431
+ pendingChars += settled.length;
20432
+ }
20433
+ tail = probe.slice(probe.length - keep2);
20434
+ refuseIfOversized(pendingChars + tail.length);
20435
+ }
20436
+ const last = parseRecord(pending.join("") + tail);
20437
+ if (last)
20438
+ yield last;
20410
20439
  } finally {
20411
20440
  reader.releaseLock();
20412
20441
  }
20413
20442
  }
20443
+ function refuseIfOversized(chars) {
20444
+ if (chars <= MAX_RECORD_CHARS)
20445
+ return;
20446
+ throw new GatewayError("UPSTREAM", `upstream SSE record exceeded ${MAX_RECORD_CHARS} characters`, { gatewayAuthored: true });
20447
+ }
20448
+ function separatorEnd(text, at) {
20449
+ const next = text.charCodeAt(at + 1);
20450
+ if (next === 10)
20451
+ return at + 2;
20452
+ if (next === 13 && text.charCodeAt(at + 2) === 10)
20453
+ return at + 3;
20454
+ return -1;
20455
+ }
20414
20456
  function parseRecord(record2) {
20415
20457
  let event = "message";
20416
20458
  const data = [];
20417
- for (const line of record2.split(`
20459
+ for (const rawLine of record2.split(`
20418
20460
  `)) {
20461
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
20419
20462
  if (line.length === 0 || line.startsWith(":"))
20420
20463
  continue;
20421
20464
  const colon = line.indexOf(":");
@@ -20891,7 +20934,7 @@ async function* decodeAnthropic(messages, opts = {}) {
20891
20934
  case "message_delta": {
20892
20935
  const reason = d.delta?.stop_reason;
20893
20936
  if (typeof reason === "string") {
20894
- const mapped = STOP_REASON[reason];
20937
+ const mapped = Object.hasOwn(STOP_REASON, reason) ? STOP_REASON[reason] : undefined;
20895
20938
  if (mapped === undefined) {
20896
20939
  yield protocolError(`unrecognized Anthropic stop reason "${reason}"`);
20897
20940
  return;
@@ -20921,7 +20964,7 @@ async function* decodeAnthropic(messages, opts = {}) {
20921
20964
  terminal = true;
20922
20965
  const type = String(d.error?.type);
20923
20966
  const message = String(d.error?.message ?? "upstream error");
20924
- const code = isFingerprintRefusal(type, message) ? "FINGERPRINT_REFUSED" : ERROR_TYPE[type] ?? "UPSTREAM";
20967
+ const code = isFingerprintRefusal(type, message) ? "FINGERPRINT_REFUSED" : (Object.hasOwn(ERROR_TYPE, type) ? ERROR_TYPE[type] : undefined) ?? "UPSTREAM";
20925
20968
  yield { type: "error", code, message, retryable: RETRYABLE[code] };
20926
20969
  break;
20927
20970
  }
@@ -21127,14 +21170,15 @@ function addAutoCacheBreakpoints(body, req, note2) {
21127
21170
  let markedPrefix = 0;
21128
21171
  const worthAMarker = (prefix) => prefix - markedPrefix >= AUTO_CACHE_MIN_TOKENS;
21129
21172
  let stablePrefixMarked = false;
21130
- const toolsPrefix = estimateInputTokens({ ...req, messages: [], system: [] });
21173
+ const prefixes = estimateInputPrefixes(req);
21174
+ const toolsPrefix = prefixes.tools;
21131
21175
  const lastTool = body.tools?.at(-1);
21132
21176
  if (lastTool !== undefined && worthAMarker(toolsPrefix)) {
21133
21177
  lastTool.cache_control = { type: "ephemeral" };
21134
21178
  markedPrefix = toolsPrefix;
21135
21179
  stablePrefixMarked = true;
21136
21180
  }
21137
- const systemPrefix = estimateInputTokens({ ...req, messages: [] });
21181
+ const systemPrefix = prefixes.toolsAndSystem;
21138
21182
  const lastSystem = body.system?.at(-1);
21139
21183
  if (lastSystem !== undefined && worthAMarker(systemPrefix)) {
21140
21184
  lastSystem.cache_control = { type: "ephemeral" };
@@ -21143,7 +21187,7 @@ function addAutoCacheBreakpoints(body, req, note2) {
21143
21187
  }
21144
21188
  if (stablePrefixMarked)
21145
21189
  note2("anthropic:cache-breakpoint-added");
21146
- if (!worthAMarker(estimateInputTokens(req)))
21190
+ if (!worthAMarker(prefixes.total))
21147
21191
  return;
21148
21192
  const lastHistory = lastCacheableHistoryBlock(body.messages);
21149
21193
  if (lastHistory === undefined)
@@ -21438,7 +21482,18 @@ async function* decodeCustomChat(messages) {
21438
21482
  }
21439
21483
  }
21440
21484
  if (typeof choice.finish_reason === "string") {
21441
- stopReason = CHAT_FINISH[choice.finish_reason] ?? "endTurn";
21485
+ const reason = choice.finish_reason;
21486
+ const mapped = Object.hasOwn(CHAT_FINISH, reason) ? CHAT_FINISH[reason] : undefined;
21487
+ if (mapped === undefined) {
21488
+ yield {
21489
+ type: "error",
21490
+ code: "UPSTREAM",
21491
+ message: `unrecognized custom endpoint finish reason "${reason}"`,
21492
+ retryable: false
21493
+ };
21494
+ return;
21495
+ }
21496
+ stopReason = mapped;
21442
21497
  }
21443
21498
  }
21444
21499
  if (done) {
@@ -21462,6 +21517,32 @@ var RESPONSES_ERROR_CODE = {
21462
21517
  context_length_exceeded: "BAD_REQUEST",
21463
21518
  content_policy_violation: "CONTENT_FILTER"
21464
21519
  };
21520
+ var KNOWN_RESPONSES_EVENTS = new Set([
21521
+ "response.created",
21522
+ "response.queued",
21523
+ "response.in_progress",
21524
+ "response.output_item.added",
21525
+ "response.output_item.done",
21526
+ "response.content_part.added",
21527
+ "response.content_part.done",
21528
+ "response.output_text.delta",
21529
+ "response.output_text.done",
21530
+ "response.output_text.annotation.added",
21531
+ "response.refusal.delta",
21532
+ "response.refusal.done",
21533
+ "response.reasoning_summary_part.added",
21534
+ "response.reasoning_summary_part.done",
21535
+ "response.reasoning_summary_text.delta",
21536
+ "response.reasoning_summary_text.done",
21537
+ "response.reasoning_text.delta",
21538
+ "response.reasoning_text.done",
21539
+ "response.function_call_arguments.delta",
21540
+ "response.function_call_arguments.done",
21541
+ "response.completed",
21542
+ "response.incomplete",
21543
+ "response.failed",
21544
+ "error"
21545
+ ]);
21465
21546
  async function* decodeCustomResponses(messages) {
21466
21547
  const indices = new Map;
21467
21548
  let next = 0;
@@ -21478,6 +21559,17 @@ async function* decodeCustomResponses(messages) {
21478
21559
  let terminal = false;
21479
21560
  const ownsBlock = new Set;
21480
21561
  for await (const msg of messages) {
21562
+ if (msg.data === "[DONE]")
21563
+ continue;
21564
+ if (!KNOWN_RESPONSES_EVENTS.has(msg.event)) {
21565
+ yield {
21566
+ type: "error",
21567
+ code: "UPSTREAM",
21568
+ message: `unrecognized custom endpoint stream event "${msg.event}"`,
21569
+ retryable: false
21570
+ };
21571
+ return;
21572
+ }
21481
21573
  const d = json3(msg.data);
21482
21574
  if (d === null)
21483
21575
  continue;
@@ -21559,6 +21651,15 @@ async function* decodeCustomResponses(messages) {
21559
21651
  stopReason = "maxTokens";
21560
21652
  else if (reason === "content_filter")
21561
21653
  stopReason = "contentFilter";
21654
+ else if (reason !== undefined || msg.event === "response.incomplete" || r.status !== undefined && r.status !== "completed") {
21655
+ yield {
21656
+ type: "error",
21657
+ code: "UPSTREAM",
21658
+ message: reason !== undefined ? `unrecognized custom endpoint incomplete reason "${String(reason)}"` : r.status !== undefined && r.status !== "incomplete" ? `custom endpoint reported terminal response status "${String(r.status)}"` : "custom endpoint reported the response incomplete without a reason",
21659
+ retryable: false
21660
+ };
21661
+ break;
21662
+ }
21562
21663
  yield {
21563
21664
  type: "end",
21564
21665
  stopReason,
@@ -21570,7 +21671,8 @@ async function* decodeCustomResponses(messages) {
21570
21671
  case "error": {
21571
21672
  terminal = true;
21572
21673
  const err = d.response?.error ?? d.error ?? {};
21573
- const code = RESPONSES_ERROR_CODE[String(err.code ?? err.type)] ?? "UPSTREAM";
21674
+ const raw = String(err.code ?? err.type);
21675
+ const code = (Object.hasOwn(RESPONSES_ERROR_CODE, raw) ? RESPONSES_ERROR_CODE[raw] : undefined) ?? "UPSTREAM";
21574
21676
  yield {
21575
21677
  type: "error",
21576
21678
  code,
@@ -22023,6 +22125,15 @@ async function* decodeGrokResponses(messages) {
22023
22125
  stopReason = "maxTokens";
22024
22126
  else if (reason === "content_filter")
22025
22127
  stopReason = "contentFilter";
22128
+ else if (reason !== undefined || msg.event === "response.incomplete" || r.status !== undefined && r.status !== "completed") {
22129
+ yield {
22130
+ type: "error",
22131
+ code: "UPSTREAM",
22132
+ message: reason !== undefined ? `unrecognized xAI incomplete reason "${String(reason)}"` : r.status !== undefined && r.status !== "incomplete" ? `xAI reported terminal response status "${String(r.status)}"` : "xAI reported the response incomplete without a reason",
22133
+ retryable: false
22134
+ };
22135
+ break;
22136
+ }
22026
22137
  yield {
22027
22138
  type: "end",
22028
22139
  stopReason,
@@ -22034,7 +22145,8 @@ async function* decodeGrokResponses(messages) {
22034
22145
  case "error": {
22035
22146
  terminal = true;
22036
22147
  const err = d.response?.error ?? d.error ?? {};
22037
- const code = ERROR_CODE[String(err.code ?? err.type)] ?? "UPSTREAM";
22148
+ const raw = String(err.code ?? err.type);
22149
+ const code = (Object.hasOwn(ERROR_CODE, raw) ? ERROR_CODE[raw] : undefined) ?? "UPSTREAM";
22038
22150
  yield {
22039
22151
  type: "error",
22040
22152
  code,
@@ -22450,7 +22562,18 @@ async function* decodeKiloChat(messages) {
22450
22562
  }
22451
22563
  }
22452
22564
  if (typeof choice.finish_reason === "string") {
22453
- stopReason = FINISH[choice.finish_reason] ?? "endTurn";
22565
+ const reason = choice.finish_reason;
22566
+ const mapped = Object.hasOwn(FINISH, reason) ? FINISH[reason] : undefined;
22567
+ if (mapped === undefined) {
22568
+ yield {
22569
+ type: "error",
22570
+ code: "UPSTREAM",
22571
+ message: `unrecognized Kilo finish reason "${reason}"`,
22572
+ retryable: false
22573
+ };
22574
+ return;
22575
+ }
22576
+ stopReason = mapped;
22454
22577
  }
22455
22578
  }
22456
22579
  if (done) {
@@ -22737,7 +22860,18 @@ async function* decodeChat(messages) {
22737
22860
  }
22738
22861
  }
22739
22862
  if (typeof choice.finish_reason === "string") {
22740
- stopReason = FINISH2[choice.finish_reason] ?? "endTurn";
22863
+ const reason = choice.finish_reason;
22864
+ const mapped = Object.hasOwn(FINISH2, reason) ? FINISH2[reason] : undefined;
22865
+ if (mapped === undefined) {
22866
+ yield {
22867
+ type: "error",
22868
+ code: "UPSTREAM",
22869
+ message: `unrecognized Kimi finish reason "${reason}"`,
22870
+ retryable: false
22871
+ };
22872
+ return;
22873
+ }
22874
+ stopReason = mapped;
22741
22875
  }
22742
22876
  }
22743
22877
  if (done) {
@@ -22895,6 +23029,32 @@ var ERROR_CODE2 = {
22895
23029
  context_length_exceeded: "BAD_REQUEST",
22896
23030
  content_policy_violation: "CONTENT_FILTER"
22897
23031
  };
23032
+ var KNOWN_EVENTS3 = new Set([
23033
+ "response.created",
23034
+ "response.queued",
23035
+ "response.in_progress",
23036
+ "response.output_item.added",
23037
+ "response.output_item.done",
23038
+ "response.content_part.added",
23039
+ "response.content_part.done",
23040
+ "response.output_text.delta",
23041
+ "response.output_text.done",
23042
+ "response.output_text.annotation.added",
23043
+ "response.refusal.delta",
23044
+ "response.refusal.done",
23045
+ "response.reasoning_summary_part.added",
23046
+ "response.reasoning_summary_part.done",
23047
+ "response.reasoning_summary_text.delta",
23048
+ "response.reasoning_summary_text.done",
23049
+ "response.reasoning_text.delta",
23050
+ "response.reasoning_text.done",
23051
+ "response.function_call_arguments.delta",
23052
+ "response.function_call_arguments.done",
23053
+ "response.completed",
23054
+ "response.incomplete",
23055
+ "response.failed",
23056
+ "error"
23057
+ ]);
22898
23058
  function json7(data) {
22899
23059
  try {
22900
23060
  const v = JSON.parse(data);
@@ -22919,6 +23079,17 @@ async function* decodeResponses(messages) {
22919
23079
  let terminal = false;
22920
23080
  const ownsBlock = new Set;
22921
23081
  for await (const msg of messages) {
23082
+ if (msg.data === "[DONE]")
23083
+ continue;
23084
+ if (!KNOWN_EVENTS3.has(msg.event)) {
23085
+ yield {
23086
+ type: "error",
23087
+ code: "UPSTREAM",
23088
+ message: `unrecognized OpenAI stream event "${msg.event}"`,
23089
+ retryable: false
23090
+ };
23091
+ return;
23092
+ }
22922
23093
  const d = json7(msg.data);
22923
23094
  if (d === null)
22924
23095
  continue;
@@ -23000,6 +23171,15 @@ async function* decodeResponses(messages) {
23000
23171
  stopReason = "maxTokens";
23001
23172
  else if (reason === "content_filter")
23002
23173
  stopReason = "contentFilter";
23174
+ else if (reason !== undefined || msg.event === "response.incomplete" || r.status !== undefined && r.status !== "completed") {
23175
+ yield {
23176
+ type: "error",
23177
+ code: "UPSTREAM",
23178
+ message: reason !== undefined ? `unrecognized OpenAI incomplete reason "${String(reason)}"` : r.status !== undefined && r.status !== "incomplete" ? `OpenAI reported terminal response status "${String(r.status)}"` : "OpenAI reported the response incomplete without a reason",
23179
+ retryable: false
23180
+ };
23181
+ break;
23182
+ }
23003
23183
  yield {
23004
23184
  type: "end",
23005
23185
  stopReason,
@@ -23011,7 +23191,8 @@ async function* decodeResponses(messages) {
23011
23191
  case "error": {
23012
23192
  terminal = true;
23013
23193
  const err = d.response?.error ?? d.error ?? {};
23014
- const code = ERROR_CODE2[String(err.code ?? err.type)] ?? "UPSTREAM";
23194
+ const raw = String(err.code ?? err.type);
23195
+ const code = (Object.hasOwn(ERROR_CODE2, raw) ? ERROR_CODE2[raw] : undefined) ?? "UPSTREAM";
23015
23196
  yield {
23016
23197
  type: "error",
23017
23198
  code,
@@ -27579,7 +27760,7 @@ function atomicWriteFile(path, contents, ops) {
27579
27760
  }
27580
27761
 
27581
27762
  // apps/cli/src/run.ts
27582
- var VERSION = "0.0.0";
27763
+ var VERSION = "0.7.0";
27583
27764
  async function run(argv, writer, options = {}) {
27584
27765
  const wants = (...flags) => argv.some((token) => flags.includes(token));
27585
27766
  if (wants("--version")) {