llm-relay 0.84.0 → 0.84.1

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
@@ -65,10 +65,17 @@ at login.
65
65
  ## Learn more
66
66
 
67
67
  - [docs/reference.md](docs/reference.md) — full reference: config, routing, pools, offload,
68
- repair, CLI, endpoints, and every caveat.
68
+ repair, CLI, endpoints, and every caveat. It opens with a table of contents.
69
+ - [docs/architecture.md](docs/architecture.md) — a map of the code, for a person who wants to
70
+ change it.
71
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — how to set up, how to prove a change, and how to report a
72
+ problem without leaking a credential.
69
73
  - [docs/subagent-routing.md](docs/subagent-routing.md) — offload design and wire evidence.
70
74
  - [docs/pool-failover.md](docs/pool-failover.md) — how failover and health tracking behave.
71
75
  - [docs/capability-sources.md](docs/capability-sources.md) — where capability scores come from.
72
76
  - [docs/project-goals.md](docs/project-goals.md) — what this project is and is not.
73
77
 
74
- `llm-relay help` lists the main commands. [CLAUDE.md](CLAUDE.md) maps the source for contributors.
78
+ `llm-relay help` lists the main commands. To contribute, start at
79
+ [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/architecture.md](docs/architecture.md).
80
+ [CLAUDE.md](CLAUDE.md) holds the same source map in full detail, written for an AI coding
81
+ assistant.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Availability ladders — spec §5.1-5.3 of docs/quota-metering-spec-2026-08-16.md.
2
+ * Availability ladders — spec §5.1-5.3 of docs/history/quota-metering-spec-2026-08-16.md.
3
3
  *
4
4
  * `remaining` and `resetsAt` are resolved per (scope, axis, period) through fixed rungs, and
5
5
  * staleness is handled HERE, at read time, as a pure function of the caller's clock — never by a
package/dist/backend.d.ts CHANGED
@@ -29,7 +29,7 @@ export declare class DialectUnparseableError extends Error {
29
29
  * fine. It is a config decision, so it is carried as a LOCAL failure — the walk must not fail over
30
30
  * (re-asking N models to produce the same refused action) and the deployment must not be charged,
31
31
  * the same rule under which a hard cap "never registers on the breaker — it is config, not
32
- * health". See docs/dialect-rescue-destructive-refusal-2026-08-24.md.
32
+ * health". See docs/history/dialect-rescue-destructive-refusal-2026-08-24.md.
33
33
  */
34
34
  export declare class DialectDestructiveError extends Error {
35
35
  readonly dialect: string;
@@ -186,7 +186,7 @@ export declare const UNMEASURED_STABILITY = 50;
186
186
  * ladder: a `Retry-After`, a 429 escalation and a 402 all still win where they applied before.
187
187
  *
188
188
  * Pure, so it is pinned directly rather than through the breaker's state machine.
189
- * Evidence: `docs/latency-demotion-regression-2026-08-30.md` §3.
189
+ * Evidence: `docs/history/latency-demotion-regression-2026-08-30.md` §3.
190
190
  */
191
191
  export declare function failureCooldown(elapsedMs: number): {
192
192
  ms: number;
package/dist/cli.d.ts CHANGED
@@ -403,7 +403,7 @@ export declare function renderCommand(invoke: {
403
403
  * ⚠ `--probe` spawns lane commands as an explicit operator action — same precedent as
404
404
  * `pools --probe` sending real completions. The request path reads the cached manifest and never
405
405
  * spawns anything; outside it the only OTHER spawn site is the relay's background lane cadence
406
- * (owner decision 2026-08-29, docs/quota-reprobe-design-2026-08-29.md). Without `--probe` this
406
+ * (owner decision 2026-08-29, docs/history/quota-reprobe-design-2026-08-29.md). Without `--probe` this
407
407
  * just prints the cache.
408
408
  */
409
409
  export declare function runLanes(): Promise<void>;
@@ -98,7 +98,7 @@ export type ThoughtSignatureMode = "none" | "sentinel";
98
98
  * `thinking: {type:"disabled"}` — DeepSeek's thinking mode requires the prior turn's
99
99
  * `reasoning_content` to be replayed on a multi-turn conversation (HTTP 400 otherwise), and
100
100
  * this relay deliberately holds no store to round-trip it (see
101
- * docs/deepseek-responses-truncation-2026-09-09.md), so the default must not think.
101
+ * docs/history/deepseek-responses-truncation-2026-09-09.md), so the default must not think.
102
102
  */
103
103
  export type ReasoningMode = "none" | "deepseek";
104
104
  /**
@@ -126,7 +126,7 @@ export interface ProviderCompatConfig {
126
126
  *
127
127
  * OpenCode Zen's contributor SKUs — Muse Spark 1.3 included — answer HTTP 500 on
128
128
  * `/chat/completions` and on Zen's Anthropic-shaped `/messages`, and 200 only on `/responses`
129
- * (measured 2026-09-04, `docs/muse-spark-1.3-opencode-zen-2026-09-04.md` rows 3 and 6-8). A third
129
+ * (measured 2026-09-04, `docs/history/muse-spark-1.3-opencode-zen-2026-09-04.md` rows 3 and 6-8). A third
130
130
  * `Kind` value for this would touch roughly 55 `kind === "openai"` sites across 19 files (same
131
131
  * doc, §3 route B); this narrower option forks only the request/response builders `src/backend.ts`
132
132
  * selects on, leaving discovery, catalog and key-check paths unchanged. Declaring it on an
@@ -329,7 +329,7 @@ export interface Routing {
329
329
  latency?: LatencyDemotionConfig;
330
330
  /**
331
331
  * Hedged attempts (owner proposal + decisions 2026-08-30,
332
- * docs/hedged-attempts-design-2026-08-30.md §7). **Default ON**, and confined to deployments
332
+ * docs/history/hedged-attempts-design-2026-08-30.md §7). **Default ON**, and confined to deployments
333
333
  * `assessCost()` calls FREE.
334
334
  *
335
335
  * ⚠ This is the FIRST behaviour here that does not merely reorder — it DUPLICATES a request onto
@@ -366,7 +366,7 @@ export interface Routing {
366
366
  pacing?: PacingConfig;
367
367
  /**
368
368
  * Post-commit CRAWL abort (backlog item 18, built 2026-09-09 after
369
- * `docs/post-commit-stall-measurement-2026-09-09.md` measured that both Claude Code and Codex
369
+ * `docs/history/post-commit-stall-measurement-2026-09-09.md` measured that both Claude Code and Codex
370
370
  * retry a stream that goes bad after content has already arrived — Claude Code once, downgraded
371
371
  * to non-streaming; Codex up to five times, staying streaming). A silent stall after commit is
372
372
  * already caught by `withStallWatchdog` at `stallTimeoutMs`; this catches the case nothing else
@@ -381,7 +381,7 @@ export interface Routing {
381
381
  crawl?: CrawlWatchdogConfig;
382
382
  /**
383
383
  * Background lane re-probing (owner decision 2026-08-29,
384
- * docs/quota-reprobe-design-2026-08-29.md): keeping lane metadata fresh is the relay's own
384
+ * docs/history/quota-reprobe-design-2026-08-29.md): keeping lane metadata fresh is the relay's own
385
385
  * job, the way the ping loop already does for HTTP. **Default ON** — catalog probes are
386
386
  * metadata commands that spend no quota, and quota probes fire only for buckets carrying an
387
387
  * ACTIVE recorded death (an alive lane is re-tested by real use for free). Boolean shorthand
@@ -391,7 +391,7 @@ export interface Routing {
391
391
  laneProbe?: LaneProbeSettings;
392
392
  /**
393
393
  * The automatic dispatch lane WALK (owner request 2026-09-06,
394
- * docs/dispatch-lane-walk-design-2026-09-06.md). **Default ON.**
394
+ * docs/history/dispatch-lane-walk-design-2026-09-06.md). **Default ON.**
395
395
  *
396
396
  * Before it, `dispatch` ran ONE lane and reported a failure when that lane was slow; the calling
397
397
  * agent then picked the next lane by hand, which is the friction the owner reported. With it,
@@ -495,7 +495,7 @@ export interface LatencyDemotionConfig {
495
495
  /**
496
496
  * `routing.hedge` — start the NEXT candidate beside a slow in-flight attempt, instead of after it
497
497
  * (owner proposal 2026-08-30; the four decisions are in
498
- * `docs/hedged-attempts-design-2026-08-30.md` §7).
498
+ * `docs/history/hedged-attempts-design-2026-08-30.md` §7).
499
499
  *
500
500
  * **Default ON, free deployments only.** That is owner decision D1, taken against the
501
501
  * recommendation of off-by-default. `assessCost()` treats an UNKNOWN price as paid, so the rule is
@@ -876,7 +876,7 @@ export interface DispatchWalkSettings {
876
876
  * answer-mode call. Default 600000 (10 minutes).
877
877
  *
878
878
  * ⚠ Two floors because the two modes are two different populations
879
- * (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3): an answer-mode call is one HTTP round trip
879
+ * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §3): an answer-mode call is one HTTP round trip
880
880
  * that answers in seconds, while an agent-mode lane runs a whole tool loop for minutes. One floor
881
881
  * fitted to both stopped every real agent task on `free-pool` at 90 s, because a burst of short
882
882
  * answer-mode calls had set the lane's p80 to 39.5 s. Like `attemptMs` it is an operator budget,
@@ -977,7 +977,7 @@ export interface McpSettings {
977
977
  /**
978
978
  * Default `routing.mcp.blockingWaitMs` — 25 minutes.
979
979
  *
980
- * Measured and documented 2026-09-17 (`docs/mcp-host-timeouts-2026-09-17.md`): Claude Code's
980
+ * Measured and documented 2026-09-17 (`docs/history/mcp-host-timeouts-2026-09-17.md`): Claude Code's
981
981
  * wall-clock tool limit (`MCP_TOOL_TIMEOUT`) defaults to about 28 hours, and a 240 s call succeeded
982
982
  * headless with and without progress. Its stdio idle timeout is 30 minutes, and the documentation
983
983
  * says a progress notification resets it. The default stays under 30 minutes so the call survives
@@ -993,7 +993,7 @@ export declare const DEFAULT_MCP_BLOCKING_WAIT_MS = 1500000;
993
993
  * somewhere between 45 s and 100 s, and Codex's code-mode `exec` tool yields its script at 31.0 s
994
994
  * with empty output ("Script running with cell ID N / Wall time 31.0 seconds"). The 2026-09-10
995
995
  * transcript sweep counted 29 of 266 first Codex dispatch calls that lost their job id that way
996
- * while this default was 40 s (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §8). 25 s sits under
996
+ * while this default was 40 s (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §8). 25 s sits under
997
997
  * both. The tool description names the config key rather than this figure, so an operator override
998
998
  * never leaves the text stale.
999
999
  */
package/dist/config.d.ts CHANGED
@@ -48,7 +48,7 @@ export declare function resolveThoughtSignatureMode(p: {
48
48
  * labelled provider fact in `src/` only while config can override it. DeepSeek's own API states its
49
49
  * thinking/reasoning vocabulary (thinking ON by default; `thinking: {type:"disabled"}` /
50
50
  * `reasoning_effort: low|high|max`; first-party evidence in
51
- * docs/deepseek-responses-truncation-2026-09-09.md), so `api.deepseek.com` defaults to `"deepseek"`
51
+ * docs/history/deepseek-responses-truncation-2026-09-09.md), so `api.deepseek.com` defaults to `"deepseek"`
52
52
  * and every other host to `"none"`. An explicit `compat.reasoning` wins in BOTH directions —
53
53
  * `"none"` on deepseek, `"deepseek"` on anything else.
54
54
  */
@@ -29,7 +29,7 @@ export type DispatchLaneStatus = (typeof DISPATCH_LANE_STATUSES)[number];
29
29
  * ⚠ It keys the lane-stats window since 2026-09-10, because the two are different populations: a
30
30
  * burst of short answer-mode calls set `free-pool`'s p80 to 39.5 s, and that one shared window then
31
31
  * cut every agent-mode task on the same lane at the 90 s floor
32
- * (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3). One `as const` array, the type indexed from
32
+ * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §3). One `as const` array, the type indexed from
33
33
  * it and the parser validating against it — the closed-union rule.
34
34
  */
35
35
  export declare const DISPATCH_MODES: readonly ["agent", "answer"];
@@ -251,7 +251,7 @@ export interface DispatchLane {
251
251
  *
252
252
  * ⚠ It exists because an abandoned run leaves no duration sample, so a window fed only by runs
253
253
  * that finished inside the budget can never show that the lane needed longer — the floor lock
254
- * measured on 2026-09-10 (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3b). Each abandonment
254
+ * measured on 2026-09-10 (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §3b). Each abandonment
255
255
  * doubles the next budget, capped at `MAX_ATTEMPT_BUDGET_MS`; one success resets the count.
256
256
  */
257
257
  raisedBy?: number;
@@ -393,7 +393,7 @@ export interface DispatchOptions {
393
393
  * WHO is asking, when that changes what can run. `"mcp"` is the `llm-relay mcp` server: it runs
394
394
  * lanes itself and has no `Agent` tool, so a pass-through relay rung — one that forwards the
395
395
  * caller's own Anthropic credential — can never run there. It comes back `unreachable`, never as a
396
- * lane the walk tries and fails in 0 s (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §4).
396
+ * lane the walk tries and fails in 0 s (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §4).
397
397
  * Absent ⇒ exactly the behaviour before this existed.
398
398
  */
399
399
  requester?: "mcp";
@@ -407,7 +407,7 @@ export interface DispatchOptions {
407
407
  * instead of the ladder. Validated against the configured providers and pools: an unknown spec
408
408
  * yields no lane and a reason. `dispatch` could not name a model before 2026-09-10, so agents that
409
409
  * had to use DeepSeek wrote their own HTTP calls to the relay
410
- * (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §7).
410
+ * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §7).
411
411
  */
412
412
  model?: string;
413
413
  }
@@ -12,7 +12,7 @@
12
12
  * ⚠ **Hedging DUPLICATES, it does not reorder** — the first behaviour in this relay that does. The
13
13
  * `CLAUDE.md` invariant reads *"Acting on counts is optional, always announced, and may only
14
14
  * reorder"*, so the duplication is bounded three ways, all of them owner decisions recorded in
15
- * `docs/hedged-attempts-design-2026-08-30.md` §7: it is confined to deployments `assessCost()`
15
+ * `docs/history/hedged-attempts-design-2026-08-30.md` §7: it is confined to deployments `assessCost()`
16
16
  * calls FREE (D1), the loser is aborted the moment a winner commits, and the response announces it.
17
17
  *
18
18
  * WHY IT EXISTS. Measured 2026-08-30: `nim/deepseek-ai/deepseek-v4-flash-0731` hung on 43
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The background lane cadence — the relay's own re-probe loop for `cli` lanes.
3
3
  *
4
- * Owner decision 2026-08-29 (docs/quota-reprobe-design-2026-08-29.md): keeping lane metadata
4
+ * Owner decision 2026-08-29 (docs/history/quota-reprobe-design-2026-08-29.md): keeping lane metadata
5
5
  * fresh is the relay's job, the way `PingLoop` already keeps HTTP health fresh — not a host
6
6
  * scheduled task's, and never the nightly maintenance run's (that belongs to another repo).
7
7
  * This closes the property the backlog demanded: a recorded quota death either expires on a
@@ -5,7 +5,7 @@
5
5
  * validated it. Measured 2026-08-08: the ladder handed an agent
6
6
  * `agy --model claude-opus-5 --effort medium`, in which BOTH halves were wrong independently —
7
7
  * AGY serves no Claude 5 at all, and `--effort` is rejected outright for its Claude models. The
8
- * lane looked healthy and completed nothing. See docs/lane-discovery.md.
8
+ * lane looked healthy and completed nothing. See docs/history/lane-discovery.md.
9
9
  *
10
10
  * ⚠ A model the vendor does not serve is `not-servable` — an EXISTENCE fact, so the rung is
11
11
  * REMOVED, not demoted. `target-facts.ts` already draws that line: `allowance-exhausted` demotes
@@ -2,7 +2,7 @@ import type { Config } from "./config-types.js";
2
2
  /**
3
3
  * `llm-relay lanes --probe` — ask each `cli` lane's own tool what it serves.
4
4
  *
5
- * ⚠ The spawn boundary (amended by owner decision 2026-08-29, docs/quota-reprobe-design-2026-08-29.md):
5
+ * ⚠ The spawn boundary (amended by owner decision 2026-08-29, docs/history/quota-reprobe-design-2026-08-29.md):
6
6
  * the REQUEST PATH never runs a `cli` lane's command — a lane's quota is client-bound, it runs its
7
7
  * own tool loop, and it returns only final text, so a relay that shelled out mid-request could
8
8
  * never return the `tool_use` blocks an HTTP turn owes its caller. Outside the request path there
@@ -11,7 +11,7 @@ import type { Config } from "./config-types.js";
11
11
  * metadata polling is the relay's job, exactly as the ping loop already does for HTTP. Nothing
12
12
  * here is reachable from `handle()`, and the request path reads only the CACHED manifest.
13
13
  *
14
- * Discovery is not symmetric (docs/lane-discovery.md):
14
+ * Discovery is not symmetric (docs/history/lane-discovery.md):
15
15
  * codex — `codex debug models` returns JSON including per-model `supported_reasoning_levels`,
16
16
  * so both the id and the effort argument are validated with no API call spent.
17
17
  * agy — `agy models` returns `id<TAB>label` and states nothing about flags, so its argument
@@ -69,7 +69,7 @@
69
69
  * succeeding, inside a window every deployment passes through on its way to being measured.
70
70
  * ⚠ A request sample with NO token count therefore reaches NEITHER statistic. That is deliberate:
71
71
  * it is a generation of unknown length, so it is not normalisable and not what `p95Ms` describes.
72
- * Evidence: `docs/latency-demotion-regression-2026-08-30.md`.
72
+ * Evidence: `docs/history/latency-demotion-regression-2026-08-30.md`.
73
73
  *
74
74
  * ⚠ **NO breaker cooldown is registered, and that is the design, not an omission.** Quota demotion
75
75
  * can register one because its evidence STATES a `resetsAt`. Latency states no reset, and this
@@ -719,7 +719,7 @@ export interface CwdCheck {
719
719
  * is offered, not imposed; that is the operator's call to make in config, not this file's.
720
720
  *
721
721
  * ⚠ **The containment test resolves BOTH sides with `path.resolve` before comparing** (closed
722
- * 2026-09-03, docs/audit-findings-2026-09-03.md finding 1 / DR-002). Without it a literal `..`
722
+ * 2026-09-03, docs/history/audit-findings-2026-09-03.md finding 1 / DR-002). Without it a literal `..`
723
723
  * segment in `cwd` — a raw string a caller sends verbatim, never normalized — satisfied a bare
724
724
  * `startsWith` prefix test while `existsSync`/`statSync` above had already resolved `..` at the
725
725
  * OS level against the REAL, escaped directory: `allowedRoots: ["C:/allowed"]` admitted
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * ⚠ The known cost, stated so it is not rediscovered: hand-rolling means owning a spec that moves.
11
11
  * The mitigation is that this file implements only the stable core. Extensions (Tasks, Apps,
12
- * sampling) are deliberately absent — see `docs/mcp-dispatch-prior-art-2026-08-30.md` §3.1, which
12
+ * sampling) are deliberately absent — see `docs/history/mcp-dispatch-prior-art-2026-08-30.md` §3.1, which
13
13
  * measured that no client ships Tasks support today.
14
14
  *
15
15
  * TRANSPORT. MCP stdio framing is one JSON message per line. A message therefore must not contain
@@ -22,7 +22,7 @@ export type ReadOnlyVerdict = {
22
22
  * Containment is tested on RESOLVED paths with a separator boundary, never a bare `startsWith`:
23
23
  * `C:/caller/tree-other` shares a prefix with `C:/caller/tree` and is not inside it, and a literal
24
24
  * `..` segment resolves at the OS level before the comparison — the `checkCwd` defect closed
25
- * 2026-09-03 (docs/audit-findings-2026-09-03.md finding 1 / DR-002), applied here in the direction
25
+ * 2026-09-03 (docs/history/audit-findings-2026-09-03.md finding 1 / DR-002), applied here in the direction
26
26
  * where getting it wrong would WRONGLY PERMIT a mutation.
27
27
  *
28
28
  * Answer mode is always allowed: `startLane` skips the cwd/spawn path entirely for a `relay` rung
@@ -175,7 +175,7 @@ export declare const LANE_LADDER_PARTIAL_ADVICE: string;
175
175
  * ⚠ The stopped lane did not fail on its own: the walk stopped it because it showed no activity the
176
176
  * relay could see. So "every dispatch lane has now been tried" is false there, and would end the
177
177
  * caller's use of dispatch for a task the lane might still finish
178
- * (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §5). A NAMED lane is never stopped for idleness,
178
+ * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §5). A NAMED lane is never stopped for idleness,
179
179
  * so this names the call that lets it run to its own timeout.
180
180
  */
181
181
  export declare function laneStoppedAdvice(laneId: string): string;
@@ -191,7 +191,7 @@ export declare const IDLE_POLL_MS = 15000;
191
191
  * MCP clients (`initialize` `clientInfo.name`) measured to survive a long tool call, so `dispatch`
192
192
  * may wait for the answer the way the host's own subagent does, up to `routing.mcp.blockingWaitMs`.
193
193
  *
194
- * Measured 2026-09-17 (`docs/mcp-host-timeouts-2026-09-17.md`): Claude Code 2.1.237 (`claude-code`)
194
+ * Measured 2026-09-17 (`docs/history/mcp-host-timeouts-2026-09-17.md`): Claude Code 2.1.237 (`claude-code`)
195
195
  * completed a 240 s tool call headless, with and without progress. Two hosts are deliberately NOT
196
196
  * here: the Claude desktop chat client (`claude-ai`) cancelled at exactly 60 s, and Codex runs a
197
197
  * call inside a code-mode `exec` that yields at 31 s. An unknown client keeps `maxWaitMs`.
@@ -359,7 +359,7 @@ export declare class McpDispatchServer {
359
359
  * streak of `LANE_UNRELIABLE_STREAK` own failures or more, or marked `failing`. Stopping a lane
360
360
  * that may still answer in order to reach those trades an answer for a near-certain failure —
361
361
  * measured 2026-09-10, when the walk stopped `free-pool` to try lanes that had answered 0 of 12,
362
- * 0 of 34 and 0 of 21 runs (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §1). A lane with no
362
+ * 0 of 34 and 0 of 21 runs (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §1). A lane with no
363
363
  * record counts as reliable: unmeasured is no opinion, never a failure.
364
364
  */
365
365
  private stopWithheld;
@@ -445,7 +445,7 @@ export declare class McpDispatchServer {
445
445
  * The quota death an AGY lane stated only in AGY's own log (`agy-quota-log.ts`). AGY retries a
446
446
  * spent quota in silence, so a lane stopped by the walk or by its own timeout printed nothing, and
447
447
  * the death reached the relay only when a run happened to last its whole length
448
- * (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §4). Undefined unless the lane is AGY, names its
448
+ * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §4). Undefined unless the lane is AGY, names its
449
449
  * model, and the log is provably this run's — `agyQuotaStatement` refuses everything else.
450
450
  */
451
451
  private agyLogReport;
@@ -10,7 +10,7 @@
10
10
  * the same reasoning as an unset `${ENV}` disabling one provider instead of aborting startup.
11
11
  *
12
12
  * Design (fork-validated in freellmapi's process-safety-net, adopted 2026-08-13 — see
13
- * docs/freellmapi-adoption-review-2026-08-13.md §1.3): swallow ONLY a closed allowlist of
13
+ * docs/history/freellmapi-adoption-review-2026-08-13.md §1.3): swallow ONLY a closed allowlist of
14
14
  * transport error codes plus a short list of Node/undici-authored message shapes, and
15
15
  * preserve Node's default fail-fast exit(1) for everything else, so genuine bugs still crash
16
16
  * loudly. The classifier is a pure function so it is unit-testable without touching global
@@ -10,7 +10,7 @@ import type { ReasoningMode } from "./config-types.js";
10
10
  * A TUNABLE DEFAULT, never a provider figure — the provenance invariant this whole module already
11
11
  * follows. Until 2026-09-09 this module substituted llm-bridge's flat 1024 here UNCONDITIONALLY,
12
12
  * even for an `openai`-kind target's outbound Chat body, where the field is genuinely optional.
13
- * Measured against DeepSeek (docs/deepseek-responses-truncation-2026-09-09.md): 20 of 68 captured
13
+ * Measured against DeepSeek (docs/history/deepseek-responses-truncation-2026-09-09.md): 20 of 68 captured
14
14
  * upstream answers ended `finish_reason: "length"` at EXACTLY `completion_tokens: 1024` — the
15
15
  * relay's own invented cap, not anything the caller or the provider asked for — and every one of
16
16
  * the 44 emitted `response.completed` events still carried `"status":"completed"`, so a cap
@@ -105,7 +105,7 @@ export type FrontProtocol = "anthropic-messages" | "chat" | "responses";
105
105
  * passthrough condition — `openai`-kind means translated on the Anthropic front, while on the
106
106
  * OpenAI front only `openai`-kind PLUS `chat` is a passthrough. The two spellings never disagreed:
107
107
  * a truth table over all six reachable combinations is in
108
- * `docs/reviews/clone-07-clone-26-evidence-2026-09-05.md`, and `test/stream-commit.test.ts` pins
108
+ * `docs/history/reviews/clone-07-clone-26-evidence-2026-09-05.md`, and `test/stream-commit.test.ts` pins
109
109
  * every row. Naming it once is what stops a fifth call site inventing a seventh row, because a new
110
110
  * front or a new protocol currently has two places to get right and no compiler help.
111
111
  */
@@ -37,7 +37,7 @@ export declare function withStallWatchdog(response: Response, controller: AbortC
37
37
  * Calibrated 2026-09-09, on the `latency-demotion.ts` precedent: 4 x `DEFAULT_LATENCY_MS_PER_TOKEN`
38
38
  * (250 ms/token, itself measured over 68 real requests on 2026-08-30 — see that file's own
39
39
  * comment). A crawl abort hands the client a failure it must retry — measured in
40
- * `docs/post-commit-stall-measurement-2026-09-09.md`: Claude Code retries once, downgraded to a
40
+ * `docs/history/post-commit-stall-measurement-2026-09-09.md`: Claude Code retries once, downgraded to a
41
41
  * NON-STREAMING request; Codex retries up to five times, staying streaming, in all four measured
42
42
  * cells — so the bar must sit far above the demotion threshold, or a deployment merely slow enough
43
43
  * to be latency-demoted would also be aborted mid-response. 250 ms/token is itself ~3.5x the
@@ -31,7 +31,7 @@ interface SchemaLike {
31
31
  * upstream. An in-band error is provenance `upstream` by default and therefore retriable, which
32
32
  * would let a streamed pre-commit refusal reroll onto the next candidate while the buffered lanes
33
33
  * treat the same refusal as terminal — two paths, one policy. See
34
- * docs/dialect-rescue-destructive-refusal-2026-08-24.md §3.
34
+ * docs/history/dialect-rescue-destructive-refusal-2026-08-24.md §3.
35
35
  */
36
36
  export declare const DIALECT_REFUSED_DESTRUCTIVE_CODE = "tool_dialect_refused_destructive";
37
37
  /**
package/docs/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Documentation index
2
+
3
+ Start with the row that matches what you want to do.
4
+
5
+ | You want | Read |
6
+ |---|---|
7
+ | To install and run the relay | [`QUICKSTART.md`](QUICKSTART.md) |
8
+ | Every option, endpoint and caveat | [`reference.md`](reference.md) |
9
+ | To understand the code before changing it | [`architecture.md`](architecture.md) |
10
+ | To submit a change | [`../CONTRIBUTING.md`](../CONTRIBUTING.md) |
11
+ | To know what this project is, and is not | [`project-goals.md`](project-goals.md) |
12
+ | The convictions that settle a design question | [`project-philosophy.md`](project-philosophy.md) |
13
+ | What is still open | [`backlog.md`](backlog.md) |
14
+
15
+ ## Subject documents
16
+
17
+ Each of these covers one subject in depth. Read one when you work on that subject.
18
+
19
+ | Document | Subject |
20
+ |---|---|
21
+ | [`pool-failover.md`](pool-failover.md) | how failover, health and circuit breaking behave |
22
+ | [`pool-eligibility.md`](pool-eligibility.md) | which deployments a pool may admit, and why |
23
+ | [`subagent-routing.md`](subagent-routing.md) | offload design, and the wire evidence behind it |
24
+ | [`host-adaptive-dispatch.md`](host-adaptive-dispatch.md) | how dispatch adapts to the calling host |
25
+ | [`offload-agentic-capability.md`](offload-agentic-capability.md) | what an offloaded agent lane can and cannot do |
26
+ | [`capability-sources.md`](capability-sources.md) | where capability scores come from |
27
+ | [`tool-call-dialect-leak.md`](tool-call-dialect-leak.md) | how a host leaks a tool call as plain text |
28
+ | [`delegate-gate.md`](delegate-gate.md) | the quality gate over a diff an agent lane returned |
29
+
30
+ ## Data files
31
+
32
+ | File | Holds |
33
+ |---|---|
34
+ | `tier-data.json` | the synced capability snapshot. Regenerate with `npm run sync:tiers`. |
35
+ | `dashboard-bundle-inventory.json` | the published dashboard bundle inventory |
36
+ | `dashboard-package-baseline.json` | the package-size ceiling the gate checks |
37
+
38
+ ## Other directories
39
+
40
+ - [`history/`](history/) — dated records: design notes, audits and lap closeouts. These are
41
+ **evidence, not documentation**. Read [`history/README.md`](history/README.md) first.
42
+ - [`../CLAUDE.md`](../CLAUDE.md) — the full source map, written for an AI coding assistant. It
43
+ states the reason behind each rule. Read [`architecture.md`](architecture.md) first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-relay",
3
- "version": "0.84.0",
3
+ "version": "0.84.1",
4
4
  "description": "Loopback bidirectional Anthropic/OpenAI API proxy with tool-call validation and multi-provider routing.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -240,6 +240,7 @@ try {
240
240
  join("references", "direct-routing.md"),
241
241
  join("references", "dispatch-lanes.md"),
242
242
  join("references", "operations.md"),
243
+ join("references", "lane-field-notes.md"),
243
244
  ];
244
245
  for (const relativePath of skillFiles) {
245
246
  const sourcePath = join(skillSourceDir, relativePath);
@@ -161,6 +161,10 @@ Do not read every reference by default.
161
161
  read [references/dispatch-lanes.md](references/dispatch-lanes.md).
162
162
  - For health commands, control authorization, failures, pool-walk headers, eligibility
163
163
  interpretation, or safety invariants, read [references/operations.md](references/operations.md).
164
+ - Before you dispatch a packet, write a brief, judge a lane's output, run several lanes at once, or
165
+ diagnose a lost job, read [references/lane-field-notes.md](references/lane-field-notes.md). It
166
+ holds the measured traps: the CLI-rung reply envelope, lost job ids, restarts that kill lanes,
167
+ the concurrency cap, what each lane can carry, and why a brief's wording is not a boundary.
164
168
 
165
169
  The CLI's `help`, `dispatch_lanes`, and live status commands are authoritative for current
166
170
  configuration and quota. Do not copy a dated lane roster from prose.
@@ -0,0 +1,202 @@
1
+ # Lane field notes — measured behaviour of llm-relay dispatch and its lanes
2
+
3
+ Every note here was MEASURED on this machine, with its date. They moved out of the machine-wide
4
+ backlog on 2026-09-17 (owner instruction: llm-relay-specific instructions belong with the llm-relay
5
+ skill, not in a shared to-do file). Each note is REFERENCE, not work: it is deleted when it becomes
6
+ untrue, never because something shipped. A defect in llm-relay itself belongs in
7
+ `C:\Code\llm-relay\docs\backlog.md` in product terms.
8
+
9
+ Ask the live tools first. `dispatch_lanes` and `llm-relay dispatch` carry the current lane record
10
+ and quota; never copy a dated roster out of prose.
11
+
12
+ ## 1. Reading a dispatch reply
13
+
14
+ - **A `cli` rung's reply body is NOT the lane's answer (2026-09-16).** A `relay` rung such as
15
+ `free-pool` returns the raw answer. A `cli` rung such as `agy-gemini` returns its harness's own
16
+ record — `{"conversation_id":…,"status":"SUCCESS","response":"<the real answer>",
17
+ "duration_seconds":…,"usage":{…}}` — so the answer sits inside `response`. Code that binds the
18
+ body to a schema fails on exactly the calls the ladder sent to a CLI rung, and the failure looks
19
+ like a bad lane. Measured in audit-tools, 2026-09-16: 28 of 62 sweep calls lost, 19 to this
20
+ envelope. (llm-relay packet M1 will unwrap it in the relay and announce an `unwrapped:` line;
21
+ until that release, unwrap it yourself.)
22
+ - **A `SUCCESS` status is not an answer (2026-09-16, owner decided not to fix this in the relay).**
23
+ A `posttooluse-typecheck.mjs` dispatch (job-0021) returned `"status":"SUCCESS"` around 40 repeated
24
+ lines of "Waiting for test execution to complete." and no work product. Read the content by hand.
25
+ - **A completed job can carry no usable answer (2026-09-07 and 2026-09-09).** Agent-mode jobs
26
+ exited after 302 s and 623 s with the single words `Now` and `Let`, and their worktrees were
27
+ clean. Job `job-0005` exited 0 after 765 s and returned `I`. A review job completed after 813 s
28
+ with a completion claim and no findings. Treat a terminal status as evidence of TERMINATION only,
29
+ then inspect the answer and the worktree. None of these outcomes proves a provider quota is
30
+ spent.
31
+ - **`status: "running"` after a long `waitMs` is normal (2026-09-16).** The server clamps `waitMs`
32
+ to `routing.mcp.maxWaitMs`. Poll `dispatch_status`, then `dispatch_result`. In audit-tools 9 of
33
+ 62 calls were lost by treating this as a fault.
34
+ - **Map a lane's verdict table to the file by HEADING, never by row number (2026-09-10).** A
35
+ read-only lane numbered 74 backlog entries out of file order (one moved from row 20 to row 8),
36
+ so its row numbers would have deleted the wrong entries. Re-derive rows with a script and match
37
+ each verdict by its heading text.
38
+
39
+ ## 2. Waits, restarts and lost jobs
40
+
41
+ - **A large `waitMs` loses the job id (2026-09-06).** `waitMs: 180000` returned `Error: Request
42
+ timed out` with no job id, so the lane could not be polled, resumed or cancelled. The same task
43
+ at `waitMs: 40000` returned `job-0001` at once and finished in 145 s. Leave `waitMs` unset.
44
+ - **Codex's code-mode `exec` gives up on a tool call at 31.0 s (2026-09-10).** It returns "Wall
45
+ time 31.0 seconds" with empty output, so a longer MCP call loses its answer: 29 of 266 first
46
+ Codex `dispatch` calls, 2026-09-07 to 2026-09-10. Keep every MCP call from a Codex host under
47
+ 30 s. llm-relay blocks 25 s by default since v0.81.0 and then hands back a job id.
48
+ Since v0.83.x the server waits longer only for hosts measured to survive it: Claude Code with a
49
+ progress token gets the answer in one call, Claude Desktop gets 50 s, every other host keeps the
50
+ ceiling (llm-relay `docs/history/mcp-host-timeouts-2026-09-17.md`).
51
+ - **An MCP server restart KILLS every lane it was running (2026-09-06, re-measured 2026-09-17).**
52
+ The old symptom — `unknown jobId` for every job, numbering restarted at `job-0001`, nothing on
53
+ disk — is fixed: the running-job journal reports each such job as `killed` (v0.80.0) and the job
54
+ archive keeps finished jobs and the id counter across restarts (v0.82.0). The WORK is still
55
+ lost: 14 of 83 archived jobs on 2026-09-17 were `killed`, 11 of them `agy-gemini`. Give every
56
+ lane its own worktree, because that directory is the only record of what a killed lane did, and
57
+ dispatch a killed job again.
58
+ - **One `llm-relay mcp` process can outlive a release (2026-09-10).** The Claude desktop app keeps
59
+ one MCP connection across its sessions, so after a global reinstall that connection runs the old
60
+ code until the app restarts. Since v0.81.0 a reply from a process older than the installed
61
+ package says so.
62
+ - **A session that hits its usage limit mid-turn loses every in-flight job (2026-09-04).** Three
63
+ `relay` subagents lost their lane jobs when the limit hit: the MCP connection was replaced, job
64
+ ids restarted at `job-0001`, and each agent saw `Request timed out` then `Connection closed`.
65
+ Keep the job handles in the main session rather than in subagents, and re-dispatch after the
66
+ reset.
67
+ - **An unrunnable lane is not a quota verdict (2026-09-09).** `Lane "anthropic" cannot be run from
68
+ here: it is a relay target (anthropic) with no cliLane template configured` means no answer was
69
+ produced. It says nothing about any account's quota. Keep the exact error.
70
+
71
+ ## 3. Lane capacity and concurrency
72
+
73
+ - **Cap concurrent lanes at three or four; seven died together (2026-09-06).** Five
74
+ `opencode-muse-spark` and two `agy-gemini` lanes all hit the 2100 s timeout at the same moment,
75
+ exit 124, empty output, four packets lost — one had written 348 lines. Three lanes ran
76
+ comfortably afterwards. The relay does not cap this; the caller must.
77
+ - The tell is SIMULTANEITY. One silent lane is a lane problem; a cohort dying at the same
78
+ elapsed second is a load problem.
79
+ - The usual cause is asking every lane to run the full repository gate. Give a lane its TARGETED
80
+ suites plus lint, and keep the full gate for the orchestrating session.
81
+ - The orchestrator's own gate runs count toward the same budget: a cohort of only three lanes
82
+ also died together at 1800 s while the orchestrating session ran five full gates. Pause
83
+ dispatching while you verify, or drop to two lanes.
84
+ - A killed lane holds its worktree directory open, so `git worktree remove` reports "Permission
85
+ denied" although git DOES unregister the worktree. Believe `git worktree list`, not the
86
+ filesystem.
87
+ - A gate step that reaches the network flakes under this load: `npm audit` returned an error
88
+ payload while three lanes ran. Rerun such a step alone before believing it.
89
+ - **Two concurrent Muse Spark lanes starve, not only three (2026-09-09).** Two packets dispatched
90
+ together ran to the 2100 s timeout with empty output and no file written; single lanes finish in
91
+ minutes. Its rungs carry `maxConcurrent: 1` since llm-relay v0.78.0; keep it that way.
92
+
93
+ ## 4. What each lane can carry
94
+
95
+ - **`opencode-muse-spark`** carries a whole implementation packet ALONE (101–998 s, 2026-09-09),
96
+ and starves as a second or third concurrent lane. Always pass `cwd`. Keep the task text under
97
+ 4,096 characters and put a long brief in a file: a longer task makes the MCP server fall back to
98
+ its start-time config snapshot. It runs as the `relay-lane` OpenCode agent.
99
+ ⚠ Headless OpenCode auto-rejects every permission set to `ask`, and the global default sets
100
+ `edit` and `bash` to `ask`, so without that agent a lane can read but cannot edit or run a suite,
101
+ and the failure looks like a model failure (measured 2026-09-04: `permission requested: edit …;
102
+ auto-rejecting`). A lane dispatched with no `cwd` had every READ rejected as
103
+ `external_directory`. The agent lives in `~/.config/opencode/opencode.json`; a repository-level
104
+ `opencode.json` merges over it, and `agent=<name>` on the run's `stream` lines in
105
+ `~/.local/share/opencode/log/opencode.log` is the only proof of which agent ran.
106
+ ⚠ The agent did not end the zero-output mode (2026-09-04/05): with everything correct, two lanes
107
+ ran 918 s and 895 s and wrote zero bytes. Read-only recon on this lane is 35–70 s, so no file
108
+ change in the worktree after about five minutes is the signal to cancel and re-dispatch.
109
+ - **`agy-gemini`** is the steady CLI lane: 24 of 24 answered at a 240 s median on 2026-09-06, and
110
+ it carried packets at 348–561 s. It ran two lanes in one worktree (authoring plus a read-only
111
+ review) without interference.
112
+ ⚠ It obeys an absolute path written INSIDE the brief over the `cwd` you passed, even when the
113
+ task says not to (2026-09-06). Never write an absolute worktree path into a shared brief, or
114
+ regenerate the brief per lane. Before concluding a lane produced nothing, look where its brief
115
+ pointed.
116
+ - **`agy-claude-opus`** drops the stream on long outputs (2026-09-04/05): `The stream was
117
+ interrupted` after a report summary, and `There was a network issue connecting to the server`
118
+ after 490 s. Split the work into packets and run them on `agy-gemini`.
119
+ - **Codex Spark** reads its whole usage window and writes nothing (2026-09-09, twice): 193k and
120
+ 477k tokens, every test file read whole, then "You've hit your usage limit". A preamble limiting
121
+ reads changed nothing. Give it a review of a bounded diff, or nothing.
122
+ - **A shape that scored 10/10 yesterday is not a cure (2026-09-06).** Three `opencode-muse-spark`
123
+ lanes using the exact shape recorded as reliable the day before wrote nothing in 25 minutes while
124
+ `dispatch_status` said `running` and ten `opencode.exe` processes sat at about 500 MB each. Read
125
+ `dispatch_lanes` before choosing a lane; the same day it read 72 calls / 6 timed out / median
126
+ 616 s for Muse Spark against 24 / 24 ok / median 240 s for `agy-gemini`.
127
+
128
+ ## 5. Writing a brief, and trusting what comes back
129
+
130
+ - **A brief's wording is NOT a boundary (2026-09-06, measured twice).** A lane told to AUDIT a
131
+ closeout spent 19 minutes writing its own `closeout-input.json` into a live repository root, with
132
+ a fabricated verification section. Five later lanes each opened with "Do NOT edit any file.
133
+ Report findings only"; one still ran suites in the shared tree, wrote to the repository root,
134
+ performed the repository's own closeout ceremony, and an untracked deliverable of the
135
+ orchestrating session vanished at the same minute. The controls that DO work: give every writing
136
+ lane its own worktree, use `mode: "answer"` when the lane needs no file access, use
137
+ `readOnly: true` for an agent lane (llm-relay binds the lane's own read-only tool flags since
138
+ v0.82.0), commit an in-progress deliverable before dispatching into the same tree, and run
139
+ `git status --porcelain` after EVERY lane returns or is cancelled. A cancelled lane leaves its
140
+ files behind exactly like a completed one.
141
+ - **A brief that says "never print the key" does not stop a lane writing the key (2026-09-09).** A
142
+ capture lane put `export DEEPSEEK_API_KEY="sk-…"` into a scratch `start-relay.sh`. Tell the lane
143
+ to read a secret from the environment at run time and never copy the value into a file, and grep
144
+ every scratch launcher for `sk-` before running it or passing it on.
145
+ - **Ask a lane to EXTRACT, not to give a VERDICT (2026-09-06).** A free-pool lane given a rubric
146
+ and asked for `clear`/`defective` answered `clear` for all 97 records: a rubric whose rules
147
+ mostly say "this is not a defect" pushes a weak model to the null answer, and the output looks
148
+ well formed. The same job as an extraction — list the terms a cold reader cannot resolve, rate
149
+ 0–10 — did not collapse. Always check a lane's label DISTRIBUTION before using its labels.
150
+ - **Well-formed output can still be wrong, and that is the version that gets believed
151
+ (2026-09-06).** A lane produced 120 clean records in the requested shape; against a hand-labelled
152
+ overlap its best agreement was 64%, while answering "clear" every time scored 73%. Never fold
153
+ lane labels into a count, a training set or a conclusion without measuring agreement on a
154
+ hand-labelled overlap, and always against the always-answer-the-majority baseline.
155
+ - **A lane that returns one large JSON object at the end returns NOTHING when it stops early
156
+ (2026-09-06, three lanes lost).** Have the lane append one JSON line per record as it works, and
157
+ slice the job to about 40 records rather than 100.
158
+ - **Free lanes cannot do open-ended reconnaissance here (2026-09-05, 7 of 7 packets fabricated).**
159
+ They CAN review a concrete diff against a stated claim, and they carry a mechanical rewrite with
160
+ a stated rule. The test is whether the output can be checked by running or reading something
161
+ specific.
162
+
163
+ ## 6. Hooks, keys and the daemon
164
+
165
+ - **A relay lane loads NO global hook (2026-09-17).** `dispatch` launches each Claude lane with
166
+ `CLAUDE_CONFIG_DIR=~/.llm-relay-claude`, so the lane reads `~/.llm-relay-claude/settings.json`
167
+ and never `~/.claude/settings.json`. A global hook guards the orchestrator's own tool calls only.
168
+ Codex, OpenCode and AGY lanes have no hook surface at all; for them the orchestrator-side
169
+ `dispatch-cwd-guard.mjs` and the lane's own worktree are the only controls.
170
+ - **Provider API keys are NOT environment variables on this machine (2026-09-09).** `llm-relay
171
+ keys` prints an `Env var` column, which is the NAME the relay looks for, not proof the variable
172
+ exists: `NVIDIA_API_KEY` is empty in every scope while the same key reads `VALID`, because the
173
+ secret lives DPAPI-wrapped in `~/.llm-relay/keystore.json`. A direct `curl` therefore sends an
174
+ empty bearer, and NVIDIA answers HTTP 500 with a Rust `axum::Extension` message that reads like a
175
+ provider fault. Probe through the relay instead: `POST http://127.0.0.1:8791/v1/messages` with
176
+ `"model": "<provider>/<model id>"`. A public `/v1/models` answer proves nothing about a key.
177
+ - **A provider timeout turns a slow model into a fake "not servable" (2026-09-09).** With `nim` at
178
+ `timeoutMs: 100000`, a 32-token probe of `deepseek-ai/deepseek-v4-flash-0731` returned HTTP 504
179
+ at 100.03 s, while its sibling answered 200 after 81.6 s for two output tokens; the same Flash
180
+ model had answered in 38 s on 2026-08-27. A 504 at the configured timeout is evidence about the
181
+ QUEUE. Raise `providers.<name>.timeoutMs` and probe again before recording a model as dead.
182
+ `firstByteTimeoutMs` (v0.78.0) fails over fast when nothing arrives at all, while a slow body
183
+ still runs to its end.
184
+ - **The daemon reads `config.json` ONCE at start.** A rung or provider edit is invisible until the
185
+ daemon restarts — confirmed live: after a rung edit the `agy.exe` command line still carried the
186
+ old `--model`, and a re-probe after a timeout raise timed out again at exactly the old value.
187
+ Each `llm-relay mcp` process loads the file once too, so a host restart is needed for it as well.
188
+ `GET /telemetry` carries `config.changedOnDisk`, and `routing show|get`, `config show|get` and
189
+ `offload status` print a notice when the running relay has not loaded an edit. Restart: stop the
190
+ node process running `dist\cli.js` with no subcommand (`llm-relay stop` since v0.78.0), then
191
+ relaunch `wscript.exe "…\Startup\llm-relay.vbs"`; verify with `GET /telemetry` and
192
+ `llm-relay dispatch --tier high`.
193
+ - **A pool request to DeepSeek runs with thinking ON, which spends output tokens on reasoning
194
+ (2026-09-09, causes since addressed).** Two authorized paid calls to `deepseek/deepseek-v4-pro`
195
+ returned HTTP 200 and `max_tokens` with zero final text. llm-relay forwards the caller's thinking
196
+ control since 2026-09-10 and `dispatch` takes a `model` argument since v0.81.0. For a pool
197
+ request, give a large `max_tokens` or send `thinking: {"type": "disabled"}`.
198
+ - **Codex Desktop cannot reach a relay pool through a collaboration child (2026-08-31).** With a
199
+ ChatGPT account the launcher validates `pool/medium` against the parent account before contacting
200
+ llm-relay and fails with HTTP 400 `The 'pool/medium' model is not supported when using Codex with
201
+ a ChatGPT account.` Use the MCP `dispatch` tool there. Generated agent files stay valid for
202
+ clients that honour custom providers.