llm-relay 0.15.3 → 0.16.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 (72) hide show
  1. package/README.md +150 -85
  2. package/config.example.json +27 -41
  3. package/dist/benchmarks.d.ts +96 -19
  4. package/dist/benchmarks.js +168 -27
  5. package/dist/benchmarks.js.map +1 -1
  6. package/dist/candidates.d.ts +23 -10
  7. package/dist/candidates.js +80 -21
  8. package/dist/candidates.js.map +1 -1
  9. package/dist/catalog.d.ts +12 -0
  10. package/dist/catalog.js +47 -2
  11. package/dist/catalog.js.map +1 -1
  12. package/dist/circuit-breaker.d.ts +1 -1
  13. package/dist/circuit-breaker.js +1 -1
  14. package/dist/cli.d.ts +6 -2
  15. package/dist/cli.js +244 -187
  16. package/dist/cli.js.map +1 -1
  17. package/dist/config.d.ts +18 -1
  18. package/dist/config.js +35 -5
  19. package/dist/config.js.map +1 -1
  20. package/dist/dispatch.d.ts +9 -1
  21. package/dist/dispatch.js +7 -0
  22. package/dist/dispatch.js.map +1 -1
  23. package/dist/dynamic-pools.d.ts +20 -3
  24. package/dist/dynamic-pools.js +98 -38
  25. package/dist/dynamic-pools.js.map +1 -1
  26. package/dist/kernel/contracts.d.ts +14 -180
  27. package/dist/kernel/contracts.js +15 -5
  28. package/dist/kernel/contracts.js.map +1 -1
  29. package/dist/kernel/index.d.ts +0 -1
  30. package/dist/kernel/index.js +0 -1
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/kernel/request-lifecycle.d.ts +1 -38
  33. package/dist/kernel/request-lifecycle.js +0 -90
  34. package/dist/kernel/request-lifecycle.js.map +1 -1
  35. package/dist/metadata.d.ts +32 -1
  36. package/dist/metadata.js +48 -30
  37. package/dist/metadata.js.map +1 -1
  38. package/dist/offload.js +2 -1
  39. package/dist/offload.js.map +1 -1
  40. package/dist/ping/cadence.d.ts +6 -1
  41. package/dist/ping/cadence.js +87 -9
  42. package/dist/ping/cadence.js.map +1 -1
  43. package/dist/ping/probe-cache.d.ts +5 -0
  44. package/dist/ping/probe-cache.js +56 -3
  45. package/dist/ping/probe-cache.js.map +1 -1
  46. package/dist/ping/runtime-telemetry.d.ts +10 -0
  47. package/dist/ping/runtime-telemetry.js +55 -5
  48. package/dist/ping/runtime-telemetry.js.map +1 -1
  49. package/dist/pool-health.d.ts +7 -1
  50. package/dist/pool-health.js +21 -9
  51. package/dist/pool-health.js.map +1 -1
  52. package/dist/registry.d.ts +1 -1
  53. package/dist/registry.js +3 -3
  54. package/dist/registry.js.map +1 -1
  55. package/dist/routes/admin.js +16 -2
  56. package/dist/routes/admin.js.map +1 -1
  57. package/dist/server.d.ts +2 -2
  58. package/dist/server.js +111 -38
  59. package/dist/server.js.map +1 -1
  60. package/dist/tier-data.d.ts +29 -3
  61. package/dist/tier-data.js +26 -12
  62. package/dist/tier-data.js.map +1 -1
  63. package/docs/tier-data.json +24842 -8365
  64. package/package.json +4 -1
  65. package/scripts/install-skill.mjs +2 -2
  66. package/scripts/sync-tiers.mjs +326 -0
  67. package/scripts/tier-scoring.d.mts +20 -0
  68. package/scripts/tier-scoring.mjs +327 -0
  69. package/skills/llm-relay/SKILL.md +59 -24
  70. package/dist/kernel/tier-snapshot.d.ts +0 -48
  71. package/dist/kernel/tier-snapshot.js +0 -126
  72. package/dist/kernel/tier-snapshot.js.map +0 -1
package/README.md CHANGED
@@ -22,32 +22,46 @@ Runs the compiled CLI as a real process against a local flaky-model backend + st
22
22
 
23
23
  ## Quick Start & Free Model Onboarding
24
24
 
25
- `llm-relay` comes pre-configured with **100%-free model presets** (NVIDIA NIM, Groq, Gemini Free, OpenRouter Free, Cerebras, SambaNova) and supports **pooling your existing subscriptions** (ChatGPT / OpenAI API, AGY, Anthropic).
25
+ `llm-relay` comes pre-configured with **100%-free model presets** (NVIDIA NIM, Groq, Gemini Free, OpenRouter Free, Cerebras, SambaNova) and supports **pooling your existing subscriptions** (ChatGPT / OpenAI API, AGY, Anthropic). This section is the whole install; the staged, hand-it-to-an-AI version with more depth is [docs/QUICKSTART.md](docs/QUICKSTART.md).
26
26
 
27
- ### Step 1: Run Guided Free Key Setup
27
+ ### Step 1: Get keys
28
28
  ```bash
29
29
  npx llm-relay onboard
30
30
  ```
31
- Scans your environment for active keys and provides direct links to acquire 100%-free API keys from NVIDIA, Groq, Google Gemini, OpenRouter, Cerebras, and SambaNova.
31
+ Creates `~/.llm-relay/config.json` (first run), scans your environment for keys you already have, and walks the free providers with direct signup links. **The part only you can do:** open the links, create the accounts, and paste each API key when prompted — onboarding saves them to `~/.llm-relay/.env`. One or two providers is enough to start; you can rerun `onboard` any time to add more. (A key added *while the relay is running* is picked up on the next relay restart, not instantly.)
32
32
 
33
- ### Step 2: Configure Claude CLI or Claude Desktop
33
+ ### Step 2: Point your client at the relay
34
34
 
35
35
  **For Claude Desktop:**
36
36
  ```bash
37
37
  llm-relay setup claude-desktop
38
38
  ```
39
- Auto-patches `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) to route Claude Desktop through `llm-relay` (`http://127.0.0.1:8791`).
39
+ Auto-patches `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) to route Claude Desktop through `llm-relay` (`http://127.0.0.1:8791`), with an isolated `CLAUDE_CONFIG_DIR` so it never conflicts with your claude.ai login. **Restart Claude Desktop afterwards** — it reads this file only at launch.
40
40
 
41
41
  **For Claude CLI (`claude`):**
42
42
  ```bash
43
43
  llm-relay setup claude-cli
44
44
  ```
45
- Verifies wrapper scripts (`scripts/claude-proxied.ps1` and `scripts/claude-proxied.sh`) that use an isolated `CLAUDE_CONFIG_DIR` so your proxy setup never conflicts with local login tokens.
45
+ Prints the two ready-made wrapper scripts (`claude-proxied.ps1` / `claude-proxied.sh`) and what they set. Nothing is modified: run `claude` through a wrapper when you want the relay in the path — see [Use it from your projects](#use-it-from-your-projects) for the wrapper vs inline-env choice.
46
46
 
47
- ### Step 3: Start the Proxy
47
+ ### Step 3: Start it and prove it works
48
48
  ```bash
49
- llm-relay
49
+ llm-relay # starts the proxy on 127.0.0.1:8791 (leave it running)
50
+ llm-relay keys # are the credentials good?
51
+ llm-relay pools --probe # does each configured model actually answer?
50
52
  ```
53
+ `keys` and `pools --probe` are the two checks worth running before blaming anything else — see [Verifying a setup](#verifying-a-setup--two-checks-two-different-questions) for what each can and cannot prove.
54
+
55
+ ### Step 4: Keep it running
56
+
57
+ The relay is a foreground process; if you close the terminal, everything you pointed at it stops working. Have your OS start it at login:
58
+
59
+ - **Windows** — save this as `llm-relay.vbs` in `shell:startup` (Win+R → `shell:startup`):
60
+ ```vbs
61
+ CreateObject("WScript.Shell").Run "llm-relay", 0
62
+ ```
63
+ - **macOS** — `brew services` has no formula for this; use a LaunchAgent: save as `~/Library/LaunchAgents/com.llm-relay.plist` with `ProgramArguments` = the full path from `which llm-relay`, `RunAtLoad` = true, then `launchctl load` it.
64
+ - **Linux** — a user systemd unit: `systemd-run --user --unit=llm-relay $(which llm-relay)` to try it, or write `~/.config/systemd/user/llm-relay.service` and `systemctl --user enable --now llm-relay`.
51
65
 
52
66
  ---
53
67
 
@@ -56,7 +70,7 @@ llm-relay
56
70
  ### 1. 100%-Free Presets & Subscription Pooling
57
71
  - **100%-Free Tier**: NVIDIA NIM (`build.nvidia.com`), Groq (`console.groq.com/keys`), Gemini Free (`aistudio.google.com/app/apikey`), OpenRouter Free (`openrouter.ai/keys`), Cerebras, SambaNova.
58
72
  - **Subscription Tier**: Mapped as `subscription` in config (e.g. OpenAI `OPENAI_API_KEY`, Anthropic `ANTHROPIC_API_KEY`).
59
- - **Priority Cascade**: `llm-relay` prioritizes high-capability subscriptions first, automatically falling back to high-stability free tier targets if 429 rate limits occur.
73
+ - **Priority Cascade**: within a multi-candidate route (an array or a pool), higher-ranked targets are tried first with automatic failover to the rest on 429s. (This is in-route failover it does not reroute traffic between clients or tiers by itself; that is the opt-in offload switch below.)
60
74
 
61
75
  ### 2. Stability-Aware Dynamic Routing & Auto-Failover
62
76
  - `CircuitBreaker` tracks latency, jitter, spike rates, and remaining rate-limit quota headers (`x-ratelimit-remaining`), computing a live **Stability Score (0–100)** for every provider target.
@@ -73,7 +87,8 @@ llm-relay
73
87
 
74
88
  ### 4. Background Adaptive Health Monitoring & Persistent Caching
75
89
  - **Adaptive Cadence Loop**: Background `PingLoop` dynamically adjusts probe frequency across 4 operational modes: `speed` (2s interval at startup/activity), `normal` (10s), `slow` (30s after 5m idle), and `forced` (4s).
76
- - **Persistent State**: Background probes, real-world proxy calls, dynamic catalogs, and local keys persist under `~/.llm-relay/` (`models-cache.json`, `probe-cache.json`, `runtime-telemetry.json`, `.env`).
90
+ - **Selective probes**: The background loop probes only deployments present in materialized routing, with pool leaders first. A recent successful real request satisfies freshness; broken targets retry with exponential backoff instead of being hammered every tick. Explicit `llm-relay ping` remains a full-catalog diagnostic.
91
+ - **Persistent State**: Background probes, real-world proxy calls, dynamic catalogs, and local keys persist under `~/.llm-relay/` (`models-cache.json`, `probe-cache.json`, `runtime-telemetry.json`, `.env`). JSON caches use bounded write-behind and flush during graceful shutdown, keeping whole-file rewrites out of request/probe hot paths.
77
92
 
78
93
  ### 5. Document (PDF/Office) Attachments on Non-Anthropic Backends
79
94
  - Anthropic `document` content blocks are converted to markdown **before** the request reaches an
@@ -99,21 +114,23 @@ llm-relay
99
114
 
100
115
  | Command | Description |
101
116
  | :--- | :--- |
102
- | `llm-relay` | Start loopback HTTP proxy server on `127.0.0.1:8791` |
103
- | `llm-relay onboard` | Run guided setup wizard for 100%-free providers & subscription keys |
104
- | `llm-relay setup claude-desktop` (or `desktop`) | Auto-patch `claude_desktop_config.json` for Claude Desktop |
105
- | `llm-relay setup claude-cli` | Display & verify Claude CLI wrapper configuration |
106
- | `llm-relay keys` (or `check-keys`) | Validate provider API **credentials** (escalates past a public `/models` to an authenticated probe) |
107
- | `llm-relay pools [--probe]` | List pool members; `--probe` sends a real completion to each — the only check that catches a listed-but-dead **model** |
108
- | `llm-relay pools set|add|remove <name> <spec...>` | Create or edit pool membership; add `--free` (or `--include free`) for a dynamic free-model pool; `pools delete <name>` removes one |
109
- | `llm-relay routing ...` | Show or edit `routing.default`, Claude tiers, subagent destinations, benchmark sorting, and other routing fields |
110
- | `llm-relay config show|get|set|unset <path>` | Read or edit any JSON config path; values can be JSON, for example `config set routing.ladder '[...]'` |
111
- | `llm-relay telemetry` | Output live JSON telemetry, stability scores, and quota metrics |
112
- | `llm-relay models [-p <name>] [-r]` | Query live `/models` catalog per provider (`-p` filter, `-r` force refresh) |
113
- | `llm-relay ping [-p <name>]` | Perform live health, latency & quota probe across providers |
114
- | `llm-relay offload [client] [on\|off\|status] [--scope subagents\|all]` | Read or change one client's offload rule; changes apply to the next request, no restart |
115
- | `llm-relay candidates [-p <name>]` | The un-blended offload decision table (capability, cost, live health, quota, breaker state) |
116
- | `llm-relay dispatch [lane] [-t <task>] [--client <name>]` | Which lane to hand a whole delegated task to next; it returns the command, **you** run it (`-x <lane>` reports one spent) |
117
+ | `llm-relay` | Start proxy |
118
+ | `llm-relay onboard` | Set up provider keys |
119
+ | `llm-relay setup [target]` | `target`: `claude-cli` | `claude-desktop` |
120
+ | `llm-relay keys | check-keys` | Check provider keys |
121
+ | `llm-relay pools [--probe]` | List pool members; `--probe` tests each |
122
+ | `llm-relay pools <action> <name> [<spec>...]` | `action`: `set` | `add` | `remove` | `delete` |
123
+ | `llm-relay routing <action> ...` | `action`: `show` | `get` | `default` | `tier` | `subagent` | `sort` | `benchmark` | `set` | `unset` |
124
+ | `llm-relay config <action> [<path>] [<value>]` | `action`: `show` | `get` | `set` | `unset` |
125
+ | `llm-relay telemetry` | Print telemetry/quota JSON |
126
+ | `llm-relay models [-p <name>] [-r]` | List provider models |
127
+ | `llm-relay ping [-p <name>]` | Probe providers |
128
+ | `llm-relay offload [status]` | Show aggregate offload state |
129
+ | `llm-relay offload <harness> <on\|off> [--scope <scope>]` | Set one harness's rule |
130
+ | `llm-relay candidates [-p <name>]` | Show offload target data |
131
+ | `llm-relay dispatch [lane] [options]` | Choose next dispatch lane |
132
+ | `llm-relay help \| --help \| -h` | Full flag/endpoint reference |
133
+ | `llm-relay version \| --version \| -v` | Print version |
117
134
 
118
135
  ---
119
136
 
@@ -124,25 +141,25 @@ and validate the complete result before writing it. Restart a running proxy afte
124
141
 
125
142
  ```bash
126
143
  # Static pool: members are tried/ranked according to the normal pool rules.
127
- llm-relay pools set coding nim/z-ai/glm-5.2 openrouter/openai/gpt-5.2-codex
128
- llm-relay pools add coding gemini/gemini-2.5-flash
129
- llm-relay pools remove coding gemini/gemini-2.5-flash
130
- llm-relay pools delete coding
144
+ llm-relay pools set medium nim/z-ai/glm-5.2 openrouter/openai/gpt-5.2-codex
145
+ llm-relay pools add medium gemini/gemini-2.5-flash
146
+ llm-relay pools remove medium gemini/gemini-2.5-flash
147
+ llm-relay pools delete medium
131
148
 
132
- # Dynamic pool: keep preferred members, then append discovered free models.
133
- llm-relay pools set coding nim/z-ai/glm-5.2 --free
149
+ # Dynamic effort pool: an empty configured prefix, then evidence-ranked free models.
150
+ llm-relay pools set medium --free --effort medium
134
151
 
135
152
  # Main fallback, Claude tier maps, subagent destinations, and ranking.
136
153
  llm-relay routing default nim/z-ai/glm-5.2 openrouter/openai/gpt-5.2-codex
137
- llm-relay routing tier sonnet pool/coding
138
- llm-relay routing subagent default pool/fast
154
+ llm-relay routing tier sonnet pool/high
155
+ llm-relay routing subagent default pool/medium
139
156
  llm-relay routing sort off
140
157
  llm-relay routing tier opus --clear
141
158
 
142
159
  # Inspect or change any less-common routing field using a JSON value.
143
160
  llm-relay routing show
144
161
  llm-relay config get routing.pools
145
- llm-relay config set routing.ladder '[{"id":"fast","kind":"relay","spec":"pool/fast"}]'
162
+ llm-relay config set routing.ladder '[{"id":"medium","kind":"relay","spec":"pool/medium"}]'
146
163
  llm-relay config unset routing.ladder
147
164
  ```
148
165
 
@@ -315,17 +332,18 @@ a `routing` block that maps each request's `model` to one provider + backend mod
315
332
  "gemini": { "base": "https://generativelanguage.googleapis.com/v1beta/openai", "kind": "openai", "authEnv": "GEMINI_API_KEY" }
316
333
  },
317
334
  "routing": {
318
- // Any spec may be an ARRAY of candidates — that is what turns on benchmark ranking
319
- // (it only sorts when there is more than one) AND failover. A lone pinned model disables both.
320
- "default": ["nim/z-ai/glm-5.2", "nim/deepseek-ai/deepseek-v4-pro"],
335
+ "default": "pool/medium",
321
336
  "tiers": { // Claude tier (substring match) → provider/model
322
- "opus": ["nim/z-ai/glm-5.2", "nim/deepseek-ai/deepseek-v4-pro"],
323
- "sonnet": "nim/z-ai/glm-5.2",
324
- "haiku": "nim/openai/gpt-oss-20b", // cheap/fast — also catches Claude's haiku side-calls
325
- "fable": "nim/openai/gpt-oss-20b"
337
+ "opus": "pool/xhigh",
338
+ "fable": "pool/xhigh",
339
+ "sonnet": "pool/high",
340
+ "haiku": "pool/medium"
326
341
  },
327
342
  "pools": { // addressable as model "pool/<name>"
328
- "coding": ["nim/z-ai/glm-5.2", "nim/deepseek-ai/deepseek-v4-pro", "nim/moonshotai/kimi-k2.6"]
343
+ "low": { "preferred": [], "include": "free", "effort": "low" },
344
+ "medium": { "preferred": [], "include": "free", "effort": "medium" },
345
+ "high": { "preferred": [], "include": "free", "effort": "high" },
346
+ "xhigh": { "preferred": [], "include": "free", "effort": "xhigh" }
329
347
  }
330
348
  },
331
349
  "mode": "repair", // detect | repair (strict accepted, aliases detect)
@@ -340,7 +358,7 @@ a `routing` block that maps each request's `model` to one provider + backend mod
340
358
 
341
359
  **Routing (lifted from free-claude-code's proven scheme — split on the first `/` only):**
342
360
  1. **Pool** — a request `model` of `pool/<name>` expands to that pool's whole candidate list,
343
- which is then benchmark-ranked and failed over. Use this to ask for *the best available*
361
+ which is then fitness-ranked and failed over. Use this to ask for *the best available*
344
362
  model instead of naming one. An unknown pool is a **400, never a silent fallback** to the
345
363
  default — a typo must not quietly succeed against a different model.
346
364
  2. **Namespaced** — a request `model` of `provider/rest` where `provider` is a configured
@@ -360,18 +378,28 @@ reserved provider name; configuring a provider called `pool` fails at load.
360
378
  Pools can be static arrays, or automatic free-model pools:
361
379
 
362
380
  ```jsonc
363
- "coding": {
364
- "preferred": ["nim/z-ai/glm-5.2", "gemini/gemini-2.5-flash"],
365
- "include": "free"
381
+ "medium": {
382
+ "preferred": [],
383
+ "include": "free",
384
+ "effort": "medium"
366
385
  }
367
386
  ```
368
387
 
369
- The preferred targets remain first in exactly the written order. The relay then appends every
388
+ The configured prefix remains first in exactly the written order. The relay then appends every
370
389
  model discovered from a `tierType: "free"` provider (excluding a model when its catalog publishes
371
390
  a positive price), plus zero-priced or explicitly free-labelled models from `tierType: "mixed"`
372
- providers, and benchmark-ranks that tail.
391
+ providers. `effort` may be `low`, `medium`, `high`, or `xhigh`. These are cumulative raw-capability
392
+ floors (50/60/70/80), not ceilings. Admission compares a whole-point capability score; an existing
393
+ member remains until it falls two points below its floor, preventing refresh noise from flapping the
394
+ pool. Automatic membership also requires an exact SKU match and at least three published capability
395
+ or task-fit signals; confidence, stability, and metadata affect ordering, not eligibility.
396
+ A strong free model remains eligible for `low`, while higher effort narrows upward
397
+ (`xhigh ⊆ high ⊆ medium ⊆ low`). Exact SKUs known not to support tools are excluded.
373
398
  Catalog refreshes re-materialize the pool automatically; adding new free models never requires a
374
- config edit. Legacy array pools keep their existing whole-array `benchmarkSort` behaviour.
399
+ config edit. Materialization builds and ranks one common discovered roster, then filters that
400
+ snapshot into all effort pools; the result is reused for a 30-second ranking epoch and invalidated
401
+ immediately by a catalog revision. Legacy array pools keep their existing whole-array
402
+ `benchmarkSort` behaviour, with their ranking likewise reused within a short epoch.
375
403
 
376
404
  **What failover actually does** (both `/v1/messages` and `/v1/chat/completions`):
377
405
 
@@ -453,9 +481,12 @@ to marked subagents only (the current behavior) or to the whole conversation:
453
481
  ```jsonc
454
482
  "routing": {
455
483
  "tiers": { "opus": "anthropic", "sonnet": "anthropic", "haiku": "anthropic", "fable": "anthropic" },
456
- "subagents": { "opus": "pool/reasoning", "haiku": "pool/fast", "default": "pool/coding" },
484
+ "subagents": {
485
+ "opus": "pool/xhigh", "fable": "pool/xhigh",
486
+ "sonnet": "pool/high", "haiku": "pool/medium", "default": "pool/medium"
487
+ },
457
488
  "offload": {
458
- "claude": { "enabled": true, "scope": "subagents" },
489
+ "claude": { "enabled": true, "scope": "subagents", "freeOnly": true },
459
490
  "codex": { "enabled": false, "scope": "all" }
460
491
  }
461
492
  }
@@ -466,17 +497,26 @@ to marked subagents only (the current behavior) or to the whole conversation:
466
497
  Claude or Codex quota is exhausted. Rules may use any future client name; an explicit `default`
467
498
  rule is the opt-in catch-all for otherwise unnamed front doors. All rules are off by default.
468
499
 
469
- The CLI changes one client without restarting the proxy:
500
+ `"freeOnly": true` is the money guard: this client's rerouted traffic may only reach deployments
501
+ assessed **free** (published zero price, an explicitly `:free`-labelled id, or a `tierType: "free"`
502
+ provider). Anything else — including *unknown* cost, and the Anthropic passthrough — is filtered
503
+ out after pool expansion, and if nothing free remains the request is **refused with a clean 503**
504
+ naming the rule, never silently sent somewhere that bills. It also binds per-call `@relay:`
505
+ directives, so a subagent prompt cannot spend money past it. Set it in the config file or with
506
+ `llm-relay config set routing.offload.claude.freeOnly true` (restart applies it).
507
+
508
+ The CLI changes one harness without restarting the proxy:
470
509
 
471
510
  ```bash
472
511
  llm-relay offload status
473
- llm-relay offload claude on --scope subagents
474
- llm-relay offload codex on --scope all
475
- llm-relay offload claude off
512
+ llm-relay offload <harness> <on|off> [--scope <scope>]
476
513
  ```
477
514
 
478
- The old `llm-relay offload on|off` command remains a global compatibility switch for configs that
479
- still use the boolean form (`"offload": false`). `GET /offload?client=claude` reads one rule;
515
+ `<harness>` is `claude`, `codex`, or another configured client. `<scope>` is `subagents` or
516
+ `all` (default: `subagents`).
517
+
518
+ The legacy boolean form remains supported in config files as a global subagents-only rule
519
+ (`"offload": false`). The CLI requires a harness name for changes. `GET /offload?client=claude` reads one rule;
480
520
  `POST /offload` accepts `{"client":"claude","enabled":true,"scope":"all"}`. Changes are
481
521
  persisted and take effect on the next request.
482
522
 
@@ -533,7 +573,7 @@ description = "Read-only coding child routed through llm-relay."
533
573
  developer_instructions = "Work read-only. Return a concise result to the parent and do not modify files."
534
574
 
535
575
  model_provider = "llm-relay"
536
- model = "pool/coding"
576
+ model = "pool/medium"
537
577
  model_reasoning_effort = "medium"
538
578
  ```
539
579
 
@@ -546,12 +586,12 @@ description = "General-purpose read-only child routed through llm-relay."
546
586
  developer_instructions = "Work read-only. Return a concise result to the parent and do not modify files."
547
587
 
548
588
  model_provider = "llm-relay"
549
- model = "pool/coding"
589
+ model = "pool/medium"
550
590
  model_reasoning_effort = "medium"
551
591
  ```
552
592
 
553
593
  With that override, a normal “use a subagent” request keeps the parent native while the generic child
554
- goes through `pool/coding`; named agents can still select a different pool explicitly.
594
+ goes through `pool/medium`; named agents can still select a different pool explicitly.
555
595
 
556
596
  Run Codex normally, without the `llm-relay` profile. Ask the parent to use exactly one subagent of
557
597
  type `relay_coding`; Codex keeps the parent on its normal provider and starts the child through the
@@ -560,11 +600,11 @@ relay. The relay pool then chooses the configured provider and can fail over nor
560
600
  Enable only Codex child offload in `~/.llm-relay/config.json`:
561
601
 
562
602
  ```bash
563
- llm-relay offload codex on --scope subagents
603
+ llm-relay offload <harness> on --scope <scope>
564
604
  ```
565
605
 
566
- To redirect the parent Codex conversation through the same relay as well, use
567
- `llm-relay offload codex on --scope all`. Claude's rule is unaffected.
606
+ For Codex, use `harness=codex` with `scope=subagents`; use `scope=all` to include the parent
607
+ conversation. Claude's rule is unaffected.
568
608
 
569
609
  The `llm-relay` profile remains available as an explicit all-relay mode, but it routes the parent
570
610
  through the relay too and is not the split setup described above. The automatic
@@ -576,18 +616,23 @@ This applies to local Codex clients that can reach `127.0.0.1`. Hosted ChatGPT/C
576
616
  reach a loopback relay, and the relay cannot spend a ChatGPT subscription on behalf of an upstream
577
617
  request; those remain separate CLI/client-bound dispatch lanes.
578
618
 
579
- Whole-task CLI dispatch can likewise vary by tier with `routing.ladders.{reasoning,coding,fast}`.
580
- Use `llm-relay dispatch --tier reasoning -t "..."`; without `--tier`, the ladder matching
581
- `subagents.default` is selected (normally `coding`). The legacy single `routing.ladder` remains
619
+ Whole-task CLI dispatch can likewise vary by tier with `routing.ladders.{low,medium,high,xhigh}`.
620
+ Use `llm-relay dispatch --tier high -t "..."`; without `--tier`, the ladder matching
621
+ `subagents.default` is selected (normally `medium`). The legacy single `routing.ladder` remains
582
622
  supported for configurations that do not need tier-specific CLI models.
583
623
 
624
+ When a lane turns out to be spent, say **which way**: `llm-relay dispatch -x <lane> --outcome
625
+ rate_limited` cools it briefly (15 min default — rate limits reset on a fast clock) while
626
+ `--outcome quota_exhausted` cools it for an hour; `--retry-after-ms <n>` passes the vendor's own
627
+ stated reset and beats both defaults. A plain `-x <lane>` keeps its old 15-minute behaviour.
628
+
584
629
  ### Choosing where to offload (`llm-relay candidates`)
585
630
 
586
631
  ```
587
- target pools / tiers str agentic coding BFCL arena $/Mout verdict p95 quota breaker ctx
588
- nim/z-ai/glm-5.2 coding,@opus 83.3/4 43.1 68.8 - - $2.402~ Perfect 310ms 84% closed 1049k~
589
- nim/moonshotai/kimi-k2.6 coding,@sonnet 77.4/5 30.3 61.8 - 1461 $2.72~ Perfect 280ms 84% closed 262k~
590
- nim/meta/llama-3.1-8b-instruct fast,@haiku,@fable 9.7/6 0.5 5.4 25.83 1211 $0.08~ Perfect 120ms 84% closed 131k~
632
+ target pools / tiers fit raw cap agentic coding BFCL arena $/Mout verdict p95 quota breaker ctx
633
+ ollama-cloud/kimi-k3 low,medium,high... 79.9 96.6 87.3/4 50.1 76.2 - - - Pending - - closed 1049k~
634
+ nim/z-ai/glm-5.2 low,medium,high... 71.8 83.3 76.6/4 43.1 68.8 - - $2.402~ Pending - - closed 1049k~
635
+ nim/deepseek-ai/deepseek-v4-pro low,medium,@haiku 64.9 67.4 67.4/5 36.4 59.4 - 1457 $0.87~ Pending - - closed 1049k~
591
636
  ```
592
637
 
593
638
  Every offload target with its dimensions side by side: capability from each leaderboard separately,
@@ -609,15 +654,34 @@ They **disagree** — the agentic index puts deepseek above kimi while the codin
609
654
  above deepseek — which is exactly why each keeps its own column, and why a blank cell means *not
610
655
  measured*, never *bad*.
611
656
 
612
- **Nothing is ranked or averaged across dimensions** — capability, latency and remaining quota trade
613
- off differently per task, and one blended number answers neither "cheapest that can do it" nor "best
614
- available".
615
-
616
- `str` is the single exception, and it exists only because ordering a pool requires an order. It is a
617
- weighted mean of whatever rank-normalized signals a model actually has (tool-use and agentic ability
618
- weighted highest this proxy drives tool loops), and it never appears without its provenance:
619
- `83.3/4` means four published signals backed it, while `obs` (ranked on this proxy's own traffic,
620
- ≥5 calls) and `neut` (nothing known) mark the fallbacks.
657
+ The raw dimensions remain separate — capability, latency and remaining quota answer different
658
+ questions. Pool ordering uses three explicit derived scores:
659
+
660
+ - `raw` is fixed at 40% agentic/tool use, 35% coding, and 25% general reasoning. Each source is
661
+ mapped through persisted raw-value calibration anchors, so an unrelated leaderboard addition
662
+ cannot silently move every model. If an entire dimension is missing, it is estimated by ridge
663
+ regression from models with overlapping dimensions rather than disappearing from the denominator.
664
+ Artificial Analysis Agentic and BFCL Overall feed agentic capability; AA Coding and Aider pass
665
+ rate feed coding; AA Intelligence and LMArena feed general reasoning.
666
+ - Design Arena's differently covered specialist categories, BFCL irrelevance, and Aider formatting
667
+ compliance are task-fit signals, not raw capability. This prevents a model measured on a favorable
668
+ specialized subset from gaining an effort tier.
669
+ - `cap` is `raw` shrunk toward neutral by capability evidence confidence. Direct dimension coverage,
670
+ published capability signals, and imputation quality determine confidence; fuzzy model-name
671
+ matches get half confidence. It affects ordering, never effort eligibility. `/4c5p` means four
672
+ direct capability signals and five total publications; `neut` means no capability evidence.
673
+ Operational telemetry never substitutes for capability.
674
+ - `fit` is 75% `cap`, 20% deployment operations, and 5% task-fit metadata. Operations combine
675
+ synthetic probe stability with success/speed/recency from at least five real calls. Metadata
676
+ uses the separate specialist/behavior score, exact-SKU tool support, and provider/reference
677
+ context and output limits. Missing inputs are neutral (50), not zero. A known tool-incompatible
678
+ SKU is excluded from automatic effort pools; breaker-open and credential-faulted deployments are
679
+ demoted after scoring.
680
+
681
+ Only coarse `raw` capability plus the exact-match/three-publication gate decides whether a model
682
+ clears an effort floor. The generated snapshot persists the two-point exit band. `fit` decides the
683
+ order among eligible deployments. The JSON view exposes dimensions, direct/imputed coverage, task
684
+ fit, and confidence.
621
685
 
622
686
  **Limits and prices are per-(provider, model), and labelled.** The same model id on two providers is
623
687
  two deployments — different context ceilings, different output caps, and possibly free on one and
@@ -644,7 +708,7 @@ an anthropic provider has no fixed model id to reshape on, so it needs an explic
644
708
  **Prefer the pool form — do not pin one reshaper model:**
645
709
 
646
710
  ```jsonc
647
- "reshaper": { "pool": "coding" } // ranked candidates, tried in order
711
+ "reshaper": { "pool": "medium" } // ranked candidates, tried in order
648
712
  ```
649
713
 
650
714
  A pinned `{ "base": …, "model": … }` still works, but if the provider stops serving that exact id
@@ -684,9 +748,10 @@ llm-relay models --provider nim # one provider
684
748
  llm-relay models --provider nim --refresh # force a re-fetch
685
749
  ```
686
750
 
687
- On startup the proxy warms the cache and **warns about any routing target its
688
- provider doesn't serve** so a stale/typo'd tier model is caught at boot, not
689
- silently at request time.
751
+ On startup the proxy warms providers referenced by routing plus free/mixed providers that can
752
+ contribute to dynamic pools, then **warns about any routing target its provider doesn't serve**.
753
+ Unrelated subscription catalogs stay lazy until first use, while a stale/typo'd routed model is
754
+ still caught at boot rather than silently failing on its first request.
690
755
 
691
756
  > Provider notes: **Groq** returns `403 "check your network settings"` from some
692
757
  > IPs/regions (a network-side block, not a key issue) — it works once your network
@@ -720,7 +785,7 @@ no mode switch. So the tier map stays the default, and dispatcher-style usage is
720
785
  "send namespaced ids + read `/registry`".
721
786
 
722
787
  OpenAI-native clients can point their base URL at `http://127.0.0.1:8791/v1` and use a
723
- namespaced model such as `anthropic/claude-sonnet-4-20250514` or `pool/coding`. Codex uses
788
+ namespaced model such as `anthropic/claude-sonnet-4-20250514` or `pool/medium`. Codex uses
724
789
  `/v1/responses`; other IDEs commonly use `/v1/chat/completions`. Configure the Anthropic
725
790
  provider with `kind: "anthropic"` and `authEnv: "ANTHROPIC_API_KEY"` when the relay should
726
791
  use its own key, or omit `authEnv` for an intentional caller-credential passthrough.
@@ -790,7 +855,7 @@ provider and point every tier at it:
790
855
  "routing": {
791
856
  "default": "anthropic",
792
857
  "tiers": { "opus": "anthropic", "sonnet": "anthropic", "haiku": "anthropic", "fable": "anthropic" },
793
- "pools": { "coding": ["nim/z-ai/glm-5.2", "nim/deepseek-ai/deepseek-v4-pro"] }
858
+ "pools": { "medium": ["nim/z-ai/glm-5.2", "nim/deepseek-ai/deepseek-v4-pro"] }
794
859
  }
795
860
  ```
796
861
 
@@ -38,55 +38,41 @@
38
38
  }
39
39
  },
40
40
  "routing": {
41
- "default": [
42
- "groq/llama-3.3-70b-versatile",
43
- "openrouter/openrouter/free",
44
- "nim/z-ai/glm-5.2",
45
- "gemini/gemini-2.5-flash",
46
- "ollama/qwen2.5-coder:32b"
47
- ],
41
+ "default": "pool/medium",
48
42
  "tiers": {
49
- "opus": [
50
- "nim/nvidia/nemotron-3-super-120b-a12b",
51
- "gemini/gemini-2.5-pro"
52
- ],
53
- "sonnet": [
54
- "groq/llama-3.3-70b-versatile",
55
- "nim/z-ai/glm-5.2",
56
- "gemini/gemini-2.5-flash",
57
- "ollama/qwen2.5-coder:32b"
58
- ],
59
- "haiku": [
60
- "groq/llama-3.1-8b-instant",
61
- "nim/meta/llama-3.1-8b-instruct",
62
- "openrouter/openrouter/free",
63
- "ollama/deepseek-r1:14b"
64
- ],
65
- "fable": [
66
- "groq/llama-3.1-8b-instant",
67
- "ollama/deepseek-r1:14b"
68
- ]
43
+ "opus": "pool/xhigh",
44
+ "fable": "pool/xhigh",
45
+ "sonnet": "pool/high",
46
+ "haiku": "pool/medium"
69
47
  },
70
48
  "pools": {
71
- "coding": {
72
- "preferred": ["nim/z-ai/glm-5.2", "groq/llama-3.3-70b-versatile"],
73
- "include": "free"
49
+ "low": {
50
+ "preferred": [],
51
+ "include": "free",
52
+ "effort": "low"
74
53
  },
75
- "reasoning": {
76
- "preferred": ["nim/nvidia/nemotron-3-super-120b-a12b", "gemini/gemini-2.5-pro"],
77
- "include": "free"
54
+ "medium": {
55
+ "preferred": [],
56
+ "include": "free",
57
+ "effort": "medium"
78
58
  },
79
- "fast": {
80
- "preferred": ["groq/llama-3.1-8b-instant", "nim/meta/llama-3.1-8b-instruct"],
81
- "include": "free"
59
+ "high": {
60
+ "preferred": [],
61
+ "include": "free",
62
+ "effort": "high"
63
+ },
64
+ "xhigh": {
65
+ "preferred": [],
66
+ "include": "free",
67
+ "effort": "xhigh"
82
68
  }
83
69
  },
84
70
  "subagents": {
85
- "opus": "pool/reasoning",
86
- "sonnet": "pool/coding",
87
- "haiku": "pool/fast",
88
- "fable": "pool/fast",
89
- "default": "pool/coding"
71
+ "opus": "pool/xhigh",
72
+ "sonnet": "pool/high",
73
+ "haiku": "pool/medium",
74
+ "fable": "pool/xhigh",
75
+ "default": "pool/medium"
90
76
  },
91
77
  "offload": {
92
78
  "claude": { "enabled": false, "scope": "subagents" },