Skip to content

fix(proxy): stop CacheCodec dropping null fields on cache round-trip - #32207

Merged
ryan-crabbe-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_lit_3277_jwt_team_membership_401
Jul 6, 2026
Merged

fix(proxy): stop CacheCodec dropping null fields on cache round-trip#32207
ryan-crabbe-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_lit_3277_jwt_team_membership_401

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-3277
Resolves LIT-3427

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

CacheCodec.serialize wrote cached Pydantic models with model_dump(exclude_none=True), which drops any None-valued key, while deserialize does a strict model_validate. When a cached model has a required-but-nullable field (Optional[X] with no default), a None value is written as an absent key and then fails model_validate on read with Field required, so the entry can never be read back

LiteLLM_ManagedVectorStoresTable is such a model, so this is live today on any deployment that resolves a managed vector store (the cache read in get_managed_vector_store_rows_by_uuids runs on the request path)

Repro config (Redis-backed cache + DB):

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL
  store_model_in_db: true
litellm_settings:
  cache: true
  cache_params: { type: redis, host: localhost, port: 6379 }

Before (current litellm_internal_staging)

# 1) create a managed vector store whose optional columns are null
curl -s $BASE/vector_store/new -H "Authorization: Bearer $MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"vector_store_id":"vs_lit3277_repro","custom_llm_provider":"openai"}'

# 2) resolve it 5x (the search reads the store from cache before anything else)
for i in 1 2 3 4 5; do
  curl -s $BASE/v1/vector_stores/vs_lit3277_repro/search -H "Authorization: Bearer $MASTER_KEY" \
    -H "Content-Type: application/json" -d '{"query":"hello world"}' -o /dev/null -w "search #$i -> HTTP %{http_code}\n"
done

# 3) how many cache reads failed to deserialize?
grep -c "CacheCodec.deserialize: validation failed" proxy.log
search #1 -> HTTP 500
search #2 -> HTTP 500
search #3 -> HTTP 500
search #4 -> HTTP 500
search #5 -> HTTP 500
4

Every read after the first logged:

ERROR ... UserApiKeyCache.async_get_cache failed to deserialize cached value for
  key='managed_vector_store_id:vs_lit3277_repro' model_type=LiteLLM_ManagedVectorStoresTable
WARNING ... CacheCodec.deserialize: validation failed for LiteLLM_ManagedVectorStoresTable (5 validation errors ...
  vector_store_name / vector_store_description / vector_store_metadata / litellm_credential_name / team_id
  Field required [type=missing] ...)

The HTTP 500 is the downstream provider call (there is no real openai vector 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 row

After (this PR)

Same flow, same searches, count of deserialize failures is 0:

search #1 -> HTTP 500
search #2 -> HTTP 500
search #3 -> HTTP 500
search #4 -> HTTP 500
search #5 -> HTTP 500
0

Type

🐛 Bug Fix

Changes

  • litellm/proxy/common_utils/cache_pydantic_utils.py: CacheCodec.serialize no longer passes exclude_none=True, so serialize and deserialize are a lossless pair and None fields are written as null. This is the root fix and covers every cached model, not just the one below
  • litellm/models/managed_files.py: give the nine Optional fields on LiteLLM_ManagedVectorStoresTable a None default, matching every other cached model; this is the one cached model still declared required-but-nullable
  • tests/test_litellm/proxy/common_utils/test_cache_codec.py: flip the two tests that asserted None keys were dropped, and add regression tests that fail under the old exclude_none behavior (a required-nullable None field must survive serialize then deserialize, and a real LiteLLM_ManagedVectorStoresTable with 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 in auth_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

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-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a silent data-loss bug in CacheCodec.serialize where model_dump(exclude_none=True) was stripping None-valued fields before writing to cache, while deserialize used a strict model_validate — causing any model with a required-but-nullable field to fail deserialization on every cache read after the first write.

  • Root fix (cache_pydantic_utils.py): drops exclude_none=True from all four model_dump calls so None fields are serialized as JSON null, making serialize/deserialize a lossless pair for all cached models.
  • Secondary fix (managed_files.py): adds = None defaults to the nine Optional fields on LiteLLM_ManagedVectorStoresTable, making old cache entries (written without those keys) still deserialize correctly after the upgrade.
  • Tests (test_cache_codec.py): existing assertions are updated to match the corrected behavior, and three new regression tests cover the required-nullable round-trip and the LiteLLM_ManagedVectorStoresTable full round-trip.

Confidence Score: 5/5

Safe 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 exclude_none=True applied consistently across all four call sites. The defaults added to LiteLLM_ManagedVectorStoresTable make the fix backward-compatible with entries already in cache. Test updates correctly reflect the new (fixed) behavior rather than masking a regression, and three new regression tests specifically exercise the previously broken round-trip scenario.

No files require special attention.

Important Files Changed

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

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ryan-crabbe-berri
ryan-crabbe-berri merged commit 7148c7c into litellm_internal_staging Jul 6, 2026
124 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_lit_3277_jwt_team_membership_401 branch July 6, 2026 19:53
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.

2 participants