fix(proxy): stop CacheCodec dropping null fields on cache round-trip - #32207
Conversation
CacheCodec.serialize dumped cached Pydantic models with model_dump(exclude_none=True), which drops any None-valued key, while deserialize does a strict model_validate. For a model with a required-but-nullable field (Optional[X] with no default), a None value is dropped on write and then fails model_validate on read with "Field required", so the entry can never be read back; that is a permanent cache miss, and in readers that rebuild the model from the raw cached dict an uncaught ValidationError that surfaces to the client as a 401 Removing exclude_none makes serialize and deserialize a lossless pair, so None fields are written as null and survive the round trip. LiteLLM_ManagedVectorStoresTable, the one cached model still carrying required-nullable fields and mis-caching on every read today, also gets the None defaults its peers already have
Greptile SummaryThis PR fixes a silent data-loss bug in
Confidence Score: 5/5Safe to merge — the change is minimal, well-scoped, and the two complementary fixes together ensure both old and new cache entries deserialize correctly. The serialize change is a one-line mechanical removal of No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/common_utils/cache_pydantic_utils.py | Removes exclude_none=True from all model_dump calls, making serialize/deserialize a lossless round-trip that preserves None-valued fields as JSON null instead of silently dropping them. |
| litellm/models/managed_files.py | Adds = None defaults to the nine required-but-nullable Optional fields on LiteLLM_ManagedVectorStoresTable, making them genuinely optional and enabling backward-compatible deserialization of old cache entries that were serialized without those keys. |
| tests/test_litellm/proxy/common_utils/test_cache_codec.py | Updates two existing assertions to reflect the corrected behavior (None fields now preserved, not dropped) and adds three regression tests — including a full round-trip for LiteLLM_ManagedVectorStoresTable with all optional fields null. |
Reviews (1): Last reviewed commit: "fix(proxy): stop CacheCodec dropping nul..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Relevant issues
Linear ticket
Resolves LIT-3277
Resolves LIT-3427
Pre-Submission checklist
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
CacheCodec.serializewrote cached Pydantic models withmodel_dump(exclude_none=True), which drops anyNone-valued key, whiledeserializedoes a strictmodel_validate. When a cached model has a required-but-nullable field (Optional[X]with no default), aNonevalue is written as an absent key and then failsmodel_validateon read withField required, so the entry can never be read backLiteLLM_ManagedVectorStoresTableis such a model, so this is live today on any deployment that resolves a managed vector store (the cache read inget_managed_vector_store_rows_by_uuidsruns on the request path)Repro config (Redis-backed cache + DB):
Before (current
litellm_internal_staging)Every read after the first logged:
The
HTTP 500is the downstream provider call (there is no realopenaivector store behind this id) and is unrelated; the cache read runs first, fails, and forces a DB re-query on every request so the cache never serves the rowAfter (this PR)
Same flow, same searches, count of deserialize failures is
0:Type
🐛 Bug Fix
Changes
litellm/proxy/common_utils/cache_pydantic_utils.py:CacheCodec.serializeno longer passesexclude_none=True, soserializeanddeserializeare a lossless pair andNonefields are written asnull. This is the root fix and covers every cached model, not just the one belowlitellm/models/managed_files.py: give the nineOptionalfields onLiteLLM_ManagedVectorStoresTableaNonedefault, matching every other cached model; this is the one cached model still declared required-but-nullabletests/test_litellm/proxy/common_utils/test_cache_codec.py: flip the two tests that assertedNonekeys were dropped, and add regression tests that fail under the oldexclude_nonebehavior (a required-nullableNonefield must surviveserializethendeserialize, and a realLiteLLM_ManagedVectorStoresTablewith all optional fields null round-trips without a validation warning)Out of scope here to keep this isolated: three readers still rebuild a cached model from the raw dict without the typed codec (
budget_reservation.py, the budget path inauth_checks.py,mcp_server_manager.py). After this fix the cached dict is complete so they no longer break, but routing them through the typed codec would make a future schema drift degrade to a cache miss instead of an uncaught error; I can do that in a separate PR