llm-relay 0.47.0 → 0.48.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 (77) hide show
  1. package/dist/accounting-store-io.js +1 -9
  2. package/dist/accounting-store-io.js.map +1 -1
  3. package/dist/accounting-store-schema.d.ts +0 -35
  4. package/dist/accounting-store-schema.js +14 -101
  5. package/dist/accounting-store-schema.js.map +1 -1
  6. package/dist/accounting-store.d.ts +7 -0
  7. package/dist/accounting-store.js +3 -1
  8. package/dist/accounting-store.js.map +1 -1
  9. package/dist/availability-snapshot.js +14 -44
  10. package/dist/availability-snapshot.js.map +1 -1
  11. package/dist/availability.d.ts +31 -4
  12. package/dist/availability.js +36 -0
  13. package/dist/availability.js.map +1 -1
  14. package/dist/backend.js +12 -20
  15. package/dist/backend.js.map +1 -1
  16. package/dist/candidates.js +12 -46
  17. package/dist/candidates.js.map +1 -1
  18. package/dist/cli.js +47 -178
  19. package/dist/cli.js.map +1 -1
  20. package/dist/configured-limits.js +55 -106
  21. package/dist/configured-limits.js.map +1 -1
  22. package/dist/cooldown-clear.d.ts +3 -0
  23. package/dist/cooldown-clear.js +158 -2
  24. package/dist/cooldown-clear.js.map +1 -1
  25. package/dist/dashboard/.vite/manifest.json +1 -1
  26. package/dist/dashboard/assets/{index-u-Ez_oE6.js → index-BA4GdK4p.js} +1 -1
  27. package/dist/dashboard/index.html +1 -1
  28. package/dist/dashboard-contract.js +1 -5
  29. package/dist/dashboard-contract.js.map +1 -1
  30. package/dist/dashboard-routes.js +29 -57
  31. package/dist/dashboard-routes.js.map +1 -1
  32. package/dist/dashboard-snapshot.js +17 -42
  33. package/dist/dashboard-snapshot.js.map +1 -1
  34. package/dist/dashboard-static.js +1 -3
  35. package/dist/dashboard-static.js.map +1 -1
  36. package/dist/dialect-stream.js +13 -21
  37. package/dist/dialect-stream.js.map +1 -1
  38. package/dist/hard-cap.d.ts +3 -1
  39. package/dist/hard-cap.js +11 -0
  40. package/dist/hard-cap.js.map +1 -1
  41. package/dist/json-shape.d.ts +22 -0
  42. package/dist/json-shape.js +40 -0
  43. package/dist/json-shape.js.map +1 -0
  44. package/dist/keys-cli.js +4 -60
  45. package/dist/keys-cli.js.map +1 -1
  46. package/dist/keystore.js +11 -18
  47. package/dist/keystore.js.map +1 -1
  48. package/dist/openai-dialect.js +46 -40
  49. package/dist/openai-dialect.js.map +1 -1
  50. package/dist/openai-request.d.ts +0 -26
  51. package/dist/openai-request.js +1 -3
  52. package/dist/openai-request.js.map +1 -1
  53. package/dist/quota-demotion.js +9 -34
  54. package/dist/quota-demotion.js.map +1 -1
  55. package/dist/refusal-interpretation.js +1 -4
  56. package/dist/refusal-interpretation.js.map +1 -1
  57. package/dist/responses-request.d.ts +0 -76
  58. package/dist/responses-request.js +1 -3
  59. package/dist/responses-request.js.map +1 -1
  60. package/dist/server.js +122 -150
  61. package/dist/server.js.map +1 -1
  62. package/dist/sse-frames.d.ts +39 -0
  63. package/dist/sse-frames.js +65 -0
  64. package/dist/sse-frames.js.map +1 -0
  65. package/dist/stream-commit.js +14 -24
  66. package/dist/stream-commit.js.map +1 -1
  67. package/dist/target-facts.d.ts +6 -0
  68. package/dist/target-facts.js +8 -5
  69. package/dist/target-facts.js.map +1 -1
  70. package/dist/think-tags.js +11 -30
  71. package/dist/think-tags.js.map +1 -1
  72. package/dist/tool-use-ids.d.ts +0 -31
  73. package/dist/tool-use-ids.js +11 -63
  74. package/dist/tool-use-ids.js.map +1 -1
  75. package/dist/usage-observer.js +22 -33
  76. package/dist/usage-observer.js.map +1 -1
  77. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { isRecord } from "./json-shape.js";
2
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { homedir } from "node:os";
@@ -8,7 +9,7 @@ import { loadEnvFile } from "./dotenv.js";
8
9
  import { recoverWindowsEnv } from "./winenv.js";
9
10
  import { offloadState, setOffload } from "./offload.js";
10
11
  import { buildCandidates } from "./candidates.js";
11
- import { CREDENTIAL_LABEL_PATTERN, makeCredentialId, parseCredentialId } from "./credential-id.js";
12
+ import { CREDENTIAL_LABEL_PATTERN, makeCredentialId } from "./credential-id.js";
12
13
  import { providerCredentialSlots, slotAllowsModel } from "./credential-fleet.js";
13
14
  import { loadLaneManifest, verifyModel } from "./lane-manifest.js";
14
15
  import { probeLanes } from "./lane-probe.js";
@@ -28,6 +29,7 @@ import { materializeDynamicPools } from "./dynamic-pools.js";
28
29
  import { currentVersion, ensureUpToDate, shouldCheckUpdates } from "./self-update.js";
29
30
  import { deleteConfigPath, parseConfigValue, readConfigDocument, readConfigPath, updateConfigDocument, writeConfigPath, } from "./config-edit.js";
30
31
  import { createControlAuthorization, resolveControlAuthorizationConfigDir } from "./control-authorization.js";
32
+ import { isCooldownClearResult } from "./cooldown-clear.js";
31
33
  import { createDashboardSnapshotReadPort } from "./dashboard-snapshot.js";
32
34
  import { DASHBOARD_MEDIA_TYPE, isDashboardUtcTimestamp } from "./dashboard-contract.js";
33
35
  import { DASHBOARD_BOOTSTRAP_SCHEMA, DASHBOARD_BOOTSTRAP_REQUEST_SCHEMA } from "./dashboard-routes.js";
@@ -751,136 +753,6 @@ function cooldownCommandFailure(message) {
751
753
  process.stderr.write(`llm-relay cooldowns: ${message}\n`);
752
754
  process.exit(1);
753
755
  }
754
- function isRecord(value) {
755
- return typeof value === "object" && value !== null && !Array.isArray(value);
756
- }
757
- function hasExactKeys(value, keys) {
758
- const actual = Object.keys(value);
759
- return actual.length === keys.length && actual.every((key) => keys.includes(key));
760
- }
761
- function nonEmptyString(value) {
762
- return typeof value === "string" && value.length > 0;
763
- }
764
- const COOLING_FACT_KINDS = new Set([
765
- "allowance-exhausted",
766
- "rate-limited",
767
- "credential-invalid",
768
- ]);
769
- function credentialIdBelongsTo(provider, value) {
770
- if (typeof value !== "string")
771
- return false;
772
- return parseCredentialId(value)?.provider === provider;
773
- }
774
- function isCooldownFactScope(value) {
775
- if (!isRecord(value))
776
- return false;
777
- switch (value.kind) {
778
- case "attempt":
779
- return hasExactKeys(value, ["kind", "provider", "credentialId", "model"]) &&
780
- nonEmptyString(value.provider) &&
781
- credentialIdBelongsTo(value.provider, value.credentialId) &&
782
- nonEmptyString(value.model);
783
- case "group": {
784
- const keys = Object.hasOwn(value, "credentialId")
785
- ? ["kind", "provider", "credentialId", "members"]
786
- : ["kind", "provider", "members"];
787
- return hasExactKeys(value, keys) &&
788
- nonEmptyString(value.provider) &&
789
- Array.isArray(value.members) &&
790
- value.members.length > 0 &&
791
- value.members.every(nonEmptyString) &&
792
- (!Object.hasOwn(value, "credentialId") || credentialIdBelongsTo(value.provider, value.credentialId));
793
- }
794
- case "deployment":
795
- return hasExactKeys(value, ["kind", "provider", "model"]) &&
796
- nonEmptyString(value.provider) && nonEmptyString(value.model);
797
- case "credential":
798
- return hasExactKeys(value, ["kind", "provider", "credentialId"]) &&
799
- nonEmptyString(value.provider) && credentialIdBelongsTo(value.provider, value.credentialId);
800
- case "provider":
801
- return hasExactKeys(value, ["kind", "provider"]) && nonEmptyString(value.provider);
802
- case "model":
803
- return hasExactKeys(value, ["kind", "model"]) && nonEmptyString(value.model);
804
- default:
805
- return false;
806
- }
807
- }
808
- function scopeIsContainedByTarget(scope, target) {
809
- const credentialId = target.credential === undefined
810
- ? undefined
811
- : makeCredentialId(target.provider, target.credential);
812
- switch (scope.kind) {
813
- case "attempt":
814
- return scope.provider === target.provider &&
815
- (target.model === undefined || scope.model === target.model) &&
816
- (credentialId === undefined || scope.credentialId === credentialId);
817
- case "group":
818
- return scope.provider === target.provider &&
819
- (target.model === undefined || scope.members.every((member) => member === target.model)) &&
820
- (credentialId === undefined || scope.credentialId === credentialId);
821
- case "deployment":
822
- return scope.provider === target.provider && credentialId === undefined &&
823
- (target.model === undefined || scope.model === target.model);
824
- case "credential":
825
- return scope.provider === target.provider && target.model === undefined &&
826
- (credentialId === undefined || scope.credentialId === credentialId);
827
- case "provider":
828
- return scope.provider === target.provider && target.model === undefined && credentialId === undefined;
829
- case "model":
830
- return false;
831
- }
832
- }
833
- function isCooldownCell(value, target) {
834
- if (!isRecord(value) || !hasExactKeys(value, ["provider", "model", "credential"]))
835
- return false;
836
- if (!nonEmptyString(value.provider) || value.provider !== target.provider)
837
- return false;
838
- if (!(value.model === null || nonEmptyString(value.model)))
839
- return false;
840
- if (!nonEmptyString(value.credential) || !CREDENTIAL_LABEL_PATTERN.test(value.credential))
841
- return false;
842
- return (target.model === undefined || value.model === target.model) &&
843
- (target.credential === undefined || value.credential === target.credential);
844
- }
845
- function isCooldownFact(value, target) {
846
- return isRecord(value) &&
847
- hasExactKeys(value, ["kind", "scope"]) &&
848
- typeof value.kind === "string" &&
849
- COOLING_FACT_KINDS.has(value.kind) &&
850
- isCooldownFactScope(value.scope) &&
851
- scopeIsContainedByTarget(value.scope, target);
852
- }
853
- function isClearedGroup(value, isItem) {
854
- if (!isRecord(value) || !hasExactKeys(value, ["count", "items"]) ||
855
- typeof value.count !== "number" || !Number.isSafeInteger(value.count) ||
856
- value.count < 0 || !Array.isArray(value.items)) {
857
- return false;
858
- }
859
- return value.count === value.items.length && value.items.every(isItem);
860
- }
861
- function isExactCooldownTarget(value, expected) {
862
- if (!isRecord(value))
863
- return false;
864
- const keys = [
865
- "provider",
866
- ...(expected.model === undefined ? [] : ["model"]),
867
- ...(expected.credential === undefined ? [] : ["credential"]),
868
- ];
869
- return hasExactKeys(value, keys) &&
870
- value.provider === expected.provider &&
871
- (expected.model === undefined || value.model === expected.model) &&
872
- (expected.credential === undefined || value.credential === expected.credential);
873
- }
874
- function isCooldownClearResult(value, target) {
875
- if (!isRecord(value) || !hasExactKeys(value, ["target", "cleared"]) ||
876
- !isExactCooldownTarget(value.target, target) || !isRecord(value.cleared) ||
877
- !hasExactKeys(value.cleared, ["breakerCells", "credentialFaults", "facts"])) {
878
- return false;
879
- }
880
- return isClearedGroup(value.cleared.breakerCells, (item) => isCooldownCell(item, target)) &&
881
- isClearedGroup(value.cleared.credentialFaults, (item) => isCooldownCell(item, target)) &&
882
- isClearedGroup(value.cleared.facts, (item) => isCooldownFact(item, target));
883
- }
884
756
  const COOLDOWN_CLEAR_OPTIONS = new Map([
885
757
  ["--credential", { semantic: "credential", takesValue: true }],
886
758
  ["--json", { semantic: "json", takesValue: false }],
@@ -1089,6 +961,11 @@ export async function runCooldowns(_action, _spec) {
1089
961
  ...(model === undefined ? {} : { model }),
1090
962
  ...(credential === undefined ? {} : { credential }),
1091
963
  };
964
+ const acceptedTargetKeys = [
965
+ "provider",
966
+ ...(model === undefined ? [] : ["model"]),
967
+ ...(credential === undefined ? [] : ["credential"]),
968
+ ];
1092
969
  const cfg = loadOrExit();
1093
970
  if (!Object.hasOwn(cfg.providers, provider)) {
1094
971
  cooldownCommandFailure(`no provider "${provider}" configured`);
@@ -1126,7 +1003,7 @@ export async function runCooldowns(_action, _spec) {
1126
1003
  const detail = controlErrorMessage(payload);
1127
1004
  cooldownCommandFailure(`the running relay rejected the clear${detail === null ? "" : `: ${detail}`}`);
1128
1005
  }
1129
- if (!isCooldownClearResult(payload, target)) {
1006
+ if (!isCooldownClearResult(payload, target, acceptedTargetKeys)) {
1130
1007
  cooldownCommandFailure("the running relay returned an invalid cooldown-clear response");
1131
1008
  }
1132
1009
  if (parsed.json) {
@@ -2348,29 +2225,7 @@ export async function runCandidates() {
2348
2225
  .join(", ");
2349
2226
  process.stdout.write(`Offload targets — ${clientState || `legacy/global: ${view.offload_enabled ? "on" : "off"}`}\n`);
2350
2227
  process.stdout.write(`${view.note}\n\n`);
2351
- const head = "target".padEnd(32) +
2352
- "credential".padEnd(40) +
2353
- "pools / tiers".padEnd(24) +
2354
- "fit".padEnd(7) +
2355
- "raw".padEnd(7) +
2356
- "cap".padEnd(11) +
2357
- "agentic".padEnd(9) +
2358
- "coding".padEnd(8) +
2359
- "BFCL".padEnd(7) +
2360
- "aider".padEnd(7) +
2361
- "arena".padEnd(7) +
2362
- "$/Mout".padEnd(8) +
2363
- "verdict".padEnd(10) +
2364
- "p95".padEnd(8) +
2365
- // Latency actually observed on this proxy's own traffic. The synthetic-probe p95 beside it is
2366
- // routinely blank, so the table could show the first pool member with NO latency signal at all
2367
- // while the proxy had already measured it at 60+ seconds per call — which is the difference
2368
- // between a pool that suits mechanical batch work and one that does not.
2369
- "obs".padEnd(8) +
2370
- "breaker".padEnd(9) +
2371
- "ctx";
2372
- process.stdout.write(head + "\n" + "-".repeat(head.length) + "\n");
2373
- for (const c of view.candidates) {
2228
+ for (const [index, c] of view.candidates.entries()) {
2374
2229
  const credential = candidateCredential(c);
2375
2230
  const tags = [...c.pools, ...c.subagentTiers.map((t) => `@${t}`)].join(",") || "-";
2376
2231
  const live = c.listed === null ? "?" : c.listed ? "yes" : "NO";
@@ -2387,32 +2242,46 @@ export async function runCandidates() {
2387
2242
  : c.breaker.credentialFault
2388
2243
  ? `AUTH ${c.breaker.lastCredentialStatus ?? ""}`.trim()
2389
2244
  : "closed";
2390
- process.stdout.write(c.spec.slice(0, 31).padEnd(32) +
2391
- `${credential.label} (${c.credentialId})`.padEnd(40) +
2392
- tags.slice(0, 23).padEnd(24) +
2393
- c.sortInputs.fitness.toFixed(1).padEnd(7) +
2394
- c.sortInputs.rawStrength.toFixed(1).padEnd(7) +
2245
+ const columns = [
2246
+ ["target", 32, c.spec.slice(0, 31)],
2247
+ ["credential", 40, `${credential.label} (${c.credentialId})`],
2248
+ ["pools / tiers", 24, tags.slice(0, 23)],
2249
+ ["fit", 7, c.sortInputs.fitness.toFixed(1)],
2250
+ ["raw", 7, c.sortInputs.rawStrength.toFixed(1)],
2395
2251
  // Capability plus how well-evidenced it is: "76.6/4" = 4 published signals behind it,
2396
2252
  // "50.0 neut" = nothing known. Never show the number alone.
2397
- `${c.sortInputs.strength.toFixed(1)}${strengthTag(c)}`.padEnd(11) +
2398
- fmt(c.scores.aaAgentic).padEnd(9) +
2399
- fmt(c.scores.aaCoding).padEnd(8) +
2400
- fmt(c.scores.bfclOverall).padEnd(7) +
2401
- fmt(c.scores.aiderPassRate).padEnd(7) +
2402
- (c.scores.arenaRating ? String(Math.round(c.scores.arenaRating)) : "-").padEnd(7) +
2403
- (c.pricePerMTokOut !== null
2404
- ? `$${c.pricePerMTokOut}${c.priceSource === "reference" ? "~" : ""}`
2405
- : "-").padEnd(8) +
2406
- (c.health?.verdict ?? "-").padEnd(10) +
2407
- fmt(c.health?.p95Ms ?? null, "ms").padEnd(8) +
2408
- obsLatency(c.observed).padEnd(8) +
2409
- breaker.padEnd(9) +
2253
+ ["cap", 11, `${c.sortInputs.strength.toFixed(1)}${strengthTag(c)}`],
2254
+ ["agentic", 9, fmt(c.scores.aaAgentic)],
2255
+ ["coding", 8, fmt(c.scores.aaCoding)],
2256
+ ["BFCL", 7, fmt(c.scores.bfclOverall)],
2257
+ ["aider", 7, fmt(c.scores.aiderPassRate)],
2258
+ ["arena", 7, c.scores.arenaRating ? String(Math.round(c.scores.arenaRating)) : "-"],
2259
+ ["$/Mout", 8, c.pricePerMTokOut !== null
2260
+ ? `$${c.pricePerMTokOut}${c.priceSource === "reference" ? "~" : ""}`
2261
+ : "-"],
2262
+ ["verdict", 10, c.health?.verdict ?? "-"],
2263
+ ["p95", 8, fmt(c.health?.p95Ms ?? null, "ms")],
2264
+ // Latency actually observed on this proxy's own traffic. The synthetic-probe p95 beside it is
2265
+ // routinely blank, so the table could show the first pool member with NO latency signal at all
2266
+ // while the proxy had already measured it at 60+ seconds per call — which is the difference
2267
+ // between a pool that suits mechanical batch work and one that does not.
2268
+ ["obs", 8, obsLatency(c.observed)],
2269
+ ["breaker", 9, breaker],
2410
2270
  // Provenance inline: "~" = another provider's figure for this model id. A NIM row must
2411
- // never present OpenRouter's ceiling as its own.
2412
- ctx +
2413
- (c.contextLengthSource === "reference" ? "~" : "") +
2414
- (live === "NO" ? " ⚠UNLISTED" : "") +
2415
- "\n");
2271
+ // never present OpenRouter's ceiling as its own. `content` keeps this trailing column unpadded.
2272
+ ["ctx", "content", ctx +
2273
+ (c.contextLengthSource === "reference" ? "~" : "") +
2274
+ (live === "NO" ? " ⚠UNLISTED" : "")],
2275
+ ];
2276
+ const formatColumns = (part) => columns.map(([heading, width, cell]) => {
2277
+ const content = part === "heading" ? heading : cell;
2278
+ return width === "content" ? content : content.padEnd(width);
2279
+ }).join("");
2280
+ if (index === 0) {
2281
+ const head = formatColumns("heading");
2282
+ process.stdout.write(head + "\n" + "-".repeat(head.length) + "\n");
2283
+ }
2284
+ process.stdout.write(`${formatColumns("cell")}\n`);
2416
2285
  // The resolved ladders print on their own line: raw observations first (what providers
2417
2286
  // said), then the derived view with its basis, so a computed figure never appears without
2418
2287
  // its provenance. Spec §5: unknown stays "-", and a negative remaining prints as-is.