Skip to content

feat: routing, fallback & rate-limit survival (Phase 4) - #6

Merged
MANVENDRA-github merged 1 commit into
mainfrom
feat/phase-4-routing-fallback
Jun 27, 2026
Merged

feat: routing, fallback & rate-limit survival (Phase 4)#6
MANVENDRA-github merged 1 commit into
mainfrom
feat/phase-4-routing-fallback

Conversation

@MANVENDRA-github

Copy link
Copy Markdown
Owner

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

  • Candidate chain + router (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).
  • Resilience executor (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-attempt AbortController timeout, 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.
  • Per-provider throttle (throttle/token-bucket.ts) — in-memory token bucket with an injectable clock that paces outbound calls to each provider's rpm; swappable for Redis later.
  • Observability — traces now carry routedProvider / routedModel / fallbackUsed / retryCount, queryable via GET /traces?fallbackUsed=true (and ?routedProvider=).
  • Config — optional routing block (tiers / fallback) and per-provider rpm in sentinel.config.json; REQUEST_TIMEOUT_MS, MAX_RETRIES, DEFAULT_RPM, THROTTLE_MAX_WAIT_MS env vars.

Decisions (locked with maintainer)

  • Cost routing is opt-in via model: "auto" — explicit model names behave exactly as before, just with the fallback chain. Drop-in semantics preserved; nothing is configured by default.
  • Throttle is in-memory per provider (Redis is a later adapter).
  • Multi-key round-robin deferred — survival comes from per-provider throttling + cross-provider fallback + local Ollama.

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 new routing/router.ts rather than mutating the ProviderRegistry interface, so existing registry behavior/tests are untouched.

Security

SECURITY_REVIEW_LOG.md SR-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 verify green (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 + fallbackUsed trace), model:"auto" tier selection, throttled-primary skip, terminal-4xx no-fallback, and streaming first-chunk fail-over.

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.
@MANVENDRA-github
MANVENDRA-github merged commit 14a8a2c into main Jun 27, 2026
1 check passed
@MANVENDRA-github
MANVENDRA-github deleted the feat/phase-4-routing-fallback branch June 27, 2026 15:02
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