feat: routing, fallback & rate-limit survival (Phase 4) - #6
Merged
Conversation
Turn every request into an ordered candidate chain run through a resilience executor, so a flaky provider or a free-tier rate limit no longer surfaces as a client error. - routing/router.ts: builds the candidate chain. Explicit models become [model, ...fallback]; `model: "auto"` is classified into the cheapest capable tier (routing/classifier.ts) and escalates through the rest. - routing/executor.ts: per-candidate retry with exponential backoff on retryable errors (routing/retryable.ts), fail-over to the next candidate, per-attempt AbortController timeout, and a final 503 when all candidates are throttled. Streaming fail-over is bounded to the pre-hijack first chunk. - throttle/token-bucket.ts: in-memory per-provider token bucket (injectable clock) that paces outbound calls to each provider's rpm. - Traces gain routedProvider/routedModel/fallbackUsed/retryCount across trace.ts, the SQLite + in-memory stores, and the /traces query API (?fallbackUsed=true, ?routedProvider=). - config.ts: optional `routing` block (tiers/fallback) + per-provider `rpm`; REQUEST_TIMEOUT_MS / MAX_RETRIES / DEFAULT_RPM / THROTTLE_MAX_WAIT_MS. - No routing configured = unchanged single-provider pass-through. Docs: README "Routing & resilience", .env.example, config example, SECURITY_REVIEW_LOG SR-004. 134 tests, 98% coverage; pnpm verify green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 4 — Routing, Fallback & Rate-Limit Survival
Adds the reliability/cost half of the gateway. Every request becomes an ordered candidate chain run through a resilience executor, gated by a per-provider throttle, so a flaky provider or a free-tier rate limit no longer surfaces as a client error.
Exit criterion (PRP C3 + C5): a workload that exceeds a single free-tier limit yields zero unhandled 429s to the client.
What's new
routing/router.ts) — explicit models become[model, ...fallback];model: "auto"is classified into the cheapest capable tier (routing/classifier.ts, rules-based) and escalates through the remaining tiers, then the fallback chain (ending at a local Ollama model).routing/executor.ts) — per-candidate retry with exponential backoff on retryable errors (routing/retryable.ts: 429 / upstream 5xx / timeout / network), fail-over to the next candidate, per-attemptAbortControllertimeout, and a final 503 when every candidate is throttled. Terminal 4xx errors fail fast (no pointless fallback). Streaming fail-over is bounded to the pre-hijack first chunk.throttle/token-bucket.ts) — in-memory token bucket with an injectable clock that paces outbound calls to each provider'srpm; swappable for Redis later.routedProvider/routedModel/fallbackUsed/retryCount, queryable viaGET /traces?fallbackUsed=true(and?routedProvider=).routingblock (tiers/fallback) and per-providerrpminsentinel.config.json;REQUEST_TIMEOUT_MS,MAX_RETRIES,DEFAULT_RPM,THROTTLE_MAX_WAIT_MSenv vars.Decisions (locked with maintainer)
model: "auto"— explicit model names behave exactly as before, just with the fallback chain. Drop-in semantics preserved; nothing is configured by default.Compatibility
No routing configured ⇒ chain is just
[registry.resolve(model)]with unlimited throttle, i.e. the Phase 0–3 single-provider pass-through is unchanged. The candidate logic lives in a newrouting/router.tsrather than mutating theProviderRegistryinterface, so existing registry behavior/tests are untouched.Security
SECURITY_REVIEW_LOG.mdSR-004 (area 7 — availability/DoS): bounded per-attempt timeouts + capped retries + a finite, config-defined fallback chain (no request-driven providers/URLs = no new SSRF surface) + per-provider pacing. Box 7.2 ticked; inbound per-client rate-limiting (7.1) still deferred.Tests / verification
pnpm verifygreen (typecheck + lint + coverage). 134 tests, 98% coverage (≥90% gate). New unit suites for the router, classifier, retryable predicate, executor (retry/fallback/terminal/throttle/timeout/streaming), and token bucket; server integration tests cover 429→fallback (200 +fallbackUsedtrace),model:"auto"tier selection, throttled-primary skip, terminal-4xx no-fallback, and streaming first-chunk fail-over.