omnigateway 0.4.13 → 0.5.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.
Files changed (47) hide show
  1. package/README.md +52 -1
  2. package/bin/omni.js +176 -72
  3. package/gateway.js +1145 -126
  4. package/package.json +1 -1
  5. package/public/assets/{CopyValue-C2DkO9Yz.js → CopyValue-dQWAaTxf.js} +3 -3
  6. package/public/assets/{Field-B79Yxdit.js → Field-DTQ2LPzA.js} +1 -1
  7. package/public/assets/{Match-BWa6L0L1.js → Match-C85GObiU.js} +1 -1
  8. package/public/assets/{Panel-CoulMx6R.js → Panel-CQVz9yeR.js} +2 -2
  9. package/public/assets/Rack-V32CHmN6.js +316 -0
  10. package/public/assets/{Readout-D1XEfywe.js → Readout-DJR1qKja.js} +1 -1
  11. package/public/assets/States-CauIsA0N.js +54 -0
  12. package/public/assets/{Toggle-CRrH9nDt.js → Toggle-BQI8jz09.js} +1 -1
  13. package/public/assets/{TokenBreakdown-C2NsJnqO.js → TokenBreakdown-B2_gfuqt.js} +1 -1
  14. package/public/assets/_app-DzzXdoyU.js +1 -0
  15. package/public/assets/_app.accounts-owsnxlbn.js +64 -0
  16. package/public/assets/_app.console-bzKNP_8Y.js +44 -0
  17. package/public/assets/_app.database-CsJbkedE.js +24 -0
  18. package/public/assets/_app.index-D4ydl2mY.js +62 -0
  19. package/public/assets/_app.keys-DKX0smty.js +82 -0
  20. package/public/assets/_app.logs-CFFsj2zR.js +68 -0
  21. package/public/assets/_app.models-B0ZxWGHV.js +148 -0
  22. package/public/assets/{_app.plugins._pluginId-BOXRBToU.js → _app.plugins._pluginId-C5X4B1cE.js} +3 -3
  23. package/public/assets/{_app.settings-DoaOUmc1.js → _app.settings-Yg_6az-L.js} +4 -4
  24. package/public/assets/_app.usage-BRIz_GCn.js +83 -0
  25. package/public/assets/index-DiCU8bLB.js +178 -0
  26. package/public/assets/{login-B9CJqU--.js → login-B4LoiALI.js} +3 -3
  27. package/public/assets/plus-xu9reaJp.js +1 -0
  28. package/public/assets/queries-DuSqKybM.js +145 -0
  29. package/public/assets/{shared-Cct_4xwp.js → shared-DYkFSPbd.js} +4 -4
  30. package/public/assets/stream-Dz5HjHBh.js +1 -0
  31. package/public/assets/{trash-2-DwMK-JrZ.js → trash-2-DbutVvFY.js} +1 -1
  32. package/public/index.html +3 -3
  33. package/public/shared/dashboard-sdk.js +1 -1
  34. package/public/assets/Rack-HM36SprU.js +0 -314
  35. package/public/assets/States-DtkkMzEu.js +0 -54
  36. package/public/assets/_app-DVpIsUid.js +0 -1
  37. package/public/assets/_app.accounts-64d_cxir.js +0 -64
  38. package/public/assets/_app.console-BBuBmLRI.js +0 -44
  39. package/public/assets/_app.database-J9Ujio8l.js +0 -24
  40. package/public/assets/_app.index-BX4yKEXO.js +0 -62
  41. package/public/assets/_app.keys-2sKwhZJc.js +0 -76
  42. package/public/assets/_app.logs-DuHXYrZU.js +0 -68
  43. package/public/assets/_app.models-B0kTfPtn.js +0 -148
  44. package/public/assets/_app.usage-B5sxB40E.js +0 -83
  45. package/public/assets/index-iCfSaRSG.js +0 -178
  46. package/public/assets/plus-DKapAGoR.js +0 -1
  47. package/public/assets/queries-CX9mStps.js +0 -145
package/README.md CHANGED
@@ -402,6 +402,53 @@ supervisor; use `omni restart` from a terminal there. Shutdown is offered in
402
402
  every shape. In a container it is a one-way door: bring the process back from
403
403
  the host.
404
404
 
405
+ ### Behind a reverse proxy
406
+
407
+ Set `OMNI_BASE_URL` to the public HTTPS origin, or OAuth callbacks come back to
408
+ the wrong host.
409
+
410
+ Beyond that, two things travel badly through a proxy, and both are streams.
411
+ Client responses on `/v1/*` are server-sent events, and the console keeps one
412
+ WebSocket open on `/api/stream`. Neither is optional: buffer the first and every
413
+ token of an agent's reply arrives at once at the end, and drop the second and
414
+ the console silently falls back to polling.
415
+
416
+ Caddy and Cloudflare pass WebSockets and unbuffered responses by default and
417
+ need nothing. nginx needs telling:
418
+
419
+ ```nginx
420
+ location / {
421
+ proxy_pass http://127.0.0.1:9000;
422
+ proxy_http_version 1.1;
423
+
424
+ # Without these two the Upgrade handshake never reaches the gateway and the
425
+ # console shows LIVE·POLL instead of LIVE·PUSH. It keeps working — the
426
+ # fallback exists for exactly this — but you paid for a socket you are not
427
+ # getting.
428
+ proxy_set_header Upgrade $http_upgrade;
429
+ proxy_set_header Connection "upgrade";
430
+
431
+ proxy_set_header Host $host;
432
+ proxy_set_header X-Forwarded-Proto $scheme;
433
+
434
+ # The socket's heartbeat is 20s and it gives up on a missed pong at 60s.
435
+ # A read timeout below that closes a healthy connection from the outside,
436
+ # and the console reconnects in a loop that looks like an unstable gateway.
437
+ proxy_read_timeout 300s;
438
+
439
+ # SSE must not be buffered. The gateway already sends
440
+ # `x-accel-buffering: no` on streaming responses, which nginx honours on its
441
+ # own, so this line is belt and braces for a proxy chain where something
442
+ # else strips that header before nginx sees it.
443
+ proxy_buffering off;
444
+ }
445
+ ```
446
+
447
+ The gateway sends downstream `: keepalive` comments on streaming responses
448
+ because provider heartbeats are decoded away, so an idle stream still looks
449
+ alive to whatever sits in between. Keep any idle timeout in the proxy above
450
+ your longest expected request.
451
+
405
452
  ## Configuration
406
453
 
407
454
  Configuration is environment variables, read from the installation's `.env`:
@@ -417,7 +464,6 @@ Configuration is environment variables, read from the installation's `.env`:
417
464
  | `OMNI_LOG_LEVEL` | No | `info` | Stdout threshold: `debug`, `info`, `warn`, or `error` |
418
465
  | `OMNI_LOG_FILE` | No | the systemd journal, when there is one | Where stdout was already redirected, so the Console screen can read it back. Names a file; does not create one |
419
466
  | `OMNI_BODY_LOGGING_ALLOWED` | No | unset | Permits request/response body capture on this installation. Read at boot. Capture also needs the runtime setting; see [Recording bodies](#recording-bodies) |
420
- | `OMNI_EXPOSE_CLAUDE_CODE_ALIASES` | No | off | Advertises the reserved `claude/*` aliases on `/v1/models`. Read at boot |
421
467
  | `OMNI_ROOT` | No | the installation in the current directory, else `~/.config/omnigateway` | Which installation the CLI acts on, when `--root` is not passed |
422
468
  | `OMNI_PLUGIN_REGISTRY` | No | the public npm registry | Registry `omni plugin install <name>` resolves through; must be `https://` |
423
469
 
@@ -680,6 +726,11 @@ ID NAME VERSION API SDK CAPABILITIES
680
726
  pokemon Pokémon Companion 1.0.0 1 ^1.0.0 storage,files,net:outbound,… ok
681
727
  ```
682
728
 
729
+ The capabilities a manifest may declare are `storage`, `files`, `net:outbound`,
730
+ `events:request`, `events:limit` and `channels` — the last being namespaced
731
+ topics on the gateway's push socket, which a plugin owns without ever touching a
732
+ connection. Anything a plugin did not declare is absent from what it is handed.
733
+
683
734
  A plugin that would *not* load is listed with the reason rather than hidden,
684
735
  because a plugin missing from the console is exactly what you are trying to
685
736
  explain. For one plugin's full detail — its entry points and the outbound
package/bin/omni.js CHANGED
@@ -405,7 +405,6 @@ function loadConfig(env) {
405
405
  const baseUrl = optionalText(env.OMNI_BASE_URL, derivedBaseUrl).replace(/\/+$/, "") || derivedBaseUrl;
406
406
  const staticDir = env.OMNI_STATIC_DIR?.trim();
407
407
  const logFile = env.OMNI_LOG_FILE?.trim();
408
- const exposeClaudeCodeAliases = TRUTHY.has((env.OMNI_EXPOSE_CLAUDE_CODE_ALIASES ?? "").trim().toLowerCase());
409
408
  const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
410
409
  const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
411
410
  const logLevel = parseLogLevel(rawLogLevel);
@@ -418,7 +417,6 @@ function loadConfig(env) {
418
417
  encryptionKey,
419
418
  baseUrl,
420
419
  staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
421
- exposeClaudeCodeAliases,
422
420
  bodyLoggingAllowed,
423
421
  logFile: logFile === undefined || logFile.length === 0 ? null : logFile
424
422
  };
@@ -18593,6 +18591,7 @@ var targetSchema = exports_external.discriminatedUnion("provider", [
18593
18591
  }),
18594
18592
  contextWindow: exports_external.number().int().positive().optional(),
18595
18593
  maxOutputTokens: exports_external.number().int().positive().optional(),
18594
+ credentialId: exports_external.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id").optional(),
18596
18595
  capabilities: exports_external.object({
18597
18596
  tools: exports_external.boolean(),
18598
18597
  images: exports_external.boolean(),
@@ -18614,6 +18613,7 @@ var targetSchema = exports_external.discriminatedUnion("provider", [
18614
18613
  }),
18615
18614
  contextWindow: exports_external.number().int().positive().optional(),
18616
18615
  maxOutputTokens: exports_external.number().int().positive().optional(),
18616
+ credentialId: exports_external.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id").optional(),
18617
18617
  capabilities: exports_external.object({
18618
18618
  tools: exports_external.boolean(),
18619
18619
  images: exports_external.boolean(),
@@ -18622,9 +18622,7 @@ var targetSchema = exports_external.discriminatedUnion("provider", [
18622
18622
  }).strict()
18623
18623
  ]);
18624
18624
  var modelSchema = exports_external.object({
18625
- id: exports_external.string().min(1).refine((value) => !value.toLowerCase().startsWith("claude/"), {
18626
- message: 'model id must not start with "claude/": that prefix is reserved for discovery mirrors'
18627
- }),
18625
+ id: exports_external.string().min(1),
18628
18626
  strategy: exports_external.enum(["score", "priority", "roundRobin", "weighted"]),
18629
18627
  isAlias: exports_external.boolean(),
18630
18628
  targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
@@ -18636,6 +18634,9 @@ var keyCreateSchema = exports_external.object({
18636
18634
  bodyLoggingOptOut: exports_external.boolean().default(false)
18637
18635
  }).strict();
18638
18636
  var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
18637
+ var keyModelsSchema = exports_external.object({
18638
+ modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
18639
+ }).strict();
18639
18640
  var retentionSchema = exports_external.object({
18640
18641
  keepLatest: exports_external.number().int().min(1).max(100),
18641
18642
  maxAgeDays: exports_external.number().int().min(1).max(3650)
@@ -18733,13 +18734,76 @@ async function readSource(deps, source, lines) {
18733
18734
  ]);
18734
18735
  return result.code === 0 ? result.stdout : "";
18735
18736
  }
18737
+ function parseConsoleLines(text, query) {
18738
+ const lines = consoleLimit(query.lines);
18739
+ return text.split(`
18740
+ `).filter((raw) => raw.trim().length > 0).map(parseLine).filter((line) => keep(line, query)).slice(-lines);
18741
+ }
18736
18742
  async function readConsole(deps, source, query) {
18737
18743
  const limited = { ...query, lines: consoleLimit(query.lines) };
18738
18744
  const text = await readSource(deps, source, scanWidth(limited));
18739
- const lines = text.split(`
18740
- `).filter((raw) => raw.trim().length > 0).map(parseLine).filter((line) => keep(line, limited)).slice(-limited.lines);
18745
+ const lines = parseConsoleLines(text, limited);
18741
18746
  return source.kind === "file" ? { source: "file", path: source.path, lines } : { source: source.kind, lines };
18742
18747
  }
18748
+ // packages/store/src/types.ts
18749
+ var WINDOW_DURATION_MS = {
18750
+ fiveHour: 5 * 60 * 60 * 1000,
18751
+ daily: 24 * 60 * 60 * 1000,
18752
+ weekly: 7 * 24 * 60 * 60 * 1000
18753
+ };
18754
+ function durationFor(windowType, windowMs) {
18755
+ return windowMs !== null && windowMs > 0 ? windowMs : WINDOW_DURATION_MS[windowType];
18756
+ }
18757
+ var SAME_WINDOW_TOLERANCE_MS = 60000;
18758
+ function sameWindow(a, b) {
18759
+ if (a === null || b === null)
18760
+ return a === b;
18761
+ return Math.abs(a - b) <= SAME_WINDOW_TOLERANCE_MS;
18762
+ }
18763
+ function quotaVerdict(window, estimate) {
18764
+ if (estimate === undefined)
18765
+ return "unknown";
18766
+ if (window.observedAt > 0 && estimate.stale)
18767
+ return "stale";
18768
+ if (estimate.ratePerHour === null || window.limit === null)
18769
+ return "unknown";
18770
+ if (estimate.survives === false && estimate.exhaustsAt !== null)
18771
+ return "empty";
18772
+ return estimate.survives === true ? "ok" : "unknown";
18773
+ }
18774
+ var READ_OVER_INPUT = 0.1;
18775
+ function cacheReadRate(prices) {
18776
+ return prices.cacheRead ?? prices.input * READ_OVER_INPUT;
18777
+ }
18778
+ function servesTarget(target, account) {
18779
+ if (account.provider !== target.provider)
18780
+ return false;
18781
+ if (target.provider === "custom" && account.providerData.endpointId !== target.endpointId) {
18782
+ return false;
18783
+ }
18784
+ return target.credentialId === undefined || account.id === target.credentialId;
18785
+ }
18786
+ function resolvePin(target, accounts) {
18787
+ if (target.credentialId === undefined)
18788
+ return;
18789
+ return accounts.find((account) => servesTarget(target, account));
18790
+ }
18791
+ var DEFAULT_SETTINGS = {
18792
+ weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
18793
+ maxAttempts: 3,
18794
+ requestDeadlineMs: 120000,
18795
+ breakerThreshold: 3,
18796
+ breakerCooldownMs: 30000,
18797
+ logRetentionDays: 30,
18798
+ quotaPollIntervalMs: 300000,
18799
+ rtkEnabled: false,
18800
+ autoCacheEnabled: true,
18801
+ bodyLoggingEnabled: false,
18802
+ bodyLoggingCaptureStreamChunks: false,
18803
+ snapshotKeepLatest: 5,
18804
+ snapshotMaxAgeDays: 30
18805
+ };
18806
+
18743
18807
  // packages/router/src/snapshot.ts
18744
18808
  var HEALTH_KEY_SEP = "::";
18745
18809
  function healthKey(credentialId, model) {
@@ -18799,12 +18863,11 @@ function eligible(input) {
18799
18863
  const excluded = [];
18800
18864
  for (const target of model.targets) {
18801
18865
  const missing = needNative && !ANTHROPIC_NATIVE_TOOLS[target.provider] ? "anthropicTools" : ["tools", "images", "reasoning"].find((cap) => need[cap] && !target.capabilities[cap]);
18866
+ let pinSeen = false;
18802
18867
  for (const credential of snapshot.credentials) {
18803
- if (credential.provider !== target.provider)
18868
+ if (!servesTarget(target, credential))
18804
18869
  continue;
18805
- if (target.provider === "custom" && credential.providerData.endpointId !== target.endpointId) {
18806
- continue;
18807
- }
18870
+ pinSeen = target.credentialId !== undefined;
18808
18871
  const drop = (reason) => {
18809
18872
  excluded.push({ credentialId: credential.id, model: target.model, reason });
18810
18873
  };
@@ -18841,56 +18904,17 @@ function eligible(input) {
18841
18904
  }
18842
18905
  pairs.push({ credential, target });
18843
18906
  }
18907
+ if (target.credentialId !== undefined && !pinSeen) {
18908
+ excluded.push({
18909
+ credentialId: target.credentialId,
18910
+ model: target.model,
18911
+ reason: "pin:missing"
18912
+ });
18913
+ }
18844
18914
  }
18845
18915
  return { pairs, excluded };
18846
18916
  }
18847
18917
 
18848
- // packages/store/src/types.ts
18849
- var WINDOW_DURATION_MS = {
18850
- fiveHour: 5 * 60 * 60 * 1000,
18851
- daily: 24 * 60 * 60 * 1000,
18852
- weekly: 7 * 24 * 60 * 60 * 1000
18853
- };
18854
- function durationFor(windowType, windowMs) {
18855
- return windowMs !== null && windowMs > 0 ? windowMs : WINDOW_DURATION_MS[windowType];
18856
- }
18857
- var SAME_WINDOW_TOLERANCE_MS = 60000;
18858
- function sameWindow(a, b) {
18859
- if (a === null || b === null)
18860
- return a === b;
18861
- return Math.abs(a - b) <= SAME_WINDOW_TOLERANCE_MS;
18862
- }
18863
- function quotaVerdict(window, estimate) {
18864
- if (estimate === undefined)
18865
- return "unknown";
18866
- if (window.observedAt > 0 && estimate.stale)
18867
- return "stale";
18868
- if (estimate.ratePerHour === null || window.limit === null)
18869
- return "unknown";
18870
- if (estimate.survives === false && estimate.exhaustsAt !== null)
18871
- return "empty";
18872
- return estimate.survives === true ? "ok" : "unknown";
18873
- }
18874
- var READ_OVER_INPUT = 0.1;
18875
- function cacheReadRate(prices) {
18876
- return prices.cacheRead ?? prices.input * READ_OVER_INPUT;
18877
- }
18878
- var DEFAULT_SETTINGS = {
18879
- weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
18880
- maxAttempts: 3,
18881
- requestDeadlineMs: 120000,
18882
- breakerThreshold: 3,
18883
- breakerCooldownMs: 30000,
18884
- logRetentionDays: 30,
18885
- quotaPollIntervalMs: 300000,
18886
- rtkEnabled: false,
18887
- autoCacheEnabled: true,
18888
- bodyLoggingEnabled: false,
18889
- bodyLoggingCaptureStreamChunks: false,
18890
- snapshotKeepLatest: 5,
18891
- snapshotMaxAgeDays: 30
18892
- };
18893
-
18894
18918
  // packages/router/src/quota.ts
18895
18919
  var UNKNOWN_QUOTA = 0.5;
18896
18920
  var QUOTA_FLOOR = 0.1;
@@ -20570,6 +20594,12 @@ function createKeyRepo(db, logger2 = noopLogger) {
20570
20594
  id
20571
20595
  ]);
20572
20596
  },
20597
+ async setModelAllowlist(id, modelAllowlist) {
20598
+ db.run("UPDATE api_keys SET model_allowlist = ? WHERE id = ?", [
20599
+ modelAllowlist === null ? null : JSON.stringify(modelAllowlist),
20600
+ id
20601
+ ]);
20602
+ },
20573
20603
  async revoke(id) {
20574
20604
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
20575
20605
  }
@@ -21198,6 +21228,7 @@ async function createStore(opts) {
21198
21228
  findByHash: (hash3) => handle.keys.findByHash(hash3),
21199
21229
  create: (input) => handle.keys.create(input),
21200
21230
  setLimits: (id, limits) => handle.keys.setLimits(id, limits),
21231
+ setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
21201
21232
  revoke: (id) => handle.keys.revoke(id)
21202
21233
  },
21203
21234
  usage: {
@@ -21831,6 +21862,14 @@ async function setKeyLimits(store, id, input, now = Date.now()) {
21831
21862
  await store.keys.setLimits(id, body2.limits);
21832
21863
  return toSummary(store, { ...key, limits: body2.limits }, now);
21833
21864
  }
21865
+ async function setKeyModels(store, id, input, now = Date.now()) {
21866
+ const body2 = parseOrThrow(keyModelsSchema, input);
21867
+ const key = (await store.keys.list()).find((entry) => entry.id === id);
21868
+ if (key === undefined)
21869
+ throw new GatewayError("BAD_REQUEST", "no such api key");
21870
+ await store.keys.setModelAllowlist(id, body2.modelAllowlist);
21871
+ return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
21872
+ }
21834
21873
  async function revokeKey(store, id) {
21835
21874
  await store.keys.revoke(id);
21836
21875
  }
@@ -21843,8 +21882,8 @@ function narrower(a, b) {
21843
21882
  ...output.length === 0 ? {} : { maxOutputTokens: Math.min(...output) }
21844
21883
  };
21845
21884
  }
21846
- function targetLimits(target, auths) {
21847
- const ways = auths.size === 0 ? ["apiKey"] : [...auths];
21885
+ function targetLimits(target, auths, pinned) {
21886
+ const ways = pinned !== undefined ? [pinned.authType] : auths.size === 0 ? ["apiKey"] : [...auths];
21848
21887
  let listed = {};
21849
21888
  for (const auth of ways) {
21850
21889
  const entry = catalogLimits(target.provider, target.model, auth);
@@ -21864,20 +21903,22 @@ function targetLimits(target, auths) {
21864
21903
  }
21865
21904
  function servingAuths(credentials) {
21866
21905
  const byProvider = new Map;
21906
+ const routable = [];
21867
21907
  for (const credential of credentials) {
21868
21908
  if (!credential.enabled)
21869
21909
  continue;
21870
21910
  const ways = byProvider.get(credential.provider) ?? new Set;
21871
21911
  ways.add(credential.authType);
21872
21912
  byProvider.set(credential.provider, ways);
21913
+ routable.push(credential);
21873
21914
  }
21874
- return byProvider;
21915
+ return { byProvider, routable };
21875
21916
  }
21876
21917
  function resolveModelLimits(model, credentials) {
21877
- const auths = servingAuths(credentials);
21918
+ const { byProvider, routable } = servingAuths(credentials);
21878
21919
  let limits = {};
21879
21920
  for (const target of model.targets) {
21880
- limits = narrower(limits, targetLimits(target, auths.get(target.provider) ?? new Set));
21921
+ limits = narrower(limits, targetLimits(target, byProvider.get(target.provider) ?? new Set, resolvePin(target, routable)));
21881
21922
  }
21882
21923
  return limits;
21883
21924
  }
@@ -21921,15 +21962,18 @@ function unreachable(model, credentials, stored) {
21921
21962
  const held = heldAuths(credentials);
21922
21963
  const grandfathered = new Set((stored?.targets ?? []).map(pairOf));
21923
21964
  for (const target of model.targets) {
21924
- if (grandfathered.has(pairOf(target)))
21965
+ const pinned = resolvePin(target, credentials);
21966
+ if (pinned === undefined && grandfathered.has(pairOf(target)))
21925
21967
  continue;
21926
- const have = held.get(target.provider);
21968
+ const have = pinned === undefined ? held.get(target.provider) : new Set([pinned.authType]);
21927
21969
  if (have === undefined)
21928
21970
  continue;
21929
21971
  const reach = catalogModelAuths(target.provider, target.model);
21930
21972
  if (reach.some((auth) => have.has(auth)))
21931
21973
  continue;
21932
- return new GatewayError("BAD_REQUEST", `${target.provider} serves "${target.model}" to ${phrase(reach)} credentials only, ` + `and every ${target.provider} credential here is ${phrase([...have])} \u2014 ` + `connect the other kind, or pick a model this one can reach`);
21974
+ const holds = pinned === undefined ? `every ${target.provider} credential here is ${phrase([...have])}` : `this target is pinned to "${pinned.label}", which is ${phrase([...have])}`;
21975
+ const fix = pinned === undefined ? "connect the other kind, or pick a model this one can reach" : "pin it to the other kind, or pick a model this account can reach";
21976
+ return new GatewayError("BAD_REQUEST", `${target.provider} serves "${target.model}" to ${phrase(reach)} credentials only, ` + `and ${holds} \u2014 ${fix}`);
21933
21977
  }
21934
21978
  return null;
21935
21979
  }
@@ -21973,14 +22017,15 @@ import { basename as basename2, join as join3, resolve as resolve2, sep as sep2
21973
22017
 
21974
22018
  // packages/plugin-api/src/version.ts
21975
22019
  var PLUGIN_API_VERSION = 1;
21976
- var DASHBOARD_SDK_VERSION = "0.1.1";
22020
+ var DASHBOARD_SDK_VERSION = "0.1.2";
21977
22021
  // packages/plugin-api/src/manifest.ts
21978
22022
  var CAPABILITIES = [
21979
22023
  "storage",
21980
22024
  "files",
21981
22025
  "net:outbound",
21982
22026
  "events:request",
21983
- "events:limit"
22027
+ "events:limit",
22028
+ "channels"
21984
22029
  ];
21985
22030
  var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
21986
22031
  var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
@@ -23678,9 +23723,11 @@ var KEY_PLACEHOLDER = "<your OmniGateway key>";
23678
23723
  async function describeModelsForSetup(store) {
23679
23724
  const models = await listModels(store);
23680
23725
  const credentials = (await listCredentials(store)).map((credential) => ({
23726
+ id: credential.id,
23681
23727
  provider: credential.provider,
23682
23728
  authType: credential.authType,
23683
- enabled: credential.enabled
23729
+ enabled: credential.enabled,
23730
+ providerData: credential.providerData
23684
23731
  }));
23685
23732
  return models.map((model) => ({
23686
23733
  model,
@@ -23716,8 +23763,7 @@ function claudeSettings(described, input, mapping, existing) {
23716
23763
  const visibleId = (slot, id) => {
23717
23764
  if (!ids.has(id))
23718
23765
  throw new Error(`${slot} names unknown virtual model "${id}"`);
23719
- const useMirror = input.discoveryMirrors === true && !/^(?:claude|anthropic)/i.test(id);
23720
- return useMirror ? `claude/${id}` : id;
23766
+ return id;
23721
23767
  };
23722
23768
  const settings = settingsObject(existing);
23723
23769
  const currentEnv = settings.env;
@@ -24603,7 +24649,8 @@ var credentialsRemove = {
24603
24649
  async run(args, { ctx, writer, prompt }) {
24604
24650
  const id = requirePositional(args, 0, "credential id");
24605
24651
  const credential = await findCredential(ctx, id);
24606
- const confirmed = await prompt.confirm(`delete ${credential.provider} credential "${credential.label}" (${id})?`);
24652
+ const pinned = (await listModels(await ctx.store())).filter((model) => model.targets.some((target) => target.credentialId === id)).map((model) => model.id);
24653
+ const confirmed = await prompt.confirm(`delete ${credential.provider} credential "${credential.label}" (${id})?` + (pinned.length === 0 ? "" : ` ${pinned.length === 1 ? "model" : "models"} ${pinned.join(", ")} ` + `${pinned.length === 1 ? "pins a target" : "pin targets"} to it, and ` + `${pinned.length === 1 ? "that target" : "those targets"} will fail rather than ` + "fall back to another account."));
24607
24654
  if (!confirmed)
24608
24655
  throw new CliError("cancelled");
24609
24656
  await removeCredential(await ctx.store(), id);
@@ -25070,6 +25117,43 @@ var keysRevoke = {
25070
25117
  emit(ctx, writer, { id, revoked: true }, () => `${id} revoked`);
25071
25118
  }
25072
25119
  };
25120
+ var keysModels = {
25121
+ usage: "keys models <id> [--allow <model> ...] [--all] [--none]",
25122
+ summary: "Show or replace one key's allowed models",
25123
+ options: {
25124
+ allow: { type: "string", multiple: true },
25125
+ all: { type: "boolean" },
25126
+ none: { type: "boolean" }
25127
+ },
25128
+ async run(args, { ctx, writer }) {
25129
+ const id = requirePositional(args, 0, "key id");
25130
+ const allow = listFlag(args.values, "allow");
25131
+ const all = boolFlag(args.values, "all");
25132
+ const none = boolFlag(args.values, "none");
25133
+ const store = await ctx.store();
25134
+ const existing = (await listKeys(store)).find((entry) => entry.id === id);
25135
+ if (existing === undefined)
25136
+ throw new CliError(`no api key "${id}"`);
25137
+ let key = existing;
25138
+ if (all || none || allow !== undefined) {
25139
+ if ([all, none, allow !== undefined].filter(Boolean).length > 1) {
25140
+ throw new UsageError("--all, --none, and --allow cannot be combined");
25141
+ }
25142
+ const next = all ? null : none ? [] : allow ?? [];
25143
+ key = await setKeyModels(store, id, { modelAllowlist: next });
25144
+ }
25145
+ emit(ctx, writer, key, () => {
25146
+ const head = fields([
25147
+ ["id", key.id],
25148
+ ["label", key.label],
25149
+ ["prefix", `${key.prefix}\u2026`]
25150
+ ]);
25151
+ const models = key.modelAllowlist === null ? "every model" : key.modelAllowlist.length === 0 ? "no models; every request this key makes is refused" : key.modelAllowlist.join(", ");
25152
+ return `${head}
25153
+ models: ${models}`;
25154
+ });
25155
+ }
25156
+ };
25073
25157
 
25074
25158
  // apps/cli/src/commands/models.ts
25075
25159
  var modelsList = {
@@ -25102,6 +25186,7 @@ var modelsShow = {
25102
25186
  async run(args, { ctx, writer }) {
25103
25187
  const id = requirePositional(args, 0, "model id");
25104
25188
  const model = await getModel(await ctx.store(), id);
25189
+ const pinned = model.targets.some((target) => target.credentialId !== undefined);
25105
25190
  emit(ctx, writer, { model }, () => [
25106
25191
  fields([
25107
25192
  ["id", model.id],
@@ -25112,6 +25197,7 @@ var modelsShow = {
25112
25197
  table([
25113
25198
  { header: "PROVIDER" },
25114
25199
  { header: "MODEL" },
25200
+ ...pinned ? [{ header: "ACCOUNT" }] : [],
25115
25201
  { header: "TIER", align: "right" },
25116
25202
  { header: "WEIGHT", align: "right" },
25117
25203
  { header: "IN $/MTOK", align: "right" },
@@ -25125,6 +25211,7 @@ var modelsShow = {
25125
25211
  ], model.targets.map((target) => [
25126
25212
  provider(ctx, target.provider),
25127
25213
  target.model,
25214
+ ...pinned ? [target.credentialId ?? "any"] : [],
25128
25215
  String(target.tier),
25129
25216
  String(target.weight),
25130
25217
  target.costPerMTok.input.toFixed(2),
@@ -25788,6 +25875,17 @@ async function orphanTables(ctx) {
25788
25875
  return null;
25789
25876
  }
25790
25877
  }
25878
+ async function danglingPins(ctx) {
25879
+ if (ctx.configError !== null || !existsSync5(ctx.databasePath))
25880
+ return null;
25881
+ try {
25882
+ const store = await ctx.store();
25883
+ const accounts = await store.credentials.list();
25884
+ return (await store.config.listModels()).flatMap((model) => model.targets.filter((target) => target.credentialId !== undefined && resolvePin(target, accounts) === undefined).map((target) => `${model.id}/${target.model} \u2192 ${target.credentialId}`));
25885
+ } catch {
25886
+ return null;
25887
+ }
25888
+ }
25791
25889
  var doctor = {
25792
25890
  usage: "doctor",
25793
25891
  summary: "Check what this CLI resolved, and whether it can do anything with it",
@@ -25800,6 +25898,7 @@ var doctor = {
25800
25898
  const usageRollup = await rollupState(ctx);
25801
25899
  const plugins = listPlugins(doctorPluginDeps(), deps.root);
25802
25900
  const orphans = await orphanTables(ctx);
25901
+ const pins = await danglingPins(ctx);
25803
25902
  const checks3 = {
25804
25903
  root: deps.root,
25805
25904
  rootSource: ctx.root.source,
@@ -25817,6 +25916,7 @@ var doctor = {
25817
25916
  pluginsDir: pluginsDir(deps.root),
25818
25917
  plugins,
25819
25918
  orphanPluginTables: orphans,
25919
+ danglingPins: pins,
25820
25920
  warnings: ctx.warnings
25821
25921
  };
25822
25922
  emit(ctx, writer, checks3, () => {
@@ -25843,6 +25943,10 @@ var doctor = {
25843
25943
  "orphan plugin tables",
25844
25944
  orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
25845
25945
  ],
25946
+ [
25947
+ "dangling pins",
25948
+ pins === null ? paint(ctx, "dim", "not checked") : pins.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${pins.length}: ${pins.join(", ")}`)
25949
+ ],
25846
25950
  ...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
25847
25951
  ]);
25848
25952
  });
@@ -25996,7 +26100,6 @@ var setupClaude = {
25996
26100
  const path = join8(dir, "settings.json");
25997
26101
  const file2 = claudeSettings(models, {
25998
26102
  baseUrl: baseUrl(ctx),
25999
- discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
26000
26103
  ...key === undefined ? {} : { apiKey: key }
26001
26104
  }, mapping, setupFs.read(path) ?? undefined);
26002
26105
  finish(ctx, writer, [at(dir, file2)], dryRun2, key, setupFs.write);
@@ -26247,6 +26350,7 @@ var COMMANDS = {
26247
26350
  "keys list": keysList,
26248
26351
  "keys create": keysCreate,
26249
26352
  "keys limits": keysLimits,
26353
+ "keys models": keysModels,
26250
26354
  "keys revoke": keysRevoke,
26251
26355
  "plugin list": pluginList,
26252
26356
  "plugin verify": pluginVerify,