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 +37 -883
- package/dist/cli.js +22 -38
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +22 -1
- package/dist/config.js +52 -3
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,900 +1,54 @@
|
|
|
1
1
|
# llm-relay
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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.
|