claude gateway: retire the auto route — a turn runs where its model id says, and fails closed - #1198
Conversation
|
📘 Docs preview — the preview channel for this PR has been deleted now that it is closed. |
2f9346d to
2038132
Compare
Real-hardware verification — sv-mag (linux, vLLM 0.28.0 / gpt-oss-20b)Ran against this branch's build, installed over the host's binaries and the The surfaces lost their route rows
The upgrade path cleans up after the retired mechanismsSeeded managed settings with the retired Stop hook and an operator's own, waired's Stop entry gone, the operator's kept, the SessionStart hook untouched, waired writes no default modelRemoved Picker cache after enable — four rows, and the retired 1M row is not among them: A Waired turn runs on WairedThe second one is worth noting on its own: with this computer's engine off, the … and fails closed when no node canLocal inference off and Through a real waired-ai/waired#665 is gone
Not answered by a local model. The response carries no — no
Not covered here
Host left on: this branch's build (0.0.0-dev), engine ready, inference on, |
40b0f9d to
9188ae0
Compare
… it (#1202) `WairedSetup-<version>-x64.exe` is attached to every release and every edge prerelease, and it is a full install path: it elevates, registers the service, starts it, and turns on the Claude Code integration by default. The site described one route, the `iwr … | iex` line, and did not name the setup program anywhere until #1199. The gap had already produced two wrong promises. packaging/windows/ sat outside docs-surface-guard's surface list until #1199 added it, so the GUI installer's behaviour could change with no docs page obliged to keep up — which is how getting-started/update.mdx came to promise a fresh-install behaviour only install.ps1 had (#1181). And every release's notes tell people to "see SmartScreen note in docs" for a note that did not exist; SmartScreen appeared nowhere under docs-site/. The new section follows the order a person downloading it actually meets: no `latest/download/…` shortcut, because the filename carries the version; "Windows protected your PC" and the way through it, which is why the page still leads with the PowerShell line; the permission prompt; a wizard that does not ask where to install and has one checkbox; that it does not run `waired init`, so signing in is still ahead of you; the log it always writes; in-place upgrade and uninstall, including that the state directory is kept by default; and the unattended switches with exit 0 / exit 7. The checkbox is described rather than quoted: its on-screen text still promises a transparent fallback to the Anthropic API, which #1198 is retiring, and a verbatim quote would go stale the day that lands. Also widen the "What gets installed" Start Menu row — both installers create **Waired** and **Waired (CLI)**, and the table listed one. Fixes #1202 Refs #1181, #1199, waired-ai/waired#1312 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JQqxQSWDFbQauMDuCbJ2
A turn now runs where its model id says. A Waired id is served on a Waired node or fails with a reason Claude Code shows at once; an id the real Anthropic API serves passes through with its errors intact; an id neither side owns is served here, so nothing but a turn naming a real Anthropic model leaves the machine. Removed: the auto route and its post-dispatch fallback (fallback.go), the SSE reroute notice (reroute_notice.go), the anthropic-route local degrade (waired-ai/waired#665), the agent-not-wired fail-open passthrough, the X-Waired-Fallback / -Fallback-Allowed / -TTFB-Budget-Ms headers, last_fallback and its per-class routing policy, `waired claude route`, the /waired-route skill, the tray's route rows and the Stop hook that announced a fallback. The wait policies stay and split by leg instead of by permission: a peer leg keeps the liveness watch and stays uncommitted so its failure can name the peer, a local leg writes SSE keepalive and is never aborted. waired also stops setting Claude Code's default model (owner ruling 2026-09-03): with fail-closed turns, a written Waired default would make every session fail on a computer with no engine and no peer. Refs #1184, #1183, #1180, waired-ai/waired#1313 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Rewrites the suites the removal inverted, each row saying so: - intercept: the three dispatch rows (an Anthropic id leaves, a Waired id does not, an id naming neither side is served here), the fail-closed 400 naming its way out, and the picker table without the retired 1M row. - gateway: waitPolicyFor split by leg instead of by the intercept's permission header, and the selection-error table split by surface — the Claude side fails closed with a 400, the OpenAI side is unchanged. - claudemanaged: Write now REMOVES a retired Stop hook instead of installing one. - claudecode: only the removal of the /waired-route skill and of the default model remains. - e2e sentinel: the escape is now an upstream-unreachable 502 on a leg that claims to be served locally; the degrade-header rows are gone with the degrade. Also keeps the ratified minimum-model-size phrasing (waired-agent#1128, "a large model", never "a large model or larger") alive at its new site, the gateway's fail-closed message, since the reroute notice that used to carry it is gone. Refs #1184, #1183, #1180 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Rewrites every page that described the auto route or the automatic fallback, English and the ja mirror: the "Which AI answered?" table from the status line's own strings, "Switch where requests go" as /model rather than /waired-route, the privacy page's "one place a cloud fallback exists", the CLI reference's route block and --min-model-size reroute, the troubleshooting sections about the ten-minute backstop and a pinned computer, the tray's Claude Code submenu, the glossary's "Falling back", the doctor sample, the landing diagram, and the first-run boxes. Adds a troubleshooting section for the new fail-closed error. Records the owner ruling that waired no longer writes Claude Code's default model (20260904/0146), which 20260828/0252 §4 had required: that was safe only while a Waired turn could still be carried to the real Anthropic API, and would now fail every session on a computer with no engine and no peer. Also makes four product strings true again — "requests go to your other computers or the cloud" no longer describes what happens — and unifies the two spellings of the fail-closed message's tail. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
… turn The routing block lost its policy rows with the routes, and took the last-served and waired-node lines with them by accident — the two that answer "where is my work actually going" now that the side is the model id's to decide and the node is `waired worker`'s. Also drops the dead code the removal left behind, and documents the rows. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Measured against Claude Code 2.1.259 with a stub gateway, one status per run: 400 is the only one shown at once, verbatim, in a single request and without a prefix. 404 discards the message and substitutes "the selected model may not exist" — which is what rc5's O14 saw — and 503 adds a server-side diagnosis of its own after three retries. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Two things the default build never compiles, and so never checked: - internal/e2e/integration is behind `-tags integration`, where the harness still named state.ClaudeRouteAnthropic and the deleted fallback header. It takes its "the turn went upstream" vocabulary from claudecode.RouteAnthropic now, and the failure block no longer tries to recover a status nothing discards. - installtest-windows.ps1's #787 contract row required the Stop hook to be written as the bare Windows command. The hook is retired, so the row is inverted — it must be absent — and the SessionStart hook that keeps the /model picker current takes its place as the entry under test. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
X-Waired-Fallback-Allowed and X-Waired-TTFB-Budget-Ms had one reader each left, and both were about a crossing that no longer happens. capacityQueueBudget read the first to keep the auto leg failing fast: a turn with somewhere else to go must not wait for a busy peer's slot. No turn has somewhere else to go now, so the exemption goes and every leg waits — which is what that function's own comment always said about a leg with no escape. Its "auto route never queues" rows are inverted. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
The install-time consent text still said local inference "falls back to the real Anthropic API when unavailable, so claude never breaks". It does not, and saying so is the sentence a user would act on. It now names the choice that replaced it: /model picks where each turn runs. Also two comments that still described the machine-wide route policy. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
`waired link --help`'s Claude Code section and doctor's engine-not-ready detail both told the reader that requests fall back to the real Anthropic API. They do not. Both now name what actually happens: /model decides the side, and a Waired turn with no node to take it fails. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
The CLI reference (and its ja mirror) said pausing put your tools back on the cloud. It stops this computer answering; a turn addressed to Waired then goes to another of your computers, or fails. Refs #1184 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR Signed-off-by: gen16k <gen16k@users.noreply.github.com>
The rebase over #1195 left doctor.mdx and troubleshooting.md's ja mirrors carrying a hash computed against the pre-rebase English page. i18n:check is green again. Refs #1184 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjQDeCzecmEYvCZvnw9R9f Signed-off-by: gen16k <gen16k@users.noreply.github.com>
The router's text is a diagnosis for a log, and the mesh wrapper appends the model id after the peer, so the message a user reads ended as two quoted values run together: `… is unreachable: "sv-mag": "gpt-oss-20b"`. Measured on sv-mag with a real Claude Code turn. This is the one error whose whole point is that a person can act on it (waired-agent#1180), so it now says which computer, in words. Refs #1180, #1184 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjQDeCzecmEYvCZvnw9R9f Signed-off-by: gen16k <gen16k@users.noreply.github.com>
waired-agent#1200 moved the ruled phrasing into router.ModelSizePhrase and gave SizeFloorError its own sentence, so the copy this branch kept alive after deleting the reroute notice is redundant. The gateway now passes the router's text through and adds only the exit specific to a floor miss — the setting to change. Also re-accepts the ja cli.md pair: main's #1164/#1207 edited the English page while this branch held the translation hash. Refs #1184, #1128, #1200 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjQDeCzecmEYvCZvnw9R9f Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Refs #1184 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjQDeCzecmEYvCZvnw9R9f Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Two places outside Go and docs-site that this branch's sweep missed, both found by the L96 lane: - waired-setup.iss's [Tasks] description is the checkbox text in the GUI installer's wizard, and it promised that Waired "transparently falls back to the real Anthropic API". It says what decides now instead. - installtest-windows.ps1 drove `waired claude route` as its proof that an allow-list-exempt read reaches the daemon over the named pipe. That command no longer contacts the daemon, so the probe would have kept passing while proving nothing. It drives `waired claude status` now, and asserts a line that is only printed once the read succeeded. Refs #1184 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjQDeCzecmEYvCZvnw9R9f Signed-off-by: gen16k <gen16k@users.noreply.github.com>
Refs #1184 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjQDeCzecmEYvCZvnw9R9f Signed-off-by: gen16k <gen16k@users.noreply.github.com>
a6fcf0a to
b2e2a2a
Compare
…the page (#1215) Every ja page carried a sourceHash in its frontmatter — a digest of the English page it was translated from — and i18n:check compared it. That is a derived value stored in a versioned file: two PRs touching one English page always rewrote it to two different values and always conflicted, on that line and nothing else, while the prose merged cleanly. Measured on main over the 60 days before this: 219 of 622 commits touch a ja page, 90% of them rewrite the hash, and 35 of 144 overlapping PR pairs share a ja file. At one or two docs lanes that is a cost per collision. At today's lane count it is a condition on landing — #1198 held 12 of the 32 ja pages for 2 h 19 m and went CONFLICTING four times, each within nine minutes of a competing PR merging, and the answer of the day was to hold everyone else's merges. It is not only wide docs PRs: #1237, a gateway change that touches three docs pages incidentally, spent two of its four force-pushes on the same line. This PR itself went CONFLICTING 2 m 40 s after it was opened, before its own CI finished, on one line of ja/troubleshooting.md. Removing the stored value removes the class. The freshness question moves to the diff, where nothing is stored and nothing conflicts: a PR that changes an English page changes its Japanese one too, enforced by scripts/ci/i18n-pair-guard.sh in docs-guard.yml, with a `translation-not-needed: <reason>` line in the PR body for the English edit that genuinely needs no translation. Replayed over every commit since the mirror was enforced (#147), that line would have been needed six times across 405 page pairs — three terminology sweeps where an English phrase changed and the Japanese sentence was already right — and nothing else in the repository's history would have failed the rule. A merge driver was considered and does not work: a custom driver only runs where git config sets it, and GitHub computes mergeability server-side, so the PR still goes DIRTY and still has to be rebased. i18n:check keeps the two questions that are about the tree and cannot be answered from a diff: is there a ja page at all, and do the two sides still have the same shape (#678, #1011). That comparison now runs on every pair, always, instead of only on pairs whose hashes already agreed — and that gate was worth less than it looked, because on the path a stale pair actually takes, --accept wrote the hash before it ever reached the refusal, recording a lost heading as intentional and reporting it only on the next run. --accept and i18n:accept are gone with the value they wrote. So is the "may be out of date" notice in PageTitle.astro and the second copy of the digest it carried: deploy-docs.yml runs i18n:check before the build in the same job, so a page the notice would fire on never reaches the site, and it has never rendered on docs.waired.ai since it was added alongside the check in #147. The rule and its opt-out are written where a contributor already reads about the sibling one — CONTRIBUTING.md next to docs-not-needed:, and CLAUDE.md §Documentation, whose "npm run i18n:check gates it" was true of freshness before this change and is not any more. Sequenced directly with the lanes that had docs PRs open (#1235, #1237 first; #1241 after), because this touches all 32 ja pages at once. Fixes #1215 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgrcRiYj339QxzD3b6KijQ
Three residuals of the /model restructure, all found reading the surrounding code during L97. CLAUDE_CODE_MAX_CONTEXT_TOKENS was written only when this host resolved a local window, so a computer with no engine wrote nothing at all — and Claude Code then assumed its own 200k default AND showed "isn't described by this version's model catalog" on every Waired row, which is the notice the variable exists to suppress. Every row such a host offers is a peer row, so a peer's window is not an approximation of something better; it is the only honest number available. WriteOptions and RemoveOptions gain a PeerContextWindow used only when the local one is 0, and both go through DeclaredContextWindow() so a scrub recognises exactly what a write would have put there. Smallest reachable window, not largest: the number sizes one session and the rows it covers are several computers, so over-declaring means a turn is compacted only after the gateway has already refused it. Nothing reachable still writes nothing — declining beats guessing about what an elevated process tells Claude Code. The general "one number for rows that hold different windows" question is settled as leave it, recorded rather than changed: the variable is a compaction hint, and what actually refuses an over-long prompt is this gateway's own 400. KindClaudeNodeChange and KindClaudeNodeFallback have had no producer since #1198 removed the anthropic-route degrade and the per-class routes. Both repositories were checked for readers: none in code, two mentions in the private dev-docs, which follow separately. Fixes #1246 Claude-Session: https://claude.ai/code/session_01HJUUSmAfbRdjER1seDdjXm Signed-off-by: gen16k <gen16k@users.noreply.github.com> Co-authored-by: gen16k <gen16k@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Retires the Claude gateway's
autoroute and every automatic crossing betweenWaired and the real Anthropic API. A turn now runs where its model id says, and
when the side it names cannot answer it fails with a reason Claude Code shows at
once.
Owner ruling: waired-ai/waired#1313, recorded in
docs/decisions/20260903/0333-no-automatic-crossing-to-or-from-anthropic.md(merged as #1189) and its private twin.
What the gateway does now
there, and it keeps the sentinel's claim exact: nothing but a turn carrying a
real Anthropic model id leaves the machine.
Removed
routeAutoand the per-class route axis;internal/proxy/intercept/fallback.go(the post-dispatch fallback recorder) and
reroute_notice.go(the SSE notice,#757);
passthroughWithLocalDegrade's degrade branch (waired-ai/waired#665),including for the auto-mode classifier; the agent-not-wired / degraded fail-open
passthrough; the
X-Waired-Fallback,X-Waired-Fallback-Allowed,X-Waired-TTFB-Budget-MsandX-Waired-Min-Model-Sizeheaders;last_fallbackand
desired-claude-routing;waired claude routeand the/waired-routeskill; the Stop hook
waired claude _fallback-hookand its managed-settingsentry; the tray's main-3 / sub-4 route rows and its fallback note; the
Waired auto — 1Mpicker row; the statusline'sfallback → Anthropic,fell back → AnthropicandWaired-onlysegments.X-Waired-Fallback-Reasonand the tray's Recent activity rows are untouched:those report Waired picking a different computer on your own mesh, which is a
different mechanism from the crossing this removes.
The fail-closed error shape
HTTP 400, with a message naming what could not answer and the two ways out.
Measured against Claude Code 2.1.259 (stub gateway, one status per run):
API Error: 400 <our message>— verbatim, at onceFailed to authenticate.Failed to authenticate.400 is the only status shown at once, verbatim, with one request and no
misleading prefix — which also confirms rc5's O14 (waired-ai/waired#1309): a 404
throws our message away, so today's
model_not_servedand floor 404s lose theirtext inside Claude Code.
A condition that clears by itself keeps its retryable shape: weights still
arriving, every peer momentarily at capacity, a runtime still installing stay
503 with a
Retry-After.anthropicSelectionStatusdecides the two sets once,so the event ring records the status the client received (waired-agent#740).
Waits
waitPolicyForsplit by leg instead of by the intercept's permission header:can still carry a status that names the peer;
This closes a hole the header hid: a pinned peer leg previously got no wait at
all and hung silently.
capacityQueueBudgetis now always armed for the samereason its own comment gives — every leg has nowhere else to go.
Fixes waired-agent#1180
A named peer row whose peer disappears now ends the turn at once with the peer's
name (
PinnedPeerUnreachableErrorgainedPeerName, resolved from the meshsnapshot, grant pseudonym for a Public Share peer per spec §8.5), instead of ten
anonymous
API error · Retrying 1/10attempts.Fixes waired-agent#1183
localRequestbuilds the local-inference request from an allowlist instead ofcloning:
Content-Type,Accept,Accept-Encoding,anthropic-version,anthropic-beta,x-claude-code-*and waired's ownX-Waired-*. Everythingelse —
Authorization,x-api-key,Cookie,Proxy-Authorization,User-Agent, and any header this build has not heard of — is not copied, so aheader added upstream is dropped by default rather than forwarded by default.
The passthrough leg stays byte-exact, as the gateway contract requires.
waired no longer sets Claude Code's default model
Owner ruling 2026-09-04, recorded in
docs/decisions/20260904/0146-waired-does-not-set-claude-codes-default-model.md,which partially supersedes
20260828/0252§4. §4 was safe only while a Wairedturn could still be carried to the real Anthropic API; with turns failing closed
a written Waired default would break every session on a computer with no engine
and no peer (rc5 had exactly such a host). A value already in the file is left
alone, and
waired claude disablestill removes one Waired wrote.Inverted tests, each saying so at its site
TestWaitPolicyFor's "peer, no header (pinned route) → no wait" row.claudemanaged: Write removes a retired Stop hook instead of installing one.TestAnthropicMessages_PinnedPeerUnreachable…: 503 → 400, andRetry-Aftermust now be absent (a fail-closed pin has nothing to wait for).
selection_error_test's contract table is now per-surface: the Claude sidefails closed, the OpenAI side is unchanged (other clients, other retry rules).
upstream-unreachable 502 on a leg that claims to be served locally.
Verification
gofmt/go vet(+-tags prod, +proto) /golangci-lint run/go test ./.../make verify-cross/make ci-lint-local(34 guards) allgreen locally.
docs-site:npm run i18n:check— 32 page pairs in sync;npm run buildOK.claudebinary.merged: single-machine fail-closed, the named-peer repro for claude: when a named peer row's peer disappears, the client just shows "API error · Retrying 1/10" — no peer name, no reason #1180, an
Anthropic id under a blackholed upstream, and that
waired claude enableleaves
~/.claude/settings.jsonalone.Notes for the lanes waiting on this
local_status_*fallback vocabulary isgone;
engine_request_failedandengine_truncated_streamare untouched.The ratified minimum-model-size phrasing (waired-agent#1128 — "a large model",
never "a large model or larger") moved from the deleted reroute notice into
internal/gateway/anthropic.go'smodelTooSmallDetail, and should collapseinto the router's own error text when router: two error texts that mislead — a ready model called "not in ready state", and a hidden reason for the public entry (0.0.3-rc5) #1178 moves the rule there.
claude-waired-autokeeps its spelling and now means"any Waired node, fail-closed"; the id redesign and the
modelPickermove arestill yours. Note that
waired claude enableno longer writes a default modelat all, which changes that issue's last bullet.
Fixes #1184
Fixes #1183
Fixes #1180
Refs waired-ai/waired#1313
Refs waired-ai/waired#1312
🤖 Generated with Claude Code
https://claude.ai/code/session_012FKTcq342KkitvJxaviMdR