feat: cache-warmth routing (sticky, backoff, thresholds, retry budget) #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "trevin/feat/sticky-backoff-routing"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements the cache-warmth routing redesign specified in
docs/requirements-sticky-backoff.md. The v1 router always tried providers in a fixed priority order with no memory of recent failures, so a persistently rate-limited primary provider was retried first on every request — burning the prompt cache on the healthy fallback and never warming the one it kept hitting.This adds sticky routing (per-client+model last-success target tried first), backoff (per-provider+key exponential suppression after 429/5xx/network errors), two cache-threshold knobs for re-probing preferred providers, and a
max_opaque_retriesbudget for intra-request walking.What changed
Config decoupling (commit 1) — API keys are now declared in a top-level
[api_keys.<name>]section (withenv,priority, optionalname) and referenced from providers viakeys = [...]. This gives keys a stable cross-restart identity used by sticky/backoff. Legacy inline[[providers.*.api_keys]]is rejected at startup with an actionable error pointing at the spec. Validation rules 9-12 added.DB schema (commit 2) — Two new sqlite tables:
sticky(client_key_hash, canonical_model, provider, key_name, last_success_ts, PK(client,model))andbackoff(provider, key_name, until_ts, last_failure_ts, window_s, failure_count, last_error_kind, PK(provider,key)). Both created idempotently.Backoff module (commit 3) —
aor/backoff.py:record_failure,is_backed_off,get,clear,clear_provider,clear_all,list_active. Exponential doubling curve (5s initial, x2 per failure, 24h cap). Success drops the record and resets the counter. Lazy expiry (rows pastuntil_tstreated as absent). Shared across all clients (one record per provider+key).Sticky module (commit 4) —
aor/sticky.py:get,set_on_success,list_all. Per-(client,model) last-success target, persisted to sqlite so it survives restarts. Sticky only changes to a different (provider,key) when that candidate succeeds — never merely because a preferred candidate failed.Routing rewrite (commit 5) —
aor/routing.pyselect_sequencenow: (1) computes non-sticky ordering, (2) filters out backed-off candidates, (3) reads sticky, (4) if sticky is live and its (provider,key) still in config → tries it first, (5) applies threshold logic to decide whether to re-probe a preferred candidate vs stick with the warm cache. Two new[routing]knobs:provider_priority_cache_thresholdandkey_priority_cache_threshold(both default 0 = always re-probe a higher-priority candidate when it's available).Proxy integration (commit 6) —
aor/proxy.py: on success records sticky + clears backoff for that (provider,key); on retryable failure records backoff if the error is 429/5xx/network (402/quota-403 are retryable for walking but do NOT trigger backoff). Newmax_opaque_retriesknob (default unlimited = v1 walk-whole-list behavior;0= first-attempt-only). When all candidates are backed off → terminal HTTP 429 withaor_all_backed_offerror code. Sticky/backoff recording is independent ofauto_fallback.Admin UI (commit 7) — Backoff panel on the stats detail view (per-provider) + active-backoff summary on the overview. Reset-backoff (per-provider) and clear-all buttons, both session-gated, with a flash confirmation message.
Testing
uv run ruff check .— cleanuv run ruff format --check .— clean (30 files)uv run pytest -q— 135 passedNew test files:
tests/test_db.py,tests/test_backoff.py,tests/test_sticky.py. Existing test config strings rewritten to the new[api_keys.*]shape. New proxy tests cover sticky persistence, backoff triggers (429/5xx/network vs 402), terminalaor_all_backed_off,max_opaque_retriesbudget (0 / limited / unlimited), andauto_fallback=falseindependence.Spec reference
Full design and decision rationale:
docs/requirements-sticky-backoff.md(merged todevseparately inc050d32).Expose the new per-(provider,key) backoff state in the admin UI (spec §7): - Per-provider detail view gains a 'Backoff' panel listing each currently- backed-off (provider, key) row with the friendly key name, a relative 'until' time, the current window length, the failure level, and the last error kind. A 'Reset backoff for <provider>' button POSTs to clear that provider's rows (DELETE FROM backoff WHERE provider = ?). - Overview gains a 'Backoff' summary listing all active rows with a 'Clear all backoffs' button (DELETE FROM backoff). - Both resets are POST forms gated by the admin session, redirect back to the stats view, and flash a one-line confirmation via a query param. - A new relative_until helper renders the future expiry compactly ('in 5s', 'in 2m', 'in 1h'). The backoff panel keys its display by key_name (the stable [api_keys.<name>] identity the backoff table stores), not env, via a new _key_display_names map; an unset friendly name falls back to the key_name identifier. Deferred per spec: the active-stickies panel (optional) and a clear-sticky action (OPEN, lean defer) are not added; the sticky store is self-healing under normal operation. Tests: 8 new admin tests covering the detail panel (populated/empty), the overview summary (count/empty), per-provider reset (clears + flash + scope), clear-all (clears + flash), and session-gating of both reset routes.