mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
* feat(redis): route the router, rate limiters and auth through the native store families * style(redis): satisfy the python lint gates Format the rate limit store, sort its __all__ and give the routing read batch and two tests concrete list and dict types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(router): give the registry's default usage store a memory-only handle RouterUsageStore now takes a handle, so the default passed None and any usage read before RustCore swapped in its store raised AttributeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(redis): build stores from native handles and wait for router tasks Tests still used the pre-refactor store constructors (Store(None), handle= factories, NativeIncrement, tuple scopes). The router outcome tests counted 20 loop ticks, too few now that the allowed-fails increment resolves on the native runtime, so settle() waits for the scheduled tasks instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
4.8 KiB
4.8 KiB
- One folder per Redis family (
router_cooldowns/,router_usage/,spend_counters/,auth_cache/,rate_limits/), each withmod.rs(the store and its ops),keys.rs(key types and spellings) andtypes.rs(the family's values, with theFromValue/IntoValuecodecs for what it stores in Redis). It is the only place that family's keys, value codec and scripts are written, and it takes domain ids, never keys. Shared pieces live once at the crate root:ErrorandErrorKind(error.rs),MemoryandClock(memory.rs),Pending(request.rs),Remoteand its attach-onceRemoteSlot, which knows nothing of memory (remote.rs),DualCache(dual_cache/) and the Python codecs (codec.rs,value.rs) - An op declared on a
SendreturnsPending<T>, a lazy future built when the op is declared. An op that only means something on Redis (anMGETthe caller falls back from, a script) returnsNonewithout one, and an op that replaced aDualCachecall answers from memory. A post-call method returnsfalsewhen there was no Redis to declare on - A store that sends a pipeline of its own uses
RedisBatch::pipelined, neverPipelineand rawValues - Python and Rust pods share these keys during a rolling deploy, so a module writes the bytes
main's Python writes and reads everythingmainreads. Python formats go throughlitellm-python-compat(repr,literal_eval,json), never a hand-rolled codec - A family holds one
DualCacheperDualCacheits Python held onmain, and eachDualCachemethod is the Python method it names, so a family op that was aDualCachecall is one call here. Wheremain's Python reached pastDualCachetoin_memory_cacheorredis_cache, the family does the same throughmemory()andredis(), which keeps that reach visible. Caches a family's Python attached to oneRedisCacheshare oneArc<RemoteSlot>. Only a family that batch-reads builds its cache withDualCache::batch_readingfrom aTierPolicythe host passes in, and the Python host fills it from whatmainreads from the environment orlitellmglobals (litellm/rust_bridge/host/settings.py,router_tiers). Callers pass aRemote(or none), never a cache. Redis-only families are plain functions over&RedisClient - Every family works with no Redis, answering from memory.
attachadds the Redis tier at most once and keeps memory, asDualCache.attach_redis_cachedoes, so a host builds the store at startup and never rebuilds it to connect - Where
mainswallows a Redis failure and serves memory, the family does the same and logs. It never turns a failure into a miss thatmainwould not - Key suffixes and wire spellings derive from strum on the family's enums. A value the family stores in Redis implements
FromValue/IntoValueoverlitellm_python_compat::Value. An argument or reply is a typed record, never a positional tuple: a frozen Python dataclass read inpython-bridgeby a#[derive(FromPyObject)]struct with#[pyo3(attribute)]fields and converted into the family's type withFrom, and a reply returned as a frozen#[pyclass]. The bridge reads an enum through its strum spelling, never a hand-written match - Wire contracts are hand-written cases with expected values, inlined as the same literals in this crate's test and in
tests/unit/redis/stores/, checked here against real Redis and there against Python's own codec. Change both sides together rate_limits.rskeeps the v3 limiter's Lua byte for byte (rate_limits/lua.rs) so pods on either stack share one script SHA; the admission decisions stay in the Python limiterRequestRedisis one request's batches: a pre-call and a post-callRedisBatchper client, the post one flushed byflush_postor its deadline. Each op exists once and takes aSend(Now,PreCall,PostCall) picked by the caller, and the store callssend.batch(&client). A store never encodes a phase in a method name. A post-call method exists only where a follow-up runs after the write settles (token_increment_post_call), as a thin wrapper over the shared op.Send::Nowis an alone batch, so a script call outside a request goes out alone, like a redis-pyScriptoutside a pipeline, soNOSCRIPTcostsSCRIPT LOADand one moreEVALSHAthe way it does on main. Only what main's Python puts on its request pipeline rides these batches. An op main sends on its own round trip, like a deployment's tpm and rpm increment, stays on its own round trip here- Every store sends the commands main's
RedisCachesends for that operation:MGETwhere Python reads several keys at once,SET ... EX, andINCRBYFLOATwithEXPIREorTTL. AMONITORdiff against main is a parity check, and any difference is a defect - Tests run against
litellm-redis-testingon standalone and cluster. A test is single-topology only for outages