llm-relay 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,900 +1,54 @@
1
1
  # llm-relay
2
2
 
3
- A standalone, **loopback** bidirectional LLM API proxy. It forwards Anthropic `/v1/messages` and OpenAI `/v1/chat/completions` or `/v1/responses` requests to any configured backend, translating protocols where needed. Anthropic responses still pass through the relay's **tool-call validation/repair** layer, so Claude Code and OpenAI-native clients can share the same routed providers.
3
+ A loopback proxy that steers your LLM traffic across providers. Point Claude Code, Codex, or
4
+ any Anthropic/OpenAI-compatible client at `http://127.0.0.1:8791`; the relay resolves the
5
+ requested model to a real deployment, ranks candidates by benchmark and live health, fails
6
+ over on errors, and validates/repairs malformed tool calls so agent harnesses can run on
7
+ weaker (often free) models.
4
8
 
5
- **The one boundary:** it fixes/flags *protocol form* (malformed tool calls), never *judgment* (bad reasoning).
6
-
7
- ## What it does
8
-
9
- - **Transparent passthrough** — forwards streaming and non-streaming `/v1/messages` byte-for-byte.
10
- - **`detect` mode** — deterministic tool_use validation (Ajv2020) with metadata-only logging of pass/fail/uncheckable. Behavior is unchanged; it only observes.
11
- - **`repair` mode** — on a validation failure, a cheap reshaper model corrects the call, the result is **re-validated**, and the corrected response is re-emitted (JSON or freshly-serialized SSE). Destructive-tool calls are **refused, never fabricated**; unrepairable calls **fail-clean** (502). Valid calls pass through untouched. The refusal matches the tool **name exactly** (case-insensitively) — see [Destructive-tool refusal](#destructive-tool-refusal-repairdestructivetools) for which tools that now covers.
12
- - **OpenAI-compatible backends** (`backend.kind:"openai"`) — front NIM / vLLM / OpenRouter / LM Studio. Requests are translated Anthropic→OpenAI and responses back (streaming SSE + non-streaming) via [`llm-bridge`](https://github.com/supermemoryai/llm-bridge) (zero-dep). The validate/repair layer always sees Anthropic Messages, regardless of backend. Verified live end-to-end.
13
- - **Bidirectional OpenAI front** — `POST /v1/chat/completions` and `POST /v1/responses` work against both `kind:"openai"` and `kind:"anthropic"` targets. OpenAI Chat Completions remains byte-transparent to OpenAI backends; Responses and Anthropic targets use the same Anthropic-shaped translation seam, including streaming SSE and tool calls.
14
- - **Streaming repair** — text-block SSE frames stream to the client **as they arrive**; the proxy only withholds from the first `tool_use` block. A pure-text response is byte-for-byte passthrough with zero added latency; a valid tool call flushes the withheld frames verbatim; an invalid one is repaired with only the corrected trailing blocks re-emitted (`message_start` + leading text already delivered). A mid-stream repair failure surfaces as an SSE `error` event, never a fabricated call. Handles LF and CRLF frame delimiters and multibyte UTF-8 across chunk boundaries.
15
-
16
- ### Live demo (no external creds)
17
-
18
- ```bash
19
- npm run build && node scripts/live-demo.mjs
20
- ```
21
- Runs the compiled CLI as a real process against a local flaky-model backend + stub reshaper, showing detect (logs the failure) then repair (delivers the fixed call).
22
-
23
- ## Quick Start & Free Model Onboarding
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). 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
-
27
- ### Step 1: Get keys
28
- ```bash
29
- npx llm-relay onboard
30
- ```
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
-
33
- ### Step 2: Point your client at the relay
34
-
35
- **For Claude Desktop:**
36
- ```bash
37
- llm-relay setup claude-desktop
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`), 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
-
41
- **For Claude CLI (`claude`):**
42
- ```bash
43
- llm-relay setup claude-cli
44
- ```
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
-
47
- ### Step 3: Start it and prove it works
48
- ```bash
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?
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`.
65
-
66
- ---
67
-
68
- ## Key Capabilities
69
-
70
- ### 1. 100%-Free Presets & Subscription Pooling
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.
72
- - **Subscription Tier**: Mapped as `subscription` in config (e.g. OpenAI `OPENAI_API_KEY`, Anthropic `ANTHROPIC_API_KEY`).
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.)
74
-
75
- ### 2. Stability-Aware Dynamic Routing & Auto-Failover
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.
77
- - Target selection dynamically sorts candidates by Stability Score and automatically cascades on 429 rate limits or timeouts.
78
- - **Multi-Candidate Failover**: `routing.default` and every `routing.tiers` entry accept an **array** of target specs (e.g. `["nim/z-ai/glm-5.2", "groq/llama-3.3-70b"]`) for continuous fallback. ⚠ Ranking and failover both require **more than one** candidate — a single pinned model silently disables both, and on providers where a listed model may not actually be servable that turns one dead backend into a dead relay. Prefer arrays.
79
- - **Named pools** (`routing.pools`, addressed as `model: "pool/<name>"`): the same ranked-candidate behaviour for callers that can only send **one model string** — notably Claude Code subagent frontmatter. Lets an agent ask for *the best available coding model* instead of naming one. An unknown pool is a loud 400, never a silent fall-through.
80
- - **Passthrough targets**: a provider with `kind:"anthropic"` and **no `authEnv`** forwards the caller's own credentials untouched, so real Claude traffic stays on real Anthropic while `pool/*` requests route elsewhere — from the same proxy.
81
- - **Granular offload** (`routing.offload` + `routing.subagents`): independently route Claude, Codex, and future client requests to other providers. Each client can be limited to subagents or set to `scope: "all"` to reroute its main conversation too. **Off by default**; `llm-relay candidates` shows what to point it at. See below.
82
-
83
- ### 3. Prompt Token & Context Length Guardrails
84
- - Estimates the request's prompt token count (`estimateRequestTokens`) against the target model's context limit, read from the warm catalog cache (`cachedLimits()` — it never fetches, so a cold cache costs no round-trip on the request path).
85
- - Rejects an oversized request before network transmission with an HTTP 400 (`request prompt estimated tokens … exceeds the context limit …`), protecting backends from context-window overflow.
86
- - ⚠ **It only fires against a limit the *serving* provider published.** If that provider publishes no limit (NIM publishes none), there is no guardrail: the request goes upstream and the backend returns its own authoritative error. llm-relay will not reject a request against a number it guessed — see the per-(provider, model) note under [Choosing where to offload](#choosing-where-to-offload-llm-relay-candidates).
87
-
88
- ### 4. Background Adaptive Health Monitoring & Persistent Caching
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).
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.
92
-
93
- ### 5. Document (PDF/Office) Attachments on Non-Anthropic Backends
94
- - Anthropic `document` content blocks are converted to markdown **before** the request reaches an
95
- OpenAI-compatible backend, via [MarkItDown](https://github.com/microsoft/markitdown). Supported:
96
- PDF, `.docx`, `.pptx`, `.xlsx`, CSV, HTML, JSON, plain text and markdown.
97
- - MarkItDown is an **optional** external dependency (a Python CLI). Install it with
98
- `pip install 'markitdown[all]'`, or point `LLM_RELAY_MARKITDOWN` at the executable. Without it, a
99
- request carrying a document gets a clear HTTP 400 naming the install command.
100
- - Images (`image` blocks, base64 and url sources) pass through natively and need nothing installed.
101
- - A document that can't be converted is **refused, never truncated or inlined raw** — the underlying
102
- translation library would otherwise stringify the block and inject the whole base64 payload into
103
- the prompt.
104
-
105
- ### 6. Programmatic Telemetry & Quota Access for Claude
106
- - **Tokenless status endpoints**: `GET /models`, `GET /telemetry`, `GET /offload`, and `GET /dispatch` are side-effect-free status reads.
107
- - **Capability-protected control endpoints**: `POST /offload`, `POST /dispatch`, `GET /ping`, `GET /registry`, `GET /health`, and `GET /candidates`. The CLI automatically carries the per-install 256-bit capability stored in `~/.llm-relay/control-token`; it is never forwarded to providers. ⚠ **Loopback is not authorization** — every request also requires a `Host` exactly matching the bound listener authority, a present `Origin` must match its exact scheme/host/effective port, and `Origin: null` is rejected. Mutating requests additionally require `content-type: application/json`.
108
- - **CLI Commands**: `llm-relay telemetry` outputs live telemetry metrics; `llm-relay models` lists live model catalogs with SWE-bench & quality scores; `llm-relay ping` performs live health & latency probes.
109
- - **Response Headers**: Proxy responses include `x-llm-relay-quota-percent`, `x-llm-relay-stability-score`, and `x-llm-relay-target`.
110
-
111
- ---
112
-
113
- ## CLI Command Reference
114
-
115
- | Command | Description |
116
- | :--- | :--- |
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 |
134
-
135
- ---
136
-
137
- ### Configure pools and routing from the CLI
138
-
139
- The configuration commands edit the selected JSON file (`--config` or the normal global config)
140
- and validate the complete result before writing it. Restart a running proxy after a routing edit.
9
+ ## Quick start
141
10
 
142
- ```bash
143
- # Static pool: members are tried/ranked according to the normal pool rules.
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
148
-
149
- # Dynamic effort pool: an empty configured prefix, then evidence-ranked free models.
150
- llm-relay pools set medium --free --effort medium
151
-
152
- # Main fallback, Claude tier maps, subagent destinations, and ranking.
153
- llm-relay routing default nim/z-ai/glm-5.2 openrouter/openai/gpt-5.2-codex
154
- llm-relay routing tier sonnet pool/high
155
- llm-relay routing subagent default pool/medium
156
- llm-relay routing sort off
157
- llm-relay routing tier opus --clear
158
-
159
- # Inspect or change any less-common routing field using a JSON value.
160
- llm-relay routing show
161
- llm-relay config get routing.pools
162
- llm-relay config set routing.ladder '[{"id":"medium","kind":"relay","spec":"pool/medium"}]'
163
- llm-relay config unset routing.ladder
164
- ```
165
-
166
- `routing set <path> <value>` and `routing unset <path>` are shorter forms for paths below
167
- `routing`. `llm-relay pools --probe` remains the liveness check to run after changing membership;
168
- editing a pool does not imply that every model is usable.
169
-
170
- ---
171
-
172
- ## Install & run
173
-
174
- ### Option 1: Instant run (no installation required)
175
- ```bash
176
- NVIDIA_API_KEY=nvapi-... npx llm-relay
177
- ```
178
-
179
- ### Option 2: Global installation
180
11
  ```bash
181
12
  npm install -g llm-relay
182
-
183
- # Start proxy:
184
- llm-relay
185
-
186
- # Check key status & signup URLs:
187
- llm-relay keys
188
-
189
- # Check that every model in your pools actually answers:
190
- llm-relay pools --probe
191
- ```
192
-
193
- **New here?** [docs/QUICKSTART.md](docs/QUICKSTART.md) is a staged setup guide written to be
194
- handed straight to an AI assistant ("set this up for me"), covering free providers, the offload
195
- switch, local models, and using your other CLI subscriptions as fallback lanes.
196
-
197
- ### Release publishing
198
-
199
- Releases publish through npm Trusted Publishing (GitHub Actions OIDC); no `NPM_TOKEN` is stored in
200
- the repository. After merging a version bump to `main`, push the matching tag:
201
-
202
- ```bash
203
- git tag vX.Y.Z
204
- git push origin vX.Y.Z
13
+ llm-relay onboard # collect free provider keys (NIM, Groq, Gemini, OpenRouter, ...)
14
+ llm-relay setup claude-desktop # or: llm-relay setup claude-cli
15
+ llm-relay # start the proxy — leave it running
205
16
  ```
206
17
 
207
- `.github/workflows/publish.yml` accepts only `v*` tags from this repository, verifies that the tag
208
- is contained in the default branch and matches `package.json`, then publishes with npm 11.5.1+.
209
- The one-time setup also requires the npm trusted publisher to reference this repository and
210
- workflow, plus the protected GitHub `npm-publish` environment to carry its approval rules.
211
-
212
- ### Verifying a setup — two checks, two different questions
213
-
214
- `keys` answers *are my credentials good?* `pools --probe` answers *will the models I configured
215
- actually answer?* Both are needed, and the cheap one can be confidently wrong in either
216
- direction:
217
-
218
- - A 200 from a provider's `/models` proves nothing when that endpoint is **public** — a revoked
219
- key still returns the full catalogue. `keys` now re-probes anonymously and escalates to an
220
- authenticated completion when it must.
221
- - A 401/403 on that probe does **not** prove the key is bad — free-tier rosters list premium
222
- models a valid key cannot touch. The probe is compared against the same request sent with no
223
- credentials: a different status means the key authenticated; an identical one means nothing
224
- could be concluded, reported as `UNVERIFIED` rather than as a bad key.
225
- - Neither of those can see a model that is configured, catalogued, and dead. Only
226
- `pools --probe` can.
227
-
228
- Keys are read from the environment and, if present, from `~/.llm-relay/.env` (one `KEY=value`
229
- per line). **A variable already set in the environment always wins over the file.**
230
-
231
- A global install drops the same generated **llm-relay skill description** into both host skill
232
- directories: `~/.claude/skills/llm-relay/SKILL.md` for Claude Code and
233
- `~/.codex/skills/llm-relay/SKILL.md` for Codex. Both are copied from the package's single
234
- `skills/llm-relay/SKILL.md` source, so the operating guide (addressing pools/models, the offload
235
- switch, `@relay:` directives, reading the candidates table, failure modes) cannot drift between
236
- hosts. Both refresh automatically on every upgrade; local/dev installs touch neither directory.
237
-
238
- The same global install also provisions local Codex: it adds the `llm-relay` Responses provider to
239
- `~/.codex/config.toml` and creates relay-backed `default` and `relay_coding` child agents under
240
- `~/.codex/agents/` when those files are absent. Existing Codex config and agent files are preserved.
241
- This keeps the parent on its normal provider while making generic or named child dispatches use the
242
- relay automatically.
243
-
244
- If your npm blocks unknown install scripts (`npm warn install-scripts … blocked`), allow this one —
245
- `npm config set allow-scripts=llm-relay --location=user` — or install the host integrations by hand:
246
- `node "$(npm root -g)/llm-relay/scripts/install-skill.mjs" --force`.
247
-
248
- ### Staying current
249
-
250
- Every start (except `help` and `version`) compares the running version against the npm registry —
251
- answer cached 6h in `~/.llm-relay/update-check.json`, 2.5s timeout, and any failure is silent and
252
- non-blocking, so an offline or slow registry never delays a start.
253
-
254
- When a newer version exists:
255
-
256
- - **a global install updates itself** — `npm install -g llm-relay@<latest>`, then it re-execs into the
257
- new build and runs your command there. Nothing to remember, and no half-updated state: if the install
258
- or the version check after it fails, it says so and continues on the version you already had.
259
- - **any other copy** (source checkout, `npx`, project dependency) just prints the version gap and the
260
- exact upgrade command, and continues.
261
-
262
- The replace is clean. Any bin shim the *old* version installed that the new one no longer declares is
263
- deleted in all of npm's spellings (bare, `.cmd`, `.ps1`, `.bat`), so a renamed or dropped command can
264
- never leave a dangling entry on your `PATH`. If pre-existing shims block npm's overwrite (`EEXIST` —
265
- typically left by a `npm link` or a half-finished install), the update clears them and reinstalls rather
266
- than leaving you pinned to an old build.
267
-
268
- Set `LLM_RELAY_NO_SELF_UPDATE=1` to skip the check entirely — it is also set automatically on the
269
- re-exec'd process, so an update can never recurse.
270
-
271
- ## Use it from your projects
272
-
273
- Point the `claude` CLI at the running proxy. **The one thing that matters:** give claude an **isolated `CLAUDE_CONFIG_DIR`**. Without it, an active claude.ai subscription session conflicts with the proxy token and claude fails client-side with `Invalid API key` / `401 Invalid bearer token` before any request is even sent. With it, the proxy's provider token is the sole credential — and your subscription is never in the path (the safe direction).
274
-
275
- Wrappers do this for you (they also set the thinking/beta/attribution flags the harness needs against a non-Anthropic model):
276
-
277
- ```powershell
278
- # PowerShell (from any project directory)
279
- C:\Code\llm-relay\scripts\claude-proxied.ps1 -p "list the files here"
280
- ```
281
- ```bash
282
- # bash
283
- /c/Code/llm-relay/scripts/claude-proxied.sh -p "list the files here"
284
- ```
285
-
286
- Or inline, if you'd rather not use the wrapper:
18
+ Then verify:
287
19
 
288
20
  ```bash
289
- env -u CLAUDECODE -u ANTHROPIC_API_KEY \
290
- CLAUDE_CONFIG_DIR="$HOME/.llm-relay-claude" \
291
- ANTHROPIC_BASE_URL=http://127.0.0.1:8791 \
292
- ANTHROPIC_AUTH_TOKEN=dummy \
293
- CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1 CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 CLAUDE_CODE_ATTRIBUTION_HEADER=0 \
294
- claude -p "list the files here"
295
- ```
296
-
297
- `ANTHROPIC_AUTH_TOKEN` can be `dummy` — the proxy strips inbound auth and injects the real backend key itself (from `authEnv`). Override the wrapper defaults with `RP_PROXY_URL`, `RP_AUTH`, `RP_CONFIG_DIR`. Verified live end-to-end: a real `claude` agentic session (tool_use → tool_result → answer) completes through the proxy against NIM.
298
-
299
- > Backend note: weak models still fail *reasoning* (they may loop or skip a tool) — repair fixes malformed tool-call *form*, not judgment. Pick a strong tool-caller as the backend model. NIM also rate-limits (HTTP 429) under load; claude's own retry/backoff absorbs it.
300
-
301
- ### What Claude Code gives up behind ANY custom `ANTHROPIC_BASE_URL`
302
-
303
- **None of these are caused by llm-relay, and none can be fixed by llm-relay** — Claude Code changes
304
- its own behaviour the moment `ANTHROPIC_BASE_URL` is not `api.anthropic.com`. They apply equally to
305
- any gateway (headroom, LiteLLM, a corporate proxy). Listed here because the symptoms look like proxy
306
- bugs and cost real time to diagnose otherwise.
307
-
308
- Verified against **Claude Code 2.1.220 (2026-07-28)**. These are client-version behaviours, not
309
- laws — re-check after a Claude Code upgrade.
310
-
311
- | What breaks | Why | Workaround |
312
- |---|---|---|
313
- | **1M context silently drops to 200k** | Claude Code omits the `context-1m-2025-08-07` beta header behind a custom base URL. Nothing errors — you just quietly get a smaller window than you are entitled to. | **Yes.** Pin the model with a `[1m]` suffix at launch: `ANTHROPIC_MODEL='claude-opus-5[1m]' claude`. Cost: this *pins* the model and overrides the in-session model picker, so set it per-launch, not globally. |
314
- | **`/remote-control` (`/rc`) is disabled** | Claude Code ≥2.1.196 hard-gates Remote Control to `api.anthropic.com`; the check is compiled in ("Remote Control is only available when using Claude via api.anthropic.com"). It also breaks under `ANTHROPIC_AUTH_TOKEN` alone. | **None.** It is a binary choice: a proxy, or Remote Control. Unset `ANTHROPIC_BASE_URL` to get it back. |
315
- | **MCP tool search off by default** | Disabled behind a non-first-party base URL. | Set `ENABLE_TOOL_SEARCH=true` (needs the proxy to forward `tool_reference` blocks — llm-relay does). |
316
-
317
- llm-relay forwards `anthropic-beta` verbatim on passthrough targets, so the 1M header **does** survive
318
- the proxy hop — the header is simply never sent by the client in the first place. That is why the
319
- workaround is client-side.
320
-
321
- ## Config — multi-provider registry
322
-
323
- A `providers{}` registry (any number of OpenAI-compatible or Anthropic backends) plus
324
- a `routing` block that maps each request's `model` to one provider + backend model:
325
-
326
- ```jsonc
327
- {
328
- "listen": "127.0.0.1:8791", // loopback ONLY — startup refuses non-loopback
329
- "providers": {
330
- "nim": { "base": "https://integrate.api.nvidia.com/v1", "kind": "openai", "authEnv": "NVIDIA_API_KEY" },
331
- "openrouter": { "base": "https://openrouter.ai/api/v1", "kind": "openai", "authEnv": "OPENROUTER_API_KEY" },
332
- "gemini": { "base": "https://generativelanguage.googleapis.com/v1beta/openai", "kind": "openai", "authEnv": "GEMINI_API_KEY" }
333
- },
334
- "routing": {
335
- "default": "pool/medium",
336
- "tiers": { // Claude tier (substring match) → provider/model
337
- "opus": "pool/xhigh",
338
- "fable": "pool/xhigh",
339
- "sonnet": "pool/high",
340
- "haiku": "pool/medium"
341
- },
342
- "pools": { // addressable as model "pool/<name>"
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" }
347
- }
348
- },
349
- "mode": "repair", // detect | repair (strict accepted, aliases detect)
350
- // Omit `destructiveTools` to get exactly this default list. Names are matched EXACTLY.
351
- "repair": { "maxAttempts": 2, "destructiveTools": ["Bash","BashOutput","Write","Edit","MultiEdit","NotebookEdit","rm","delete","delete_file","remove","overwrite","drop","reset","force_push"] },
352
- "log": { "level": "metadata", "file": null }, // metadata-only; NEVER logs headers/bodies
353
- // Stop `llm-relay onboard` nudging you about providers you have decided not to configure.
354
- // Nudge suppression ONLY — see below.
355
- "leave_me_alone": ["openai", "anthropic"]
356
- }
21
+ llm-relay keys # are the credentials good?
22
+ llm-relay pools --probe # does every configured model actually answer?
357
23
  ```
358
24
 
359
- **Routing (lifted from free-claude-code's proven scheme split on the first `/` only):**
360
- 1. **Pool** a request `model` of `pool/<name>` expands to that pool's whole candidate list,
361
- which is then fitness-ranked and failed over. Use this to ask for *the best available*
362
- model instead of naming one. An unknown pool is a **400, never a silent fallback** to the
363
- default — a typo must not quietly succeed against a different model.
364
- 2. **Namespaced** — a request `model` of `provider/rest` where `provider` is a configured
365
- provider routes there directly; the entire tail (nested slashes, `:free` suffixes) is the
366
- backend model, verbatim. E.g. `nim/openai/gpt-oss-120b`, `openrouter/openai/gpt-5.2-codex`.
367
- Deliberately verbatim: a pinned spec is never re-ranked.
368
- 3. **Tier** — otherwise the Claude model id is substring-matched against `routing.tiers`
369
- (`opus`/`sonnet`/`haiku`/`fable`). This also fixes Claude's haiku-class side-calls, which
370
- would otherwise blindly hit one model and 404.
371
- 4. **Default** — anything unrecognized falls to `routing.default`.
372
-
373
- `pool/<name>` exists because some callers can only express a **single model string** — notably
374
- Claude Code subagent frontmatter (`model:`), which accepts a full model id but not a candidate
375
- list. A pool is the indirection that gives those callers ranking and failover. `pool` is a
376
- reserved provider name; configuring a provider called `pool` fails at load.
377
-
378
- Pools can be static arrays, or automatic free-model pools:
379
-
380
- ```jsonc
381
- "medium": {
382
- "preferred": [],
383
- "include": "free",
384
- "effort": "medium"
385
- }
386
- ```
387
-
388
- The configured prefix remains first in exactly the written order. The relay then appends every
389
- model discovered from a `tierType: "free"` provider (excluding a model when its catalog publishes
390
- a positive price), plus zero-priced or explicitly free-labelled models from `tierType: "mixed"`
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.
398
- Catalog refreshes re-materialize the pool automatically; adding new free models never requires a
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.
403
-
404
- **What failover actually does** (both `/v1/messages` and `/v1/chat/completions`):
405
-
406
- - **429 / 5xx / 400 / 404** → the candidate is recorded as a breaker failure and the next one is
407
- tried. A `Retry-After` sets that candidate's cooldown for exactly as long as the provider asked.
408
- - **401 / 403** → the next candidate is tried, but the fault is recorded on its own axis rather
409
- than as ill health, so `llm-relay candidates` shows it as `AUTH 401` instead of hiding it. It
410
- expires after 5 minutes, so a rotated key recovers with no restart.
411
- - **A genuine client 4xx** (413, 422, …) → returned as-is. Every other candidate would reject it
412
- identically.
413
- - **Every candidate failed** → the last real upstream error, not a synthesized one.
414
-
415
- Responses carry **`x-llm-relay-served-by`**: the deployment that served, or on an error every
416
- deployment that was tried, in order.
417
-
418
- ⚠ **A pool routes to fewer members than it lists** when some declare an `authEnv` that is unset —
419
- those are dropped before ranking, so a 14-member pool can resolve to 7 and the config's *tenth*
420
- entry can legitimately be the one that answers. `llm-relay candidates` reports the count.
421
- Background: [docs/pool-failover.md](docs/pool-failover.md).
422
-
423
- ### Quieting the onboarding nudge (`leave_me_alone`)
424
-
425
- `llm-relay onboard` walks every known provider and prompts for the keys you are missing. For a
426
- provider you have deliberately decided not to configure, that prompt is permanent noise. List it
427
- in `leave_me_alone` and onboarding stops mentioning it.
25
+ New here? [docs/QUICKSTART.md](docs/QUICKSTART.md) is a staged setup guide you can hand
26
+ straight to an AI assistant ("set this up for me"). It also covers keeping the relay running
27
+ at login.
428
28
 
429
- ```jsonc
430
- "leave_me_alone": ["openai", "anthropic", "some-provider-you-never-set-up"]
431
- ```
432
-
433
- Two deliberate properties:
434
-
435
- - **A name matching no configured provider is legal** — no error, no warning. The list is the
436
- *negative space*: the providers worth suppressing are exactly the ones that are not in your
437
- `providers{}` block, and most are only ever preset names. Validating against the known set would
438
- reject the main use case. (The value's *shape* is still checked loudly — a bare string where a
439
- list belongs is a mistake with no plausible reading.)
440
- - **It suppresses a nudge, it does not hide state.** A suppressed provider still appears in
441
- `llm-relay keys`, in `/registry`, in telemetry and in `llm-relay candidates`, and still routes
442
- normally. Those are the surfaces you go to when something is wrong; a provider that vanished
443
- from them would be undebuggable.
444
-
445
- Matching is case- and whitespace-insensitive, and is against the provider *name* only — never its
446
- `authEnv` or display name — so one entry can never silence a provider you did not name.
447
-
448
- ### Destructive-tool refusal (`repair.destructiveTools`)
449
-
450
- A repaired tool call may run under `--dangerously-skip-permissions`, so llm-relay refuses to emit
451
- one that names a destructive tool — it never guesses arguments for it. Two things about the list
452
- are worth knowing before you configure it:
453
-
454
- - **Matching is exact on the tool name, case-insensitively** — not substring. A pattern ending in
455
- `*` is an opt-in prefix form (`"git_*"` covers `git_push` and `git_reset_hard` but not
456
- `gitlab_read`); a bare `"*"` matches nothing.
457
- - **The default list leads with the harness's own write/execute tools** — `Bash`, `BashOutput`,
458
- `Write`, `Edit`, `MultiEdit`, `NotebookEdit` — then the conventional MCP-style names (`rm`,
459
- `delete`, `delete_file`, `remove`, `overwrite`, `drop`, `reset`, `force_push`).
460
-
461
- Both of those changed, and both are visible in behaviour. Refusal used to be substring matching
462
- over fragments like `rm`/`delete`/`push`, which was wrong in **both** directions at once: none of
463
- those fragments occur in `Bash`/`Write`/`Edit`, so the tools that can actually destroy something
464
- were never guarded — while `push` matched `PushNotification` and `reset` matched `ResetZoom`,
465
- refusing safe calls. So:
466
-
467
- - a malformed `Bash`/`Write`/`Edit`/`MultiEdit`/`NotebookEdit`/`BashOutput` call that used to be
468
- repaired is now **refused** (logged as `repair: "refused_destructive"`; the request fails clean
469
- instead of emitting a call the model did not correctly produce);
470
- - a call named `PushNotification`, `ResetZoom` or `ForceRefresh` is now **permitted**.
471
-
472
- There is no built-in list inside the proxy: an empty `repair.destructiveTools` refuses nothing, so
473
- coverage is always traceable to your config.
474
-
475
- ### Granular offload (`routing.offload` + `routing.subagents`)
476
-
477
- Offload rules are keyed by the originating harness. Claude requests use the `/v1/messages` front
478
- door; Codex requests use `/v1/responses`. Each rule is independent and chooses whether it applies
479
- to marked subagents only (the current behavior) or to the whole conversation:
480
-
481
- ```jsonc
482
- "routing": {
483
- "tiers": { "opus": "anthropic", "sonnet": "anthropic", "haiku": "anthropic", "fable": "anthropic" },
484
- "subagents": {
485
- "opus": "pool/xhigh", "fable": "pool/xhigh",
486
- "sonnet": "pool/high", "haiku": "pool/medium", "default": "pool/medium"
487
- },
488
- "offload": {
489
- "claude": { "enabled": true, "scope": "subagents", "freeOnly": true },
490
- "codex": { "enabled": false, "scope": "all" }
491
- }
492
- }
493
- ```
29
+ ## What you get
494
30
 
495
- `scope: "subagents"` preserves the existing topology. `scope: "all"` also applies the same
496
- `routing.subagents` tier/default map to the client's main conversation, which is useful when a
497
- Claude or Codex quota is exhausted. Rules may use any future client name; an explicit `default`
498
- rule is the opt-in catch-all for otherwise unnamed front doors. All rules are off by default.
31
+ - **Pools with failover** `model: "pool/medium"` expands to a ranked candidate list; 429s
32
+ and outages cascade to the next member. Free-model pools update themselves from live catalogs.
33
+ - **Passthrough** Claude traffic keeps your own credentials and reaches real Anthropic
34
+ untouched, while `pool/*` requests go elsewhere. One proxy, both behaviours.
35
+ - **Opt-in offload** — route Claude/Codex subagents (or whole conversations) to free
36
+ providers. `llm-relay candidates` compares targets; a `freeOnly` guard ensures rerouted
37
+ traffic never spends money.
38
+ - **Tool-call repair** — malformed tool calls are corrected and re-validated; destructive
39
+ tool calls are refused, never fabricated; unrepairable calls fail clean.
40
+ - **Both API fronts** — Anthropic `/v1/messages` plus OpenAI `/v1/chat/completions` and
41
+ `/v1/responses`, translated in either direction, streaming included.
42
+ - **Honest metadata** — per-deployment limits and prices with provenance, capability scores
43
+ synced from four leaderboards, metadata-only logging, loopback-only binding.
499
44
 
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:
509
-
510
- ```bash
511
- llm-relay offload status
512
- llm-relay offload <harness> <on|off> [--scope <scope>]
513
- ```
514
-
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;
520
- `POST /offload` accepts `{"client":"claude","enabled":true,"scope":"all"}`. Changes are
521
- persisted and take effect on the next request.
522
-
523
- Claude Code stamps `cc_is_subagent=true` into the `system` block of subagent requests (built-in
524
- agents like Explore included — verified on the wire, Claude Code 2.1.220). Local Codex stamps
525
- `x-codex-turn-metadata: {"request_kind":"subagent",...}` on child-agent turns. A subagents-only
526
- rule requires that marker; an all-scope rule also accepts ordinary main-conversation requests.
527
- An explicit `@relay:` directive remains a subagent-only per-call opt-in, even when a client has an
528
- all-scope rule, so text in a human conversation cannot self-reroute it.
529
-
530
- A dispatcher chooses a destination with the Agent tool's `model` parameter (`sonnet|opus|haiku|fable`)
531
- or, when it does not choose, `subagents.default` applies and the pool's ranking picks the model.
532
-
533
- **To pin an exact model for one call**, put a directive on its own line in the subagent's prompt:
534
-
535
- ```
536
- @relay: nim/z-ai/glm-5.2
537
- Trace every caller of parseConfig and report the file:line of each.
538
- ```
539
-
540
- `<spec>` is any normal spec (`pool/<name>` or `<provider>/<model>`). The line is **stripped before
541
- the request is forwarded**, so the model never sees it. This works **whether or not the switch is
542
- on** — it is the per-call opt-in, so you can offload one dispatch without offloading everything.
543
-
544
- ⚠ **The directive is read only from the last text block of `messages[0]`** — the dispatcher's
545
- authored prompt. Block 0 is Claude Code's injected `<system-reminder>` (your CLAUDE.md, the date,
546
- …), and later messages carry tool results, i.e. file contents. Reading either would let any file a
547
- subagent happens to read redirect its own routing. Both cases are covered by tests.
548
-
549
- Precedence for a marked subagent request: `@relay:` directive → `subagents[<tier>]` →
550
- `subagents.default` → normal routing. The map applies only when that request's client rule is
551
- enabled and its scope admits the request; omit `routing.subagents` entirely and nothing changes.
552
-
553
- #### Local Codex setup
554
-
555
- For the intended split, keep the parent Codex session on its normal provider and define a named
556
- child agent whose own Responses requests use llm-relay. A global `llm-relay` install creates the
557
- provider and agents below automatically. If npm lifecycle scripts were blocked, run the bundled
558
- installer manually with `--force`, or create the files yourself as follows.
559
-
560
- ```toml
561
- [model_providers.llm-relay]
562
- name = "llm-relay"
563
- base_url = "http://127.0.0.1:8791/v1"
564
- wire_api = "responses"
565
- requires_openai_auth = true
566
- ```
567
-
568
- Then create `~/.codex/agents/relay_coding.toml`:
569
-
570
- ```toml
571
- name = "relay_coding"
572
- description = "Read-only coding child routed through llm-relay."
573
- developer_instructions = "Work read-only. Return a concise result to the parent and do not modify files."
574
-
575
- model_provider = "llm-relay"
576
- model = "pool/medium"
577
- model_reasoning_effort = "medium"
578
- ```
579
-
580
- To make an unqualified child dispatch use the relay automatically, override Codex's built-in
581
- `default` agent with `~/.codex/agents/default.toml`:
582
-
583
- ```toml
584
- name = "default"
585
- description = "General-purpose read-only child routed through llm-relay."
586
- developer_instructions = "Work read-only. Return a concise result to the parent and do not modify files."
587
-
588
- model_provider = "llm-relay"
589
- model = "pool/medium"
590
- model_reasoning_effort = "medium"
591
- ```
592
-
593
- With that override, a normal “use a subagent” request keeps the parent native while the generic child
594
- goes through `pool/medium`; named agents can still select a different pool explicitly.
595
-
596
- Run Codex normally, without the `llm-relay` profile. Ask the parent to use exactly one subagent of
597
- type `relay_coding`; Codex keeps the parent on its normal provider and starts the child through the
598
- relay. The relay pool then chooses the configured provider and can fail over normally.
599
-
600
- Enable only Codex child offload in `~/.llm-relay/config.json`:
601
-
602
- ```bash
603
- llm-relay offload <harness> on --scope <scope>
604
- ```
605
-
606
- For Codex, use `harness=codex` with `scope=subagents`; use `scope=all` to include the parent
607
- conversation. Claude's rule is unaffected.
608
-
609
- The `llm-relay` profile remains available as an explicit all-relay mode, but it routes the parent
610
- through the relay too and is not the split setup described above. The automatic
611
- `x-codex-turn-metadata` marker is still recognized when a Codex client sends it; using a relay pool
612
- as the named child model keeps the split setup reliable even when a custom-agent request omits that
613
- private marker.
614
-
615
- This applies to local Codex clients that can reach `127.0.0.1`. Hosted ChatGPT/Cloud tasks cannot
616
- reach a loopback relay, and the relay cannot spend a ChatGPT subscription on behalf of an upstream
617
- request; those remain separate CLI/client-bound dispatch lanes.
618
-
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
622
- supported for configurations that do not need tier-specific CLI models.
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
-
629
- ### Choosing where to offload (`llm-relay candidates`)
630
-
631
- ```
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~
636
- ```
637
-
638
- Every offload target with its dimensions side by side: capability from each leaderboard separately,
639
- live behaviour (verdict, avg/p95 latency, jitter, uptime), availability and cost right now (quota,
640
- circuit-breaker state, price per million tokens, whether the provider still lists the model), and
641
- traffic actually observed through the proxy.
642
-
643
- **Capability comes from `npm run sync:tiers`**, which merges four sources into
644
- `docs/tier-data.json` (~770 models) — see [docs/capability-sources.md](docs/capability-sources.md):
645
-
646
- | Source | Contributes |
647
- |---|---|
648
- | OpenRouter | Artificial Analysis intelligence / coding / **agentic** indices, Design Arena Elo, context length, pricing, tool support — and the only source whose ids match routing specs exactly |
649
- | BFCL | tool-call accuracy, multi-turn, irrelevance detection |
650
- | LMArena | general preference rating + rank |
651
- | Aider | polyglot edit benchmark + edit-format compliance |
652
-
653
- They **disagree** — the agentic index puts deepseek above kimi while the coding index puts kimi
654
- above deepseek — which is exactly why each keeps its own column, and why a blank cell means *not
655
- measured*, never *bad*.
656
-
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.
685
-
686
- **Limits and prices are per-(provider, model), and labelled.** The same model id on two providers is
687
- two deployments — different context ceilings, different output caps, and possibly free on one and
688
- metered on the other. Where a provider publishes its own figures (Groq, Mistral, OpenRouter) those
689
- are used and shown unmarked; where it publishes none (NIM returns only `id`/`object`/`created`/
690
- `owned_by`) the table falls back to another provider's figure for the same id and marks it `~`.
691
- If nobody publishes one, the cell is blank — llm-relay does not guess a limit or a price.
692
-
693
- That honesty is load-bearing: the **context guardrail only fires against a limit the serving
694
- provider published**. If the limit is unknown the request goes upstream and the backend answers with
695
- its own error, rather than llm-relay rejecting it against a number it made up.
696
-
697
- `GET /candidates` returns the full JSON (the table shows a subset). The CLI prefers a running proxy
698
- so the live columns come from warm ping history rather than a cold start.
699
-
700
- 📄 Full design, the wire evidence behind it, and **how to re-verify the marker after a Claude Code
701
- upgrade**: [docs/subagent-routing.md](docs/subagent-routing.md).
702
-
703
- Each provider is `kind:"openai"` (translated Anthropic↔OpenAI via llm-bridge) or
704
- `kind:"anthropic"` (forwarded as-is). In `repair` mode an openai target reshapes on itself;
705
- an anthropic provider has no fixed model id to reshape on, so it needs an explicit top-level
706
- `reshaper` block.
707
-
708
- **Prefer the pool form — do not pin one reshaper model:**
709
-
710
- ```jsonc
711
- "reshaper": { "pool": "medium" } // ranked candidates, tried in order
712
- ```
713
-
714
- A pinned `{ "base": …, "model": … }` still works, but if the provider stops serving that exact id
715
- your repair path dies with it and nothing says so. `{ "pool": … }` expands to the pool's ranked
716
- candidates and fails over on transport errors. A **refusal** is never retried on the next
717
- candidate — a reshaper declining to guess is a real judgement, and retrying it elsewhere is
718
- shopping for a more compliant answer, which is how a fabricated tool call gets through.
719
-
720
- If **every** candidate fails at the transport level, that is a total outage, not a judgement: the
721
- turn fails clean and is logged `repair: "failed"` (nothing was reachable), never `"refused"` (a
722
- model declined). The two are kept distinguishable in the log because they call for opposite
723
- responses — one is an infrastructure problem, the other is the safety boundary working.
724
-
725
- Anthropic-kind entries in the pool are skipped (they cannot reshape); a pool with no usable
726
- target is a loud startup error, never a silently absent reshaper.
727
-
728
- ### Repointing without editing the file
729
-
730
- Config strings may reference env vars as `${NAME}` (unset → loud startup error). Or override
731
- routing from the CLI (wins over the file):
732
-
733
- ```bash
734
- node dist/cli.js --config config.json --default openrouter/openai/gpt-5.2-codex --mode repair
735
- ```
736
-
737
- `llm-relay --help` lists every override.
738
-
739
- ### Model discovery (dynamic + cached)
740
-
741
- Model ids are **discovered live** from each provider's `/models` endpoint — never
742
- hand-maintained. The catalog is cached in `~/.llm-relay/models-cache.json`
743
- (10-min TTL, fail-open: a fetch failure serves the last-known list).
744
-
745
- ```bash
746
- llm-relay models # list live models for every provider
747
- llm-relay models --provider nim # one provider
748
- llm-relay models --provider nim --refresh # force a re-fetch
749
- ```
750
-
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.
755
-
756
- > Provider notes: **Groq** returns `403 "check your network settings"` from some
757
- > IPs/regions (a network-side block, not a key issue) — it works once your network
758
- > allows it. **Mistral** needs `MISTRAL_API_KEY` set in your environment.
759
-
760
- ### Discovery endpoint (`GET /registry`) — for a dispatcher
761
-
762
- For a caller that does its own selection (an external dispatcher weighing
763
- quota / rate limits / token budget), `GET http://127.0.0.1:8791/registry` returns one
764
- coherent JSON view:
765
-
766
- - **providers** — each with `base`, `kind`, `has_key` (auth env set?), `reachable`
767
- (did the live `/models` catalog return anything?), and `models[]` where every model
768
- carries a best-effort `capability` (raw BFCL + Arena scores, **never collapsed** to
769
- tiers — `null` when no confident leaderboard match).
770
- - **routing** — the current default + tier map.
771
- - **capability_source** — the full raw leaderboard dataset, so a consumer can run a
772
- finer id→score join than the built-in best-effort one.
773
-
774
- The consumer then dispatches by pointing its OpenAI-compatible pool at :8791 and
775
- setting each packet's model to a **namespaced** `provider/model` (it picked the exact
776
- backend). llm-relay exposes an **OpenAI-compatible front** for exactly this —
777
- `POST /v1/chat/completions` (and `/chat/completions`) plus `POST /v1/responses`: the
778
- request's `model` is routed by namespace/tier. OpenAI-compatible targets receive the
779
- backend model id directly; Anthropic targets receive a translated `/v1/messages` request
780
- and their response is translated back to the caller's OpenAI envelope. Responses streaming,
781
- tool calls and usage are supported. The Anthropic `/v1/messages` front with tool-call repair
782
- stays available in parallel for a Claude-harness client. Meanwhile a plain `claude` client
783
- that sends `claude-sonnet-…` still gets the **dumb tier/default routing** — both coexist,
784
- no mode switch. So the tier map stays the default, and dispatcher-style usage is just
785
- "send namespaced ids + read `/registry`".
786
-
787
- OpenAI-native clients can point their base URL at `http://127.0.0.1:8791/v1` and use a
788
- namespaced model such as `anthropic/claude-sonnet-4-20250514` or `pool/medium`. Codex uses
789
- `/v1/responses`; other IDEs commonly use `/v1/chat/completions`. Configure the Anthropic
790
- provider with `kind: "anthropic"` and `authEnv: "ANTHROPIC_API_KEY"` when the relay should
791
- use its own key, or omit `authEnv` for an intentional caller-credential passthrough.
792
-
793
- ### Model tiers from leaderboards (never a hand-maintained table)
794
-
795
- `npm run sync:tiers` snapshots capability rankings into `docs/tier-data.json` from **four** sources
796
- — **OpenRouter** (Artificial Analysis intelligence / coding / agentic indices, Design Arena Elo,
797
- context length, pricing, tool support), **BFCL** (Berkeley Function-Calling Leaderboard — tool-use
798
- accuracy, the primary signal for a tool-call proxy, incl. its Irrelevance-Detection metric = the
799
- malformed-call proxy), **LMArena** (general capability) and **Aider** (polyglot edit benchmark) —
800
- and prints the top tool-callers so you can pick tier targets from real data. Every source is
801
- synced-not-forked, and each is independently failable so one dead endpoint does not cost the
802
- others; a **schema change inside** a source still fails the sync loudly, because a renamed column
803
- is corruption rather than absence. Zero working sources is fatal.
804
-
805
- The reshaper also takes `"kind": "openai"` — so `repair` mode can run entirely on an OpenAI-compatible provider (e.g. NIM) with no Anthropic key. The reshaper is asked only for the **corrected arguments per tool-call id** (not the full message envelope), which is far more reliable on weaker models; the proxy reconstructs the message and re-validates it.
806
-
807
- ### Live run
808
-
809
- ```bash
810
- node scripts/nim-front.mjs # runs the compiled proxy fronting live NIM end-to-end (uses NVIDIA_API_KEY)
811
- ```
812
- Then point a `claude` CLI at it (see "Install & run" above) and inspect the log to see which calls trip the validator on your traffic.
813
-
814
- ## What it logs (per request, metadata only)
815
-
816
- `{ ts, path, servedProvider, servedModel, hadTools, streamed, backendStatus, validated: pass|fail|uncheckable|skipped, toolUseCount, uncheckableCount, errorKinds[], repair: none|fixed|failed|refused|refused_destructive, latencyMs }`
817
-
818
- That list is an **allow-list applied at the sink**, not a convention: the writer projects every record through it, so a caller that hands over a wider object cannot leak a header, a body or an error string carrying a key — and a new field starts being logged only when someone deliberately adds it to the list. `path` is passed through `logSafePath()`, which keeps the route and each query parameter's *name* and replaces its value with the value's length, because a `?task=` value is user prose, not metadata. A failed log write is swallowed to stderr: a full disk is a logging problem, never a request failure.
819
-
820
- `uncheckable` = a declared tool with no `input_schema` (built-in `bash`/`text_editor`/…) or a schema that wouldn't compile — surfaced distinctly so an unvalidatable call is never miscounted as a clean pass.
821
-
822
- ⚠ `servedProvider`/`servedModel` are the deployment that actually served the request — draw "which model trips the validator" conclusions from them. The model the **client asked for** is deliberately not recorded: there used to be a `backendModel` field carrying it, and for a tier or pool spec it is routinely not the model that answered, so every conclusion drawn from this dataset was attributed to whatever id the client happened to send. `null` in the served fields means genuinely nothing served the turn (a guardrail rejection, a routing error, an admin endpoint answered locally).
823
-
824
- This is the dataset for deciding which backend models are *format-broken* (reshapeable later) vs pass cleanly. Run in `detect` first, measure, then decide on repair.
825
-
826
- ### Trip-rate dataset
827
-
828
- `node scripts/nim-trip-rate.mjs` probes a list of backend models across difficulty-graded tool schemas (× N trials), runs each call through the real validator, and repairs the failures — producing a per-model **trip rate** (share of tool calls that fail schema validation) and **repair-fix rate**. Latest live NIM run: [`docs/nim-trip-rate.md`](docs/nim-trip-rate.md) (raw records in `docs/nim-trip-rate.jsonl`). The sharp result: even strong Llama-3.1 models emit `days:"5"` (string) against an `integer` schema on every trial — and the proxy repairs it every time; the flat/enum/nested schemas pass clean.
829
-
830
- ## Composing with headroom (optional)
831
-
832
- [headroom](../headroom) is a separate loopback proxy that **optimizes/compresses**
833
- context on the way to the model. Both it and llm-relay are transparent
834
- Anthropic-Messages proxies, so they chain — but only in one order, because
835
- llm-relay's backend speaks OpenAI/NIM while headroom only forwards Anthropic:
836
-
837
- ```
838
- claude → headroom (:8787, context optimization, OUTER) → llm-relay (:8791, validate/repair + translate, INNER) → NIM/…
839
- ```
840
-
841
- llm-relay must be **innermost**. To chain them, point headroom's upstream at
842
- llm-relay — headroom exposes this as a launch flag, so its own code is untouched:
843
-
844
- ```bash
845
- ANTHROPIC_TARGET_API_URL=http://127.0.0.1:8791 # headroom → llm-relay
846
- ```
847
-
848
- That env var repoints *all* of headroom's Anthropic traffic — including your real
849
- subscription sessions — at llm-relay. **That is fine, and you do not need a second
850
- headroom instance for it**, provided you give llm-relay an `anthropic` passthrough
851
- provider and point every tier at it:
852
-
853
- ```jsonc
854
- "providers": { "anthropic": { "base": "https://api.anthropic.com", "kind": "anthropic" } },
855
- "routing": {
856
- "default": "anthropic",
857
- "tiers": { "opus": "anthropic", "sonnet": "anthropic", "haiku": "anthropic", "fable": "anthropic" },
858
- "pools": { "medium": ["nim/z-ai/glm-5.2", "nim/deepseek-ai/deepseek-v4-pro"] }
859
- }
860
- ```
861
-
862
- A passthrough provider declares **no `authEnv`**, so llm-relay forwards the caller's own
863
- credentials byte-for-byte (`authorization`/`x-api-key` *and* `anthropic-beta`). Every Claude
864
- model you pick therefore reaches real Anthropic untouched, while anything addressed as
865
- `pool/<name>` goes to another provider. One instance, both behaviours.
866
-
867
- ⚠ **Do not route Codex through headroom.** headroom has a single OpenAI upstream covering both
868
- `/v1/chat/completions` and `/v1/responses`; when using headroom, Codex must reach api.openai.com.
869
- Codex can instead point directly at llm-relay, whose OpenAI front supports `/v1/responses`.
870
-
871
- Note the `claude-proxied` wrappers set `ANTHROPIC_BASE_URL` straight to :8791 with a dummy
872
- token and an isolated `CLAUDE_CONFIG_DIR`, so **they bypass headroom entirely** — they are for
873
- testing this proxy against a non-Anthropic backend, not for subscription use.
874
-
875
- **Is it worth it?** headroom's headline win is $/token savings vs *paid* Anthropic —
876
- **moot on the free NIM pool**. What still pays off through the chain: context
877
- **compression to fit a smaller backend context window** + lower latency, plus
878
- headroom's backend-agnostic memory/learn layer. So stack it for context-fit, not cost.
879
-
880
- ## Design
881
-
882
- Consumers (an external dispatcher, plain `claude` CLI) point `ANTHROPIC_BASE_URL` at this proxy; it validates one backend per request. Target *selection* / token-prediction is a separate concern (the router/auditor), deliberately not here. For architecture, invariants, and the script inventory, see [CLAUDE.md](CLAUDE.md).
883
-
884
- ## Dev
885
-
886
- ```bash
887
- npm run check # both typechecks + suite — the one gate, and exactly what CI runs
888
- npm run typecheck # tsc --noEmit, src/ only (tsconfig.json — it drives dist/)
889
- npm run typecheck:test # tsc over the suite (tsconfig.test.json)
890
- npm test # vitest (validator, SSE reconstruction, e2e transparency+detection)
891
- npm run build # tsc -> dist/ (scripts/*.mjs read dist/, so rebuild before running them)
892
- ```
45
+ ## Learn more
893
46
 
894
- `.github/workflows/ci.yml` runs `npm run check` on every push to `main` and every pull request.
47
+ - [docs/reference.md](docs/reference.md) full reference: config, routing, pools, offload,
48
+ repair, CLI, endpoints, and every caveat.
49
+ - [docs/subagent-routing.md](docs/subagent-routing.md) — offload design and wire evidence.
50
+ - [docs/pool-failover.md](docs/pool-failover.md) — how failover and health tracking behave.
51
+ - [docs/capability-sources.md](docs/capability-sources.md) — where capability scores come from.
52
+ - [docs/project-goals.md](docs/project-goals.md) — what this project is and is not.
895
53
 
896
- `test/` needs its own tsconfig because `tsconfig.json` compiles `src/` only and vitest transpiles
897
- tests without type-checking them. Until `tsconfig.test.json` existed nothing checked them at all,
898
- so a `@ts-expect-error` in a test file was never evaluated and proved nothing — treat any
899
- pre-existing one with suspicion, and prefer a runtime assertion when the point is that a surface
900
- does not exist.
54
+ `llm-relay help` lists every command. [CLAUDE.md](CLAUDE.md) maps the source for contributors.