feat(caching): add valkey-semantic cache backend and fix semantic cache scope keys#30675
Conversation
|
|
82b6063 to
6078a55
Compare
|
@greptileai please review the latest commit |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
6078a55 to
fa04892
Compare
|
@greptileai re-review latest commit (modernized type hints, valkey-search index-info override) |
Greptile SummaryThis PR adds a new
Confidence Score: 4/5The new valkey-semantic backend is well-implemented, but the scope-key change in get_cache_key() silently invalidates the entire warm cache for all existing redis-semantic and qdrant-semantic deployments on upgrade. The get_cache_key() modification removes prompt content from the hash and adds tenant scope for every semantic cache type. Existing redis-semantic and qdrant-semantic users will generate a different key format after upgrading, making all previously stored entries unreachable for the remainder of their TTL. The change is intentional and fixes a real bug, but it has no opt-in flag and no migration guidance, so the cold-cache impact is invisible to the operator upgrading a production system. litellm/caching/caching.py — the get_cache_key() scope-key change and its reach across existing semantic cache backends.
|
| Filename | Overview |
|---|---|
| litellm/caching/caching.py | Adds VALKEY_SEMANTIC dispatch to Cache.init and modifies get_cache_key() to exclude prompt params for ALL semantic cache types — a backwards-incompatible change for existing redis-semantic and qdrant-semantic users whose warm caches become unreachable on upgrade. |
| litellm/caching/valkey_semantic_cache.py | New ValkeySemanticCache implementation talking directly to valkey-search over redis-py; clean design with lazy redis imports gated behind the lazy import in caching.py. Two previously-flagged issues (partial-client init path, _index_dim set unconditionally after already-exists) are tracked in prior review threads. |
| litellm/types/caching.py | Adds VALKEY_SEMANTIC = "valkey-semantic" enum value; minimal and correct. |
| tests/test_litellm/caching/test_caching.py | New unit tests for semantic scope-key logic; all use mocked/lazy Redis clients, no real network calls, good coverage of paraphrase bucketing, tenant isolation, model separation, and exact-cache regression. |
| tests/test_litellm/caching/test_valkey_semantic_cache.py | Comprehensive mock-only unit tests for ValkeySemanticCache; covers URL building, dimension mismatch, TTL, threshold, async roundtrip, index info override, and the lazy-import guarantee via subprocess. |
Reviews (7): Last reviewed commit: "feat(caching): add valkey-semantic cache..." | Re-trigger Greptile
fa04892 to
255d845
Compare
|
Addressed both P2 findings in the latest commit (255d845):
@greptileai please re-review |
255d845 to
53a8481
Compare
|
Fixed the code-quality failure: |
53a8481 to
e4c8ffc
Compare
Adds setup docs for the new valkey-semantic cache backend (semantic prompt caching on Valkey clusters running valkey-search, including AWS ElastiCache for Valkey) to the SDK caching guide and the proxy caching guide, plus a blog post announcing the feature. Pairs with BerriAI/litellm#30675
|
Hardened for production deployment in the latest commit: added TLS support ( |
aa0951a to
bcff127
Compare
bcff127 to
f91a588
Compare
Adds setup docs for the new valkey-semantic cache backend (semantic prompt caching on Valkey clusters running valkey-search, including AWS ElastiCache for Valkey) to the SDK caching guide and the proxy caching guide, plus a blog post announcing the feature. Pairs with BerriAI/litellm#30675
Adds setup docs for the new valkey-semantic cache backend (semantic prompt caching on Valkey clusters running valkey-search, including AWS ElastiCache for Valkey) to the SDK caching guide and the proxy caching guide, plus a blog post announcing the feature. Pairs with BerriAI/litellm#30675
f91a588 to
c0198c0
Compare
| @@ -293,7 +349,15 @@ def get_cache_key(self, **kwargs) -> str: | |||
|
|
|||
| combined_kwargs = ModelParamHelper._get_all_llm_api_params() | |||
| litellm_param_kwargs = all_litellm_params | |||
| is_semantic_cache = self._is_semantic_cache() | |||
| scope_excluded_params = ( | |||
| self._SEMANTIC_CACHE_SCOPE_EXCLUDED_PARAMS | |||
| if is_semantic_cache | |||
| else frozenset() | |||
| ) | |||
| for param in kwargs: | |||
| if param in scope_excluded_params: | |||
| continue | |||
| if param in combined_kwargs: | |||
| param_value: Optional[str] = self._get_param_value(param, kwargs) | |||
| if param_value is not None: | |||
| @@ -309,6 +373,9 @@ def get_cache_key(self, **kwargs) -> str: | |||
| param_value = kwargs[param] | |||
| cache_key += f"{str(param)}: {str(param_value)}" | |||
|
|
|||
| if is_semantic_cache: | |||
| cache_key += self._get_semantic_cache_tenant_scope(kwargs) | |||
There was a problem hiding this comment.
Breaking scope-key change invalidates all existing
redis-semantic and qdrant-semantic entries
get_cache_key() now excludes messages, prompt, and input from the hashed key for every semantic cache type — including the pre-existing redis-semantic and qdrant-semantic backends. Any instance that has been running one of those backends will find that every stored entry becomes permanently unreachable after upgrading: the SHA-256 scope tag changes for every document, so all KNN filter lookups miss indefinitely. Dropping the cache silently on upgrade violates the backwards-compatibility rule; the fix should be opt-in (e.g. litellm.semantic_cache_scope_fix = True) so operators can schedule the cold-start window. New valkey-semantic deployments are unaffected since they have no prior entries to lose.
Rule Used: What: avoid backwards-incompatible changes without... (source)
There was a problem hiding this comment.
Intentional, and I think a flag is the wrong call here.
The entries this invalidates were only ever reachable by a byte-identical prompt, because the prompt was baked into the scope key and the KNN filter required an exact key match, so semantic matching never actually ran. The warm cache that goes cold on upgrade therefore only held exact-prompt repeats, which is the single case where a semantic cache adds nothing over a plain exact cache. Keeping the old key format preserves nothing of value.
The impact is also self-healing and bounded. Semantic cache entries are TTL'd and best-effort: an orphaned entry sits until its TTL and expires, and a lookup miss just re-calls the provider, so there is no correctness impact and no data loss, only a one-time cold start for the remainder of the existing TTL. The new key additionally appends the authenticated tenant identity, so it is strictly more isolated than before, never less.
An opt-in flag would default every existing redis-semantic and qdrant-semantic deployment to the broken behavior, so semantic caching keeps silently not working until someone finds the flag, and it adds a second code path and config surface to preserve a cache state with no value. Defaulting to the correct behavior is the better trade. An operator who wants to reclaim the orphaned entries immediately can flush the index or shorten the TTL; otherwise it clears itself within one TTL.
…he scope keys Adds a "valkey-semantic" cache type so semantic prompt caching can run against Valkey clusters (for example AWS ElastiCache for Valkey) using the valkey-search module. The existing "redis-semantic" backend cannot drive valkey-search. RedisVL gates the connection on a RediSearch module version that valkey-search does not report, and its SemanticCache index declares the prompt as a TEXT field, which valkey-search does not implement. ValkeySemanticCache therefore talks to valkey-search directly over redis-py: it builds a vector index from the field types valkey-search supports (TAG for caller scope, VECTOR for the prompt embedding) and runs KNN queries for retrieval. Prompt extraction, embedding generation, and cached-response parsing are reused from RedisSemanticCache since those are backend agnostic. The redis dependency is imported lazily in the cache dispatch so importing litellm without redis installed still works. It also fixes semantic-cache scope keys so similarity matching works across reworded prompts. get_cache_key() hashed messages / prompt / input into the litellm_cache_key that every semantic backend filters its KNN search on, so a paraphrase landed in a different bucket and never matched, even far above the similarity threshold. For semantic cache types the prompt-bearing params are now excluded from the scope key and the server-set tenant identity (user_api_key, team, org) is appended instead, restoring embedding matching within a tenant while keeping cache entries scoped to the authenticated key / team / org. The three semantic backends share this key, so the same change fixes redis-semantic and qdrant-semantic. Connections resolve from VALKEY_HOST / VALKEY_PORT / VALKEY_PASSWORD, falling back to REDIS_* for drop-in compatibility, and passwordless clusters (IAM or no-auth) are supported. Resolves #29121 Fixes #29086
c0198c0 to
ce7a4cb
Compare
Adds setup docs for the new valkey-semantic cache backend (semantic prompt caching on Valkey clusters running valkey-search, including AWS ElastiCache for Valkey) to the SDK caching guide and the proxy caching guide, plus a blog post announcing the feature. Pairs with BerriAI/litellm#30675
…he scope keys (BerriAI#30675) Adds a "valkey-semantic" cache type so semantic prompt caching can run against Valkey clusters (for example AWS ElastiCache for Valkey) using the valkey-search module. The existing "redis-semantic" backend cannot drive valkey-search. RedisVL gates the connection on a RediSearch module version that valkey-search does not report, and its SemanticCache index declares the prompt as a TEXT field, which valkey-search does not implement. ValkeySemanticCache therefore talks to valkey-search directly over redis-py: it builds a vector index from the field types valkey-search supports (TAG for caller scope, VECTOR for the prompt embedding) and runs KNN queries for retrieval. Prompt extraction, embedding generation, and cached-response parsing are reused from RedisSemanticCache since those are backend agnostic. The redis dependency is imported lazily in the cache dispatch so importing litellm without redis installed still works. It also fixes semantic-cache scope keys so similarity matching works across reworded prompts. get_cache_key() hashed messages / prompt / input into the litellm_cache_key that every semantic backend filters its KNN search on, so a paraphrase landed in a different bucket and never matched, even far above the similarity threshold. For semantic cache types the prompt-bearing params are now excluded from the scope key and the server-set tenant identity (user_api_key, team, org) is appended instead, restoring embedding matching within a tenant while keeping cache entries scoped to the authenticated key / team / org. The three semantic backends share this key, so the same change fixes redis-semantic and qdrant-semantic. Connections resolve from VALKEY_HOST / VALKEY_PORT / VALKEY_PASSWORD, falling back to REDIS_* for drop-in compatibility, and passwordless clusters (IAM or no-auth) are supported. Resolves BerriAI#29121 Fixes BerriAI#29086
Relevant issues
Resolves #29121
Docs: BerriAI/litellm-docs#377
Linear ticket
Resolves LIT-3760
Addresses LIT-3860 (semantic cache scope fix; redis/qdrant rollout tracked there)
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Run against a live proxy talking to a real Valkey instance with the valkey-search vector module and real OpenAI APIs.
Valkey (valkey-search bundled):
Proxy config (
litellm_settings.cache_params):Before this change, configuring
type: valkey-semanticis unrecognized; the Cache builds no backend and the proxy crashes on startup:After this change the proxy starts, builds the vector index on valkey-search, and serves a semantic cache hit. Sending the same request twice returns the same response
id; the second call is served from Valkey with no new LLM call:The data landed in valkey-search under the scoped index:
E2E Testing in K8s
Elasticache


Proxy Config
Live semantic scope fix on the managed ElastiCache endpoint, deployed image
litellm-pr-30675-c0198c0(Valkey 9.0 overrediss://): a long prompt, then the same prompt with one word changed (meticulous->careful). The reworded request returns the same responseidin a fraction of the time, and the valkey-search indexnum_docsis unchanged across it, so it is a cache hit rather than a new store. Pre-fix this same reworded request missed with a new id, full latency, and a new document.Semantic scope fix: reworded prompt now hits on all three backends
The scope fix lives in
get_cache_key(), which every semantic backend filters on, so the same change fixes all three. It was verified end to end against each backend's real engine and real OpenAI, by sending a long prompt then the same prompt with one word changed (meticulous->careful). In every case the reworded request returns the same responseidin a fraction of the time with no new entry stored, a real cache hit. Pre-fix each of these produced a new id, full latency, and a new stored entry.valkey-semanticredis-semanticqdrant-semanticRepresentative local run (
valkey-semanticagainstvalkey/valkey-bundle); the managed ElastiCache run is in the E2E Testing in K8s section above:Cross-tenant isolation (two keys do not share a bucket) is covered by
test_semantic_cache_key_isolates_tenants.Type
🆕 New Feature
Changes
Adds a
valkey-semanticcache type for semantic prompt caching on Valkey clusters running the valkey-search module, which is what AWS ElastiCache for Valkey exposes.The existing
redis-semanticbackend cannot be pointed at valkey-search. It is built on RedisVL, which gates the connection on a RediSearch module version that valkey-search does not report, and whoseSemanticCacheindex declares the prompt as aTEXTfield that valkey-search does not implement. Both were confirmed againstvalkey/valkey-bundle: RedisVL raisesRedisModuleVersionErroron connect, and forcing past it fails index creation withInvalid field type for field 'prompt': Unknown argument 'TEXT'.ValkeySemanticCachetalks to valkey-search directly over redis-py instead. It creates the index from the field types valkey-search supports, aTAGfield for caller scope and aVECTORfield (HNSW, cosine) for the prompt embedding, stores each entry as a hash, and retrieves with a KNN query filtered to the caller's scope. Prompt extraction, embedding generation, and cached-response parsing are inherited fromRedisSemanticCache. The scope tag is a sha256 of the litellm cache key so it is always a valid, exact-match TAG token regardless of the key contents. Theredisdependency is imported lazily in the cache dispatch, soimport litellmstill works on a base install without redis.Connections resolve from
VALKEY_HOST/VALKEY_PORT/VALKEY_PASSWORD, falling back toREDIS_*, and passwordless clusters are supported for IAM or no-auth setups. A fullredis_url(includingrediss://for TLS) can be passed for ElastiCache encryption in transit.There is no semantic-caching docs page in this repo to extend, so the configuration example lives in this PR for now.
Semantic cache scope fix
This PR also corrects how the semantic cache scope key is built, which previously made semantic caching behave like an exact-match cache.
get_cache_key()hashedmessages/prompt/inputinto thelitellm_cache_keythat all three semantic backends (redis-semantic,qdrant-semantic,valkey-semantic) filter their KNN search on, so a reworded prompt landed in a different bucket and never matched even when vector similarity was well above the threshold. For semantic cache types the prompt-bearing params are now excluded from the scope key and the server-set tenant identity (user_api_key, team, org) is appended instead, so embedding matching works within a tenant while cache entries stay scoped to the authenticated key / team / org. Because the change lives inget_cache_key()and the backends already filter on that key, it fixesredis-semanticandqdrant-semanticas well. Exact caches keep the prompt in their key and are unchanged.Production configuration (TLS, topology)
Connections support TLS for managed endpoints: pass
ssl: truealongside host and port to build arediss://URL, or pass a fullredis_url. On AWS, vector search requires a node-based ElastiCache for Valkey 8.2+ cluster; a cluster-mode-disabled node group is the supported and recommended target, and a primary with read replicas is fine since only horizontal sharding is unsupported. ElastiCache Serverless does not support vector search. Multi-shard (cluster-mode-enabled) endpoints are rejected with a clear error, since the async client cannot route theFT.*search commands across shards.