Leave the gateway running: install it as a local service, and route offline-first when there's no network. Around those two features is a code audit that closes reliability and correctness gaps across the request path and the surface tools. The routing decision is unchanged — still instant, offline, no model call (WF-ADR-0001).
Run the gateway as a local service
wayfinder-router service install registers Wayfinder with launchd (macOS) or systemd (Linux) so it auto-starts at login on 127.0.0.1:8088 and restarts if it exits. Point every OpenAI-compatible app at it once:
wayfinder-router service install
export OPENAI_BASE_URL=http://127.0.0.1:8088/v1uninstall, status, and --print (emit the unit without installing) round it out. Packaging only; your keys stay in the config (WF-ADR-0038).
Offline-first delivery
Set [gateway] offline = true (or send X-Wayfinder-Offline: true per request) and Wayfinder serves the local tier and never calls cloud — for flights, air-gapped machines, or privacy. It reuses the existing degrade + breaker, marks the response with x-wayfinder-router-offline: true, and now takes precedence over the response cache (no dearer-tier replay) and a hard budget block (delivered from local, not a 402). Visible at /healthz, in service status, and in the ?debug payload (WF-ADR-0039).
Hardening — a code audit, turned into fixes
Each ships with a regression test; none touches the routing decision.
Request path
- A bad/expired upstream key (401/403/407) now trips the circuit breaker instead of counting as a success, so failover engages.
- The gateway-wide rate limit runs before virtual-key auth, so a token flood is shed with 429, not
401'd one at a time. - Budget enforcement reads the live config's pricing, so a hot reload takes effect on the next request, not one late.
Claude Code adapter
- Parallel tool calls survive streaming — interleaved calls are buffered per index instead of being merged into one synthetic block (WF-DESIGN-0011).
Surface tools
- Configure saves back to the config it loaded (no stray file when editing from a subdirectory).
- An unknown onboarding arm returns 400, not 500.
- Thread ids use eight random bytes, so same-second chats can't overwrite each other.
service installverifies the unit loaded and exits non-zero on failure instead of printing "installed".
Input guards
- A malformed / old-schema savings ledger no longer crashes
/v1/savings. calibrate --l2 0(or negative) errors cleanly instead of raisingZeroDivisionError.- Non-UTF-8 input files give a usage error, not a traceback.
keys newescapes ids/tags, so the emitted TOML always round-trips.- The launchd generator resolves
~, so a plist built without a log dir spawns. cross_validated_accuracyrejectsk < 2instead of returning a misleading0.0.
Routing controls
/scopeand/stickynow actually route in both backends, not just the status bar.
The suite is up to 604 tests (constant-time key matching, a parsed /metrics exposition, an injectable rate-limiter clock).
Upgrading
pip install -U "wayfinder-router[gateway]"Additive — existing configs, clients, and the routing decision are unchanged.
Full changelog: https://github.com/itsthelore/wayfinder-router/blob/main/CHANGELOG.md