Skip to content

Add a failover gateway over profiles - #54

Closed
jellologic wants to merge 1 commit into
mainfrom
feat/gateway
Closed

Add a failover gateway over profiles#54
jellologic wants to merge 1 commit into
mainfrom
feat/gateway

Conversation

@jellologic

Copy link
Copy Markdown
Owner

Adds src/adapters/gateway/ — a local gateway that routes across profiles and fails over when a provider is overloaded. Motivated by 2026-09-03, when Anthropic returned 529 thirty-three times in ~4 minutes while a healthy z.ai profile sat idle.

Groundwork for #42, #43 and #44 in EPIC #52.

  • Routes derived from existing profiles via resolveProfileRefsbuildIntent; no second config format.
  • Failover remaps the model by tier, so an opus-tier request reaches the fallback's opus-tier model rather than forwarding claude-opus-5 to a host that has never heard of it.
  • Byte-for-byte forwarding. Anthropic signs thinking blocks against the bytes it received, so the body is re-serialized only when the model actually changes.
  • A credential never crosses routes; a session-mode profile forwards the caller's own credential rather than substituting one. Both asserted in tests.
  • count_tokens answered locally — Claude Code calls it for auto-compact and Anthropic rejects it for subscription tokens, so forwarding or 404ing it breaks compaction silently.
  • A 429 whose body reads as a spend cap is not retried.

Off the launch path by construction: reached only through the existing dynamic import of config-root, so the closure stays at 42 modules with no node:http and no fetch. test/architecture.test.ts passes unchanged.

Note for review, given #35: the swisscode config proxy entry point added here is scheduled to move to the web UI. The adapter under src/adapters/gateway/ is the durable part and is what #42/#43 build on.

Also raises the size budget 150 → 175 kB. Worth a look: the artifact had already drifted to 147.7 kB — 1.5% under the old ceiling — while the comment beside it still claimed ~27% headroom. The gateway adds 4.8 kB packed. See #36.

844 tests green.

A launcher answers "which provider should this session use?" once, at launch.
It cannot answer it again twenty minutes later, when that provider starts
returning 529 — which is the failure this addresses. On 2026-09-03 Anthropic
returned 529 thirty-three times in about four minutes while a healthy second
provider sat configured and idle.

`swisscode config proxy --profile work --fallback glm` runs a local gateway
that retries, honours retry-after, and then moves to the next profile.

It has no configuration of its own. Every route is derived from existing
profiles through the same resolution a launch uses (resolveProfileRefs ->
buildIntent), so the gateway and the launcher cannot disagree about what a
profile means. A second config format would be a second thing to keep true.

Failover remaps the model by TIER rather than forwarding the client's string:
an opus-tier request reaches the fallback's opus-tier model, because sending
claude-opus-5 to z.ai is a 404 in a working request's costume.

Properties worth recording:

- Requests are forwarded byte for byte. Anthropic signs `thinking` blocks
  against the bytes it received, so re-encoding a body invalidates them; the
  gateway re-serializes only when it actually changes the model, on failover.
- A credential never crosses routes. A profile authenticated by a login rather
  than a key forwards the caller's own credential instead of substituting one.
  Both directions are asserted in tests.
- count_tokens is answered locally. Claude Code calls it to decide when to
  auto-compact and Anthropic rejects it for subscription tokens, so forwarding
  or 404ing it breaks compaction silently.
- A 429 whose body reads as a spend cap is not retried; that one will still be
  429 in an hour.
- The routing table, the retry policy and the estimator are pure and separately
  tested. Only server.ts touches a socket — the same split adapters/web makes.

Off the launch path by construction: reached solely through the existing
dynamic import of config-root, so the closure stays at 42 modules with no
node:http and no fetch. test/architecture.test.ts still passes unchanged.

The "no proxy, no daemon" claim in README, AGENTS.md and ARCHITECTURE.md is
narrowed to what is actually enforced — a LAUNCH leaves nothing running. The
gateway is opt-in, foreground-only, and stopped with Ctrl-C.

The size budget is raised 150 -> 175 kB. Worth noting in review: the artifact
had already drifted to 147.7 kB, 1.5% under the old ceiling, while the comment
beside it still claimed ~27% headroom. The gateway adds 4.8 kB packed; the
stale headroom is recorded in the script so the next raise starts honest.

Signed-off-by: jellologic <31935831+jellologic@users.noreply.github.com>
@jellologic jellologic closed this Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant