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.
- package/README.md +52 -1
- package/bin/omni.js +176 -72
- package/gateway.js +1145 -126
- package/package.json +1 -1
- package/public/assets/{CopyValue-C2DkO9Yz.js → CopyValue-dQWAaTxf.js} +3 -3
- package/public/assets/{Field-B79Yxdit.js → Field-DTQ2LPzA.js} +1 -1
- package/public/assets/{Match-BWa6L0L1.js → Match-C85GObiU.js} +1 -1
- package/public/assets/{Panel-CoulMx6R.js → Panel-CQVz9yeR.js} +2 -2
- package/public/assets/Rack-V32CHmN6.js +316 -0
- package/public/assets/{Readout-D1XEfywe.js → Readout-DJR1qKja.js} +1 -1
- package/public/assets/States-CauIsA0N.js +54 -0
- package/public/assets/{Toggle-CRrH9nDt.js → Toggle-BQI8jz09.js} +1 -1
- package/public/assets/{TokenBreakdown-C2NsJnqO.js → TokenBreakdown-B2_gfuqt.js} +1 -1
- package/public/assets/_app-DzzXdoyU.js +1 -0
- package/public/assets/_app.accounts-owsnxlbn.js +64 -0
- package/public/assets/_app.console-bzKNP_8Y.js +44 -0
- package/public/assets/_app.database-CsJbkedE.js +24 -0
- package/public/assets/_app.index-D4ydl2mY.js +62 -0
- package/public/assets/_app.keys-DKX0smty.js +82 -0
- package/public/assets/_app.logs-CFFsj2zR.js +68 -0
- package/public/assets/_app.models-B0ZxWGHV.js +148 -0
- package/public/assets/{_app.plugins._pluginId-BOXRBToU.js → _app.plugins._pluginId-C5X4B1cE.js} +3 -3
- package/public/assets/{_app.settings-DoaOUmc1.js → _app.settings-Yg_6az-L.js} +4 -4
- package/public/assets/_app.usage-BRIz_GCn.js +83 -0
- package/public/assets/index-DiCU8bLB.js +178 -0
- package/public/assets/{login-B9CJqU--.js → login-B4LoiALI.js} +3 -3
- package/public/assets/plus-xu9reaJp.js +1 -0
- package/public/assets/queries-DuSqKybM.js +145 -0
- package/public/assets/{shared-Cct_4xwp.js → shared-DYkFSPbd.js} +4 -4
- package/public/assets/stream-Dz5HjHBh.js +1 -0
- package/public/assets/{trash-2-DwMK-JrZ.js → trash-2-DbutVvFY.js} +1 -1
- package/public/index.html +3 -3
- package/public/shared/dashboard-sdk.js +1 -1
- package/public/assets/Rack-HM36SprU.js +0 -314
- package/public/assets/States-DtkkMzEu.js +0 -54
- package/public/assets/_app-DVpIsUid.js +0 -1
- package/public/assets/_app.accounts-64d_cxir.js +0 -64
- package/public/assets/_app.console-BBuBmLRI.js +0 -44
- package/public/assets/_app.database-J9Ujio8l.js +0 -24
- package/public/assets/_app.index-BX4yKEXO.js +0 -62
- package/public/assets/_app.keys-2sKwhZJc.js +0 -76
- package/public/assets/_app.logs-DuHXYrZU.js +0 -68
- package/public/assets/_app.models-B0kTfPtn.js +0 -148
- package/public/assets/_app.usage-B5sxB40E.js +0 -83
- package/public/assets/index-iCfSaRSG.js +0 -178
- package/public/assets/plus-DKapAGoR.js +0 -1
- 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)
|
|
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
|
|
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
|
|
18868
|
+
if (!servesTarget(target, credential))
|
|
18804
18869
|
continue;
|
|
18805
|
-
|
|
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
|
|
21918
|
+
const { byProvider, routable } = servingAuths(credentials);
|
|
21878
21919
|
let limits = {};
|
|
21879
21920
|
for (const target of model.targets) {
|
|
21880
|
-
limits = narrower(limits, targetLimits(target,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|