Skip to content

chore(caching): isolate semantic cache entries - #26990

Merged
yuneng-berri merged 13 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:codex/semantic-cache-tenant-isolation
May 4, 2026
Merged

chore(caching): isolate semantic cache entries#26990
yuneng-berri merged 13 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:codex/semantic-cache-tenant-isolation

Conversation

@stuxf

@stuxf stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Relevant issues

Veria: VERIA-54

What changed

  • Scope semantic cache entries by the generated cache key before storing or searching.
  • Require key-scoped filters for Redis and Qdrant semantic lookup paths.
  • Ignore old or unscoped semantic cache hits that do not carry the matching cache key.
  • Keep Redis upgrades non-disruptive when an existing index schema cannot support key filters.
  • Best-effort create the Qdrant payload index used by scoped lookups.
  • Stabilize lazy OpenAPI snapshot operation IDs so CI verification is deterministic.

Tests

  • uv run pytest tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py tests/test_litellm/test_lazy_openapi_snapshot.py -q
  • uv run ruff check litellm/caching/redis_semantic_cache.py litellm/caching/qdrant_semantic_cache.py litellm/proxy/_lazy_openapi_snapshot.py tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py tests/test_litellm/test_lazy_openapi_snapshot.py
  • uv run --with redisvl==0.4.1 mypy --no-incremental litellm/caching/redis_semantic_cache.py litellm/caching/qdrant_semantic_cache.py
  • uv run pytest tests/test_litellm/caching/test_qdrant_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/test_lazy_openapi_snapshot.py --cov=litellm.caching.qdrant_semantic_cache --cov=litellm.caching.redis_semantic_cache --cov=litellm.proxy._lazy_openapi_snapshot --cov-report=term-missing -q
  • uv run black .

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR scopes semantic cache entries to their LiteLLM-generated cache key for both Redis (via filterable_fields + filter expressions) and Qdrant (via payload field + Qdrant filter objects), preventing cross-scope cache hits. A user-controlled allow_legacy_unscoped_cache_hits flag and a graceful Redis fallback to a new _isolated index make the upgrade non-destructive in most paths. Two residual P2 concerns: the stale-isolated-index path in Redis silently destroys cached data via overwrite=True with no operator opt-out (per the backwards-compatibility rule); and Qdrant's legacy mode fetches only one result, so a wrong-scope top-hit can mask a valid same-key entry lower in the ranked list.

Confidence Score: 5/5

Safe to merge; both findings are P2 with no immediate data-corruption risk on current deployments.

No P0 or P1 issues found. Both findings are speculative or edge-case scenarios (stale isolated-index schema requires a second future upgrade; legacy Qdrant false-miss requires a near-identical cross-scope vector). The core isolation logic is correct and well-tested with mock-only tests.

litellm/caching/redis_semantic_cache.py — stale isolated-index overwrite path; litellm/caching/qdrant_semantic_cache.py — legacy mode limit=1 search

Important Files Changed

Filename Overview
litellm/caching/redis_semantic_cache.py Adds key-scoped isolation via filterable_fields and a graceful fallback to _isolated index on schema mismatch; the stale-isolated-index overwrite path silently destroys cached data without an opt-out flag
litellm/caching/qdrant_semantic_cache.py Adds cache-key isolation via payload filters and best-effort index creation; legacy mode fetches only 1 result so wrong-scope hits block valid same-key hits from being found
tests/test_litellm/caching/test_redis_semantic_cache.py Comprehensive new unit tests for isolation logic, legacy-mode fallback, bytes decoding, and filter-expression construction; all mock-based with no network calls
tests/test_litellm/caching/test_qdrant_semantic_cache.py Adds tests for scoped cache hits/misses, unscoped rejection, legacy-mode opt-in, payload-index failure resilience, and metadata propagation; all mock-based

Reviews (9): Last reviewed commit: "Add semantic cache legacy migration flag" | Re-trigger Greptile

Comment thread litellm/caching/qdrant_semantic_cache.py Outdated
Comment thread litellm/caching/redis_semantic_cache.py Outdated
Comment thread litellm/caching/redis_semantic_cache.py Outdated
Comment thread litellm/caching/redis_semantic_cache.py
Comment thread litellm/caching/qdrant_semantic_cache.py
@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.59829% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/caching/qdrant_semantic_cache.py 88.13% 7 Missing ⚠️
litellm/caching/redis_semantic_cache.py 93.10% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review after ae9b63c. The Qdrant path now best-effort creates the payload index used for scoped lookups. Existing unscoped Qdrant points do not contain the generated LiteLLM cache key or request metadata needed to assign them to the correct scope, so an unfiltered fallback would keep the isolation issue. They are intentionally treated as misses and repopulated by scoped writes, which is the secure cold-cache upgrade path.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review current head 1c19bdd after the coverage-only follow-up.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review current head 74e9344. This follow-up addresses the remaining Qdrant sync scoped-miss metadata thread.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review current head d8c11f9. This follow-up addresses the remaining Redis sync semantic-similarity metadata note.

@krrish-berri-2

Copy link
Copy Markdown
Contributor

please resolve merge conflicts @stuxf

@stuxf

stuxf commented May 2, 2026

Copy link
Copy Markdown
Collaborator Author

Merge conflicts are resolved; the PR diff is back to semantic-cache isolation files only. Re-triggering review on the current head.

@greptileai

Comment thread litellm/caching/redis_semantic_cache.py Outdated
@stuxf

stuxf commented May 2, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed this Redis isolated-index fallback edge case in 8cb52ce0bb: if the original index has the old schema we still fall back to {index}_isolated; if that isolated index also has a schema mismatch, we recreate only the isolated index with overwrite=True. Non-schema ValueErrors still propagate so we do not hide Redis/config failures.

Validation:

  • uv run pytest tests/test_litellm/caching/test_redis_semantic_cache.py -q
  • uv run ruff check litellm/caching/redis_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py
  • uv run black --check litellm/caching/redis_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py

@greptileai

@stuxf

stuxf commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the Qdrant sync metadata finding in a05d3b5. get_cache now sets metadata["semantic-similarity"] on None, empty-result, scoped-key mismatch, and below-threshold miss paths, matching async_get_cache.

Validation:

  • uv run pytest tests/test_litellm/caching/test_qdrant_semantic_cache.py -q
  • uv run ruff check litellm/caching/qdrant_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py
  • uv run black --check litellm/caching/qdrant_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py

@greptileai please rerun on the latest commit.

@stuxf

stuxf commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

Cleaned up the Redis semantic-cache leftovers in 9f1feaadebb432b695017cb46a4c8f6c3458078c:

  • broadened schema-mismatch detection beyond the exact schema does not match wording
  • kept the isolated-index fallback wrapped and covered by tests
  • removed redundant store / astore branches
  • removed the stale async metadata TODO

Validation:

  • uv run pytest tests/test_litellm/caching/test_redis_semantic_cache.py -q
  • uv run pytest tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py -q
  • uv run ruff check litellm/caching/redis_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py
  • uv run black --check litellm/caching/redis_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py

@greptileai please rerun on the latest commit.

Comment thread litellm/caching/redis_semantic_cache.py
@stuxf

stuxf commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

Added an explicit semantic-cache legacy migration flag in af7794272b25b8f971025a0ee27fb23e0c1918449:

  • default behavior remains secure/key-scoped
  • LITELLM_SEMANTIC_CACHE_ALLOW_LEGACY_UNSCOPED_HITS=true deliberately allows legacy unscoped Redis/Qdrant hits during a migration/warm-up window
  • Redis reuses the existing unscoped index only when that flag is set
  • Qdrant omits the cache-key search filter only when that flag is set, while still rejecting keyed hits for the wrong key
  • removed the mutable Redis class-level filter dict

Validation:

  • uv run pytest tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py -q
  • uv run ruff check litellm/caching/redis_semantic_cache.py litellm/caching/qdrant_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py
  • uv run black --check litellm/caching/redis_semantic_cache.py litellm/caching/qdrant_semantic_cache.py tests/test_litellm/caching/test_redis_semantic_cache.py tests/test_litellm/caching/test_qdrant_semantic_cache.py

@greptileai please rerun on the latest commit.

stuxf added 2 commits May 4, 2026 22:10
Mypy infers the dict's value type from the first branch
(Dict[str, bool]) which clashes with the scalar branch's mixed-type
inner dict. Explicit Dict[str, Any] annotation lifts the inference.
The flag was an opt-in escape hatch for the cross-tenant leak the rest
of the patch closes — flipping it on (env var or constructor param)
re-enables exactly the VERIA-54 primitive on either backend. There is
no operational need that the secure path doesn't already meet:

- Qdrant: legacy points without ``litellm_cache_key`` payload are
  excluded by the must-clause filter and treated as misses; new sets
  populate the cache key, so cold-start lasts only as long as the
  natural cache rebuild.
- Redis: existing unscoped index can't carry the new schema; the init
  path falls back to ``{name}_isolated`` (and recreates it on stale
  schema), leaving the legacy index untouched.

Drop the constructor param, env-var fallback, ``_using_legacy_unscoped_index``
flag, the legacy-reuse branch in ``_init_semantic_cache``, and the
matching guards in set/get paths. Update tests to drop the legacy-mode
cases and assert the secure-only behaviour.
@yuneng-berri
yuneng-berri merged commit 07824b5 into BerriAI:litellm_internal_staging May 4, 2026
42 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…nt-isolation

chore(caching): isolate semantic cache entries
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.

3 participants