From 8cb52ce0bbcae4d1e421796224543a4c118fa41f Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Fri, 1 May 2026 22:15:03 -0700 Subject: [PATCH] fix(caching): handle stale isolated Redis semantic index --- litellm/caching/redis_semantic_cache.py | 38 +++++++--- .../caching/test_redis_semantic_cache.py | 73 +++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/litellm/caching/redis_semantic_cache.py b/litellm/caching/redis_semantic_cache.py index 85d3a4f1b4..27538530bc 100644 --- a/litellm/caching/redis_semantic_cache.py +++ b/litellm/caching/redis_semantic_cache.py @@ -128,6 +128,9 @@ class RedisSemanticCache(BaseCache): redis_url: str, cache_vectorizer: Any, ) -> Any: + def _is_schema_mismatch(exc: ValueError) -> bool: + return "schema does not match" in str(exc) + try: return semantic_cache_cls( name=index_name, @@ -138,7 +141,7 @@ class RedisSemanticCache(BaseCache): overwrite=False, ) except ValueError as exc: - if "schema does not match" not in str(exc): + if not _is_schema_mismatch(exc): raise isolated_index_name = f"{index_name}_isolated" @@ -146,14 +149,31 @@ class RedisSemanticCache(BaseCache): "Redis semantic-cache existing index schema is not isolated; " f"using isolated index - {isolated_index_name}" ) - return semantic_cache_cls( - name=isolated_index_name, - redis_url=redis_url, - vectorizer=cache_vectorizer, - distance_threshold=self.distance_threshold, - filterable_fields=[self.CACHE_KEY_FILTERABLE_FIELD], - overwrite=False, - ) + try: + return semantic_cache_cls( + name=isolated_index_name, + redis_url=redis_url, + vectorizer=cache_vectorizer, + distance_threshold=self.distance_threshold, + filterable_fields=[self.CACHE_KEY_FILTERABLE_FIELD], + overwrite=False, + ) + except ValueError as isolated_exc: + if not _is_schema_mismatch(isolated_exc): + raise + + print_verbose( + "Redis semantic-cache isolated index schema is stale; " + f"recreating isolated index - {isolated_index_name}" + ) + return semantic_cache_cls( + name=isolated_index_name, + redis_url=redis_url, + vectorizer=cache_vectorizer, + distance_threshold=self.distance_threshold, + filterable_fields=[self.CACHE_KEY_FILTERABLE_FIELD], + overwrite=True, + ) def _get_cache_filters(self, key: str) -> Dict[str, str]: return {self.CACHE_KEY_FIELD_NAME: str(key)} diff --git a/tests/test_litellm/caching/test_redis_semantic_cache.py b/tests/test_litellm/caching/test_redis_semantic_cache.py index f5c84c9d3e..02b06bed62 100644 --- a/tests/test_litellm/caching/test_redis_semantic_cache.py +++ b/tests/test_litellm/caching/test_redis_semantic_cache.py @@ -232,6 +232,79 @@ def test_redis_semantic_cache_uses_isolated_index_for_old_schema(monkeypatch): ] +def test_redis_semantic_cache_overwrites_stale_isolated_index(monkeypatch): + fallback_cache_mock = MagicMock() + semantic_cache_mock = MagicMock( + side_effect=[ + ValueError("Existing index schema does not match"), + ValueError("Existing index schema does not match"), + fallback_cache_mock, + ] + ) + custom_vectorizer_mock = MagicMock() + + with patch.dict( + "sys.modules", + { + "redisvl.extensions.llmcache": MagicMock(SemanticCache=semantic_cache_mock), + "redisvl.utils.vectorize": MagicMock( + CustomTextVectorizer=custom_vectorizer_mock + ), + }, + ): + from litellm.caching.redis_semantic_cache import RedisSemanticCache + + monkeypatch.setenv("REDIS_HOST", "localhost") + monkeypatch.setenv("REDIS_PORT", "6379") + monkeypatch.setenv("REDIS_PASSWORD", "test_password") + + redis_semantic_cache = RedisSemanticCache( + similarity_threshold=0.8, + index_name="existing_index", + ) + + assert redis_semantic_cache.llmcache is fallback_cache_mock + assert ( + semantic_cache_mock.call_args_list[2].kwargs["name"] + == "existing_index_isolated" + ) + assert semantic_cache_mock.call_args_list[2].kwargs["overwrite"] is True + assert semantic_cache_mock.call_args_list[2].kwargs["filterable_fields"] == [ + RedisSemanticCache.CACHE_KEY_FILTERABLE_FIELD + ] + + +def test_redis_semantic_cache_reraises_unexpected_isolated_index_error(monkeypatch): + semantic_cache_mock = MagicMock( + side_effect=[ + ValueError("Existing index schema does not match"), + ValueError("connection failed"), + ] + ) + custom_vectorizer_mock = MagicMock() + + with patch.dict( + "sys.modules", + { + "redisvl.extensions.llmcache": MagicMock(SemanticCache=semantic_cache_mock), + "redisvl.utils.vectorize": MagicMock( + CustomTextVectorizer=custom_vectorizer_mock + ), + }, + ): + from litellm.caching.redis_semantic_cache import RedisSemanticCache + + monkeypatch.setenv("REDIS_HOST", "localhost") + monkeypatch.setenv("REDIS_PORT", "6379") + monkeypatch.setenv("REDIS_PASSWORD", "test_password") + + with pytest.raises(ValueError, match="connection failed"): + RedisSemanticCache( + similarity_threshold=0.8, + index_name="existing_index", + ) + + def test_redis_semantic_cache_reraises_unexpected_index_error(): from litellm.caching.redis_semantic_cache import RedisSemanticCache