From d2393040be580b8159dc77a020b160e9690ea8b6 Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:00:29 +0300 Subject: [PATCH 1/8] fix(caching): honor supported_call_types for embeddings and router functions (#41003) --- litellm/caching/caching.py | 14 + litellm/caching/caching_handler.py | 265 ++++++++++++------ .../caching/test_caching_handler.py | 94 +++++++ 3 files changed, 280 insertions(+), 93 deletions(-) diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index d6dd2a073af..08bfb6220c8 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -886,6 +886,20 @@ class Cache: If cache is default_on then this is True If cache is default_off then this is only true when user has opted in to use cache """ + if self.supported_call_types is not None: + candidates: set[str] = set() + call_type = kwargs.get("call_type") or kwargs.get("route_type") + if call_type: + candidates.add(str(call_type)) + candidates.add(str(call_type).lstrip("_")) + original_function = kwargs.get("original_function") + if original_function is not None: + fn_name = getattr(original_function, "__name__", str(original_function)) + candidates.add(fn_name) + candidates.add(fn_name.lstrip("_")) + if candidates and not (candidates & set(self.supported_call_types)): + return False + if self.mode == CacheMode.default_on: return True diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 139dcf058d2..f08c2733576 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -208,9 +208,14 @@ class LLMCachingHandler: """ # Check if caching should be performed BEFORE doing expensive operations if ( - (kwargs.get("caching", None) is None and litellm.cache is not None) or kwargs.get("caching", False) is True - ) and ( - kwargs.get("cache", {}).get("no-cache", False) is not True + ( + (kwargs.get("caching", None) is None and litellm.cache is not None) + or kwargs.get("caching", False) is True + ) + and (kwargs.get("cache", {}).get("no-cache", False) is not True) + and self._is_call_type_supported_by_cache( + original_function=original_function, call_type=call_type, kwargs=kwargs + ) ): # allow users to control returning cached responses from the completion function args = args or () final_embedding_cached_response: EmbeddingResponse | None = None @@ -226,86 +231,85 @@ class LLMCachingHandler: parent_otel_span: Final = _get_parent_otel_span_from_kwargs(kwargs) kwargs["parent_otel_span"] = parent_otel_span - if litellm.cache is not None and self._is_call_type_supported_by_cache(original_function=original_function): - verbose_logger.debug("Checking Async Cache") - cached_result = await self._retrieve_from_cache( + verbose_logger.debug("Checking Async Cache") + cached_result = await self._retrieve_from_cache( + call_type=call_type, + kwargs=kwargs, + args=args, + ) + cache_check_end_time = time.perf_counter() + + if cached_result is not None and not isinstance(cached_result, list): + verbose_logger.debug("Cache Hit!") + cache_hit: Final = True + end_time: Final = datetime.datetime.now() + model, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=model, + custom_llm_provider=kwargs.get("custom_llm_provider", None), + api_base=kwargs.get("api_base", None), + api_key=kwargs.get("api_key", None), + ) + cache_duration_ms: Final = (cache_check_end_time - cache_check_start_time) * 1000 + self._update_litellm_logging_obj_environment( + logging_obj=logging_obj, + model=model, + kwargs=kwargs, + cached_result=cached_result, + is_async=True, + custom_llm_provider=custom_llm_provider, + cache_duration_ms=cache_duration_ms, + ) + + call_type = original_function.__name__ + + cached_result = self._convert_cached_result_to_model_response( + cached_result=cached_result, call_type=call_type, kwargs=kwargs, + logging_obj=logging_obj, + model=model, + custom_llm_provider=kwargs.get("custom_llm_provider", None), args=args, ) - cache_check_end_time = time.perf_counter() - - if cached_result is not None and not isinstance(cached_result, list): - verbose_logger.debug("Cache Hit!") - cache_hit: Final = True - end_time: Final = datetime.datetime.now() - model, custom_llm_provider, _, _ = litellm.get_llm_provider( - model=model, - custom_llm_provider=kwargs.get("custom_llm_provider", None), - api_base=kwargs.get("api_base", None), - api_key=kwargs.get("api_key", None), - ) - cache_duration_ms: Final = (cache_check_end_time - cache_check_start_time) * 1000 - self._update_litellm_logging_obj_environment( + if not _should_defer_streaming_cache_hit_callbacks(kwargs=kwargs): + # LOG SUCCESS + self._async_log_cache_hit_on_callbacks( logging_obj=logging_obj, - model=model, - kwargs=kwargs, cached_result=cached_result, - is_async=True, - custom_llm_provider=custom_llm_provider, - cache_duration_ms=cache_duration_ms, - ) - - call_type = original_function.__name__ - - cached_result = self._convert_cached_result_to_model_response( - cached_result=cached_result, - call_type=call_type, - kwargs=kwargs, - logging_obj=logging_obj, - model=model, - custom_llm_provider=kwargs.get("custom_llm_provider", None), - args=args, - ) - if not _should_defer_streaming_cache_hit_callbacks(kwargs=kwargs): - # LOG SUCCESS - self._async_log_cache_hit_on_callbacks( - logging_obj=logging_obj, - cached_result=cached_result, - start_time=start_time, - end_time=end_time, - cache_hit=cache_hit, - ) - cache_key: Final = ( - self.preset_cache_key - or self.request_kwargs.get("cache_key") - or litellm.cache.get_cache_key(**self.request_kwargs) - ) - if hasattr(cached_result, "_hidden_params"): - cached_result._hidden_params["cache_key"] = cache_key - return CachingHandlerResponse(cached_result=cached_result) - elif ( - call_type == CallTypes.aembedding.value - and cached_result is not None - and isinstance(cached_result, list) - and litellm.cache is not None - and not isinstance(litellm.cache.cache, S3Cache) # s3 doesn't support bulk writing. Exclude. - ): - ( - final_embedding_cached_response, - embedding_all_elements_cache_hit, - ) = self._process_async_embedding_cached_response( - final_embedding_cached_response=final_embedding_cached_response, - cached_result=cached_result, - kwargs=kwargs, - logging_obj=logging_obj, start_time=start_time, - model=model, - ) - return CachingHandlerResponse( - final_embedding_cached_response=final_embedding_cached_response, - embedding_all_elements_cache_hit=embedding_all_elements_cache_hit, + end_time=end_time, + cache_hit=cache_hit, ) + cache_key: Final = ( + self.preset_cache_key + or self.request_kwargs.get("cache_key") + or litellm.cache.get_cache_key(**self.request_kwargs) + ) + if hasattr(cached_result, "_hidden_params"): + cached_result._hidden_params["cache_key"] = cache_key + return CachingHandlerResponse(cached_result=cached_result) + elif ( + call_type == CallTypes.aembedding.value + and cached_result is not None + and isinstance(cached_result, list) + and litellm.cache is not None + and not isinstance(litellm.cache.cache, S3Cache) # s3 doesn't support bulk writing. Exclude. + ): + ( + final_embedding_cached_response, + embedding_all_elements_cache_hit, + ) = self._process_async_embedding_cached_response( + final_embedding_cached_response=final_embedding_cached_response, + cached_result=cached_result, + kwargs=kwargs, + logging_obj=logging_obj, + start_time=start_time, + model=model, + ) + return CachingHandlerResponse( + final_embedding_cached_response=final_embedding_cached_response, + embedding_all_elements_cache_hit=embedding_all_elements_cache_hit, + ) verbose_logger.debug("CACHE RESULT: %s", cached_result) return CachingHandlerResponse( @@ -328,7 +332,7 @@ class LLMCachingHandler: cached_result: Any | None = None # Check if caching should be performed BEFORE doing expensive kwargs copy - if litellm.cache is not None and self._is_call_type_supported_by_cache(original_function=original_function): + if litellm.cache is not None and self._is_call_type_supported_by_cache(original_function=original_function, call_type=call_type, kwargs=kwargs): args = args or () # Now that we confirmed caching will happen, prepare kwargs new_kwargs: Final = kwargs.copy() @@ -343,6 +347,8 @@ class LLMCachingHandler: if new_kwargs.get("stream") is True and "cache_key" not in new_kwargs: new_kwargs["cache_key"] = litellm.cache.get_cache_key(**new_kwargs) self.request_kwargs = _drop_logging_obj_from_kwargs(new_kwargs) + new_kwargs.setdefault("call_type", call_type) + new_kwargs.setdefault("original_function", original_function) print_verbose("Checking Sync Cache") cached_result = litellm.cache.get_cache(**new_kwargs) if cached_result is not None: @@ -747,12 +753,17 @@ class LLMCachingHandler: elif not isinstance(new_kwargs["input"], list): raise ValueError("input must be a string or a list") tasks: Final = [] + aembedding_kwargs = new_kwargs.copy() + aembedding_kwargs.pop("cache_key", None) + aembedding_kwargs.setdefault("call_type", call_type) + aembedding_kwargs.setdefault("original_function", self.original_function) for idx, i in enumerate(new_kwargs["input"]): preset_cache_key = litellm.cache.get_cache_key(**{**new_kwargs, "input": i}) tasks.append( litellm.cache.async_get_cache( cache_key=preset_cache_key, dynamic_cache_object=self.dual_cache, + **aembedding_kwargs, ) ) cached_result = await asyncio.gather(*tasks) @@ -765,6 +776,8 @@ class LLMCachingHandler: request_kwargs: Final = new_kwargs.copy() request_cache_key: Final = _request_cache_key(request_kwargs) request_kwargs.pop("cache_key", None) + request_kwargs.setdefault("call_type", call_type) + request_kwargs.setdefault("original_function", self.original_function) if litellm.cache._supports_async() is True: ## check if dual cache is supported ## self.preset_cache_key = request_cache_key or litellm.cache.get_cache_key(**request_kwargs) @@ -1018,8 +1031,18 @@ class LLMCachingHandler: ) parent_otel_span: Final = _get_parent_otel_span_from_kwargs(new_kwargs) new_kwargs["parent_otel_span"] = parent_otel_span + new_kwargs.setdefault("original_function", original_function) + + call_type: str | None = None + if isinstance(result, litellm.EmbeddingResponse): + call_type = CallTypes.aembedding.value + elif isinstance(result, litellm.ModelResponse): + call_type = new_kwargs.get("call_type") or CallTypes.acompletion.value + # [OPTIONAL] ADD TO CACHE - if self._should_store_result_in_cache(original_function=original_function, kwargs=new_kwargs): + if self._should_store_result_in_cache( + original_function=original_function, kwargs=new_kwargs, call_type=call_type + ): if ( isinstance(result, litellm.ModelResponse) or isinstance(result, litellm.EmbeddingResponse) @@ -1031,9 +1054,13 @@ class LLMCachingHandler: isinstance(result, EmbeddingResponse) and not isinstance(cache.cache, S3Cache) # s3 doesn't support bulk writing. Exclude. ): + embedding_pipeline_kwargs = { + **new_kwargs, + "call_type": call_type or CallTypes.aembedding.value, + } create_cache_write_task( lambda: cache.async_add_cache_pipeline( - result, dynamic_cache_object=self.dual_cache, **new_kwargs + result, dynamic_cache_object=self.dual_cache, **embedding_pipeline_kwargs ) ) else: @@ -1068,23 +1095,44 @@ class LLMCachingHandler: if litellm.cache is None: return - if self._should_store_result_in_cache(original_function=self.original_function, kwargs=new_kwargs): + call_type: str | None = None + if isinstance(result, litellm.EmbeddingResponse): + call_type = CallTypes.embedding.value + elif isinstance(result, litellm.ModelResponse): + call_type = new_kwargs.get("call_type") or CallTypes.completion.value + + new_kwargs.setdefault("original_function", self.original_function) + if call_type is not None: + new_kwargs.setdefault("call_type", call_type) + + if self._should_store_result_in_cache( + original_function=self.original_function, kwargs=new_kwargs, call_type=call_type + ): litellm.cache.add_cache(result, **new_kwargs) return - def _should_store_result_in_cache(self, original_function: Callable, kwargs: dict[str, Any]) -> bool: + def _should_store_result_in_cache( + self, + original_function: Callable | str | None = None, + kwargs: dict[str, Any] | None = None, + call_type: str | None = None, + ) -> bool: """ Helper function to determine if the result should be stored in the cache. Returns: bool: True if the result should be stored in the cache, False otherwise. """ - return ( - (litellm.cache is not None) - and litellm.cache.supported_call_types is not None - and (str(original_function.__name__) in litellm.cache.supported_call_types) - and (kwargs.get("cache", {}).get("no-store", False) is not True) + kwargs = kwargs or {} + if litellm.cache is None: + return False + if kwargs.get("cache", {}).get("no-store", False) is True: + return False + return self._is_call_type_supported_by_cache( + original_function=original_function, + call_type=call_type, + kwargs=kwargs, ) def wrap_streaming_result_for_cache( @@ -1096,7 +1144,9 @@ class LLMCachingHandler: ): return result if litellm.cache is None or not self._should_store_result_in_cache( - original_function=self.original_function, kwargs=self.request_kwargs + original_function=self.original_function, + kwargs=self.request_kwargs, + call_type=call_type, ): return result if not isinstance(result, AsyncIterator): @@ -1109,7 +1159,9 @@ class LLMCachingHandler: def _is_call_type_supported_by_cache( self, - original_function: Callable, + original_function: Callable | str | None = None, + call_type: str | None = None, + kwargs: dict[str, Any] | None = None, ) -> bool: """ Helper function to determine if the call type is supported by the cache. @@ -1121,13 +1173,40 @@ class LLMCachingHandler: Returns: bool: True if the call type is supported by the cache, False otherwise. """ - if ( - litellm.cache is not None - and litellm.cache.supported_call_types is not None - and str(original_function.__name__) in litellm.cache.supported_call_types - ): + if litellm.cache is None: + return False + if litellm.cache.supported_call_types is None: return True - return False + + candidates: set[str] = set() + + if call_type is not None: + val = getattr(call_type, "value", str(call_type)) + candidates.add(str(val)) + candidates.add(str(val).lstrip("_")) + + for fn in (original_function, getattr(self, "original_function", None)): + if fn is not None: + fn_name = getattr(fn, "__name__", str(fn)) + candidates.add(fn_name) + candidates.add(fn_name.lstrip("_")) + + if kwargs: + kw_call_type = kwargs.get("call_type") or kwargs.get("route_type") + if kw_call_type: + kw_val = getattr(kw_call_type, "value", str(kw_call_type)) + candidates.add(str(kw_val)) + candidates.add(str(kw_val).lstrip("_")) + kw_fn = kwargs.get("original_function") + if kw_fn is not None: + kw_fn_name = getattr(kw_fn, "__name__", str(kw_fn)) + candidates.add(kw_fn_name) + candidates.add(kw_fn_name.lstrip("_")) + + if not candidates: + return False + + return bool(candidates & set(litellm.cache.supported_call_types)) async def _add_streaming_response_to_cache(self, processed_chunk: ModelResponse): """ diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index 071b99850f6..fa6a0a2c73f 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -693,3 +693,97 @@ async def test_cache_hit_records_the_looked_up_key_as_the_preset_cache_key(monke assert handler.preset_cache_key is not None assert logging_obj.litellm_params["preset_cache_key"] == handler.preset_cache_key assert hit.cached_result._hidden_params["cache_key"] == handler.preset_cache_key + + +@pytest.mark.asyncio +async def test_supported_call_types_router_prefix_and_filtering(monkeypatch): + """Test that router functions with leading underscores match supported_call_types, and excluded call types are not cached (#41003).""" + import litellm + from litellm.caching.caching import Cache + from litellm.types.utils import CallTypes + + async def _aembedding(**kwargs): + return None + + async def _acompletion(**kwargs): + return None + + # 1. When supported_call_types has completions only (embedding excluded) + cache_no_embed = Cache(type="local", supported_call_types=["completion", "acompletion"]) + monkeypatch.setattr(litellm, "cache", cache_no_embed) + + handler = LLMCachingHandler(original_function=_aembedding, request_kwargs={}, start_time=datetime.now()) + + assert handler._is_call_type_supported_by_cache(original_function=_aembedding, call_type="aembedding") is False + assert handler._is_call_type_supported_by_cache(call_type="aembedding") is False + assert handler._is_call_type_supported_by_cache(original_function=_aembedding) is False + assert handler._should_store_result_in_cache(original_function=_aembedding, call_type="aembedding") is False + assert cache_no_embed.should_use_cache(call_type="aembedding") is False + assert cache_no_embed.should_use_cache(original_function=_aembedding) is False + + # completions should still be supported + assert handler._is_call_type_supported_by_cache(original_function=_acompletion, call_type="acompletion") is True + assert cache_no_embed.should_use_cache(call_type="acompletion") is True + assert cache_no_embed.should_use_cache(original_function=_acompletion) is True + + # 2. When supported_call_types has embeddings included, router's _aembedding should match + cache_with_embed = Cache(type="local", supported_call_types=["embedding", "aembedding"]) + monkeypatch.setattr(litellm, "cache", cache_with_embed) + + assert handler._is_call_type_supported_by_cache(original_function=_aembedding, call_type="aembedding") is True + assert handler._is_call_type_supported_by_cache(original_function=_aembedding) is True + assert handler._should_store_result_in_cache(original_function=_aembedding, call_type="aembedding") is True + assert cache_with_embed.should_use_cache(call_type="aembedding") is True + assert cache_with_embed.should_use_cache(original_function=_aembedding) is True + + +@pytest.mark.asyncio +async def test_async_get_and_set_cache_respects_supported_call_types(monkeypatch): + """Test that _async_get_cache and async_set_cache respect supported_call_types (#41003).""" + import litellm + from litellm.caching.caching import Cache + from litellm.types.utils import CallTypes + + async def _aembedding(**kwargs): + return None + + # Exclude embeddings from supported_call_types + cache = Cache(type="local", supported_call_types=["completion", "acompletion"]) + monkeypatch.setattr(litellm, "cache", cache) + + kwargs = {"model": "text-embedding-3-small", "input": ["hello world"], "caching": True} + handler = LLMCachingHandler(original_function=_aembedding, request_kwargs=kwargs, start_time=datetime.now()) + logging_obj = _build_logging_obj(CallTypes.aembedding.value, stream=False) + logging_obj.async_success_handler = AsyncMock() + + # Get cache should immediately return None because call_type is not supported + res = await handler._async_get_cache( + model="text-embedding-3-small", + original_function=_aembedding, + logging_obj=logging_obj, + start_time=datetime.now(), + call_type=CallTypes.aembedding.value, + kwargs=kwargs, + args=(), + ) + assert res is None + + # Set cache should not write anything to cache + mock_embedding_response = litellm.EmbeddingResponse( + data=[{"embedding": [0.1, 0.2], "index": 0, "object": "embedding"}], + model="text-embedding-3-small", + usage=litellm.Usage(prompt_tokens=2, total_tokens=2), + ) + await handler.async_set_cache( + result=mock_embedding_response, + original_function=_aembedding, + kwargs=kwargs, + ) + + # Allow any background cache write tasks to run if any were scheduled + await asyncio.sleep(0.05) + + # Verify cache remains empty + key = cache.get_cache_key(**kwargs) + assert cache.get_cache(key) is None + From 0325302cbf226ba13995f98768cc0bd96becc2c2 Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:10:03 +0300 Subject: [PATCH 2/8] refactor(caching): make call-type candidate resolution immutable and prevent kwargs key pollution --- litellm/caching/caching.py | 22 ++--- litellm/caching/caching_handler.py | 89 +++++++------------ .../caching/test_caching_handler.py | 58 +++++++++--- 3 files changed, 92 insertions(+), 77 deletions(-) diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index 08bfb6220c8..e20e7ec349c 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -887,16 +887,18 @@ class Cache: If cache is default_off then this is only true when user has opted in to use cache """ if self.supported_call_types is not None: - candidates: set[str] = set() - call_type = kwargs.get("call_type") or kwargs.get("route_type") - if call_type: - candidates.add(str(call_type)) - candidates.add(str(call_type).lstrip("_")) - original_function = kwargs.get("original_function") - if original_function is not None: - fn_name = getattr(original_function, "__name__", str(original_function)) - candidates.add(fn_name) - candidates.add(fn_name.lstrip("_")) + raw_candidates: Final = ( + kwargs.get("call_type"), + kwargs.get("route_type"), + kwargs.get("original_function"), + ) + candidates: Final = frozenset( + variant + for item in raw_candidates + if item is not None + for name in (getattr(item, "value", None) or getattr(item, "__name__", None) or str(item),) + for variant in (name, name.lstrip("_")) + ) if candidates and not (candidates & set(self.supported_call_types)): return False diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index f08c2733576..abf9b68a6cc 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -347,8 +347,6 @@ class LLMCachingHandler: if new_kwargs.get("stream") is True and "cache_key" not in new_kwargs: new_kwargs["cache_key"] = litellm.cache.get_cache_key(**new_kwargs) self.request_kwargs = _drop_logging_obj_from_kwargs(new_kwargs) - new_kwargs.setdefault("call_type", call_type) - new_kwargs.setdefault("original_function", original_function) print_verbose("Checking Sync Cache") cached_result = litellm.cache.get_cache(**new_kwargs) if cached_result is not None: @@ -753,10 +751,7 @@ class LLMCachingHandler: elif not isinstance(new_kwargs["input"], list): raise ValueError("input must be a string or a list") tasks: Final = [] - aembedding_kwargs = new_kwargs.copy() - aembedding_kwargs.pop("cache_key", None) - aembedding_kwargs.setdefault("call_type", call_type) - aembedding_kwargs.setdefault("original_function", self.original_function) + aembedding_kwargs: Final = {k: v for k, v in new_kwargs.items() if k != "cache_key"} for idx, i in enumerate(new_kwargs["input"]): preset_cache_key = litellm.cache.get_cache_key(**{**new_kwargs, "input": i}) tasks.append( @@ -773,11 +768,8 @@ class LLMCachingHandler: if all(result is None for result in cached_result): cached_result = None else: - request_kwargs: Final = new_kwargs.copy() + request_kwargs: Final = {k: v for k, v in new_kwargs.items() if k != "cache_key"} request_cache_key: Final = _request_cache_key(request_kwargs) - request_kwargs.pop("cache_key", None) - request_kwargs.setdefault("call_type", call_type) - request_kwargs.setdefault("original_function", self.original_function) if litellm.cache._supports_async() is True: ## check if dual cache is supported ## self.preset_cache_key = request_cache_key or litellm.cache.get_cache_key(**request_kwargs) @@ -1031,13 +1023,12 @@ class LLMCachingHandler: ) parent_otel_span: Final = _get_parent_otel_span_from_kwargs(new_kwargs) new_kwargs["parent_otel_span"] = parent_otel_span - new_kwargs.setdefault("original_function", original_function) - call_type: str | None = None - if isinstance(result, litellm.EmbeddingResponse): - call_type = CallTypes.aembedding.value - elif isinstance(result, litellm.ModelResponse): - call_type = new_kwargs.get("call_type") or CallTypes.acompletion.value + call_type: Final = ( + CallTypes.aembedding.value + if isinstance(result, litellm.EmbeddingResponse) + else new_kwargs.get("call_type") + ) # [OPTIONAL] ADD TO CACHE if self._should_store_result_in_cache( @@ -1054,13 +1045,9 @@ class LLMCachingHandler: isinstance(result, EmbeddingResponse) and not isinstance(cache.cache, S3Cache) # s3 doesn't support bulk writing. Exclude. ): - embedding_pipeline_kwargs = { - **new_kwargs, - "call_type": call_type or CallTypes.aembedding.value, - } create_cache_write_task( lambda: cache.async_add_cache_pipeline( - result, dynamic_cache_object=self.dual_cache, **embedding_pipeline_kwargs + result, dynamic_cache_object=self.dual_cache, **new_kwargs ) ) else: @@ -1095,15 +1082,11 @@ class LLMCachingHandler: if litellm.cache is None: return - call_type: str | None = None - if isinstance(result, litellm.EmbeddingResponse): - call_type = CallTypes.embedding.value - elif isinstance(result, litellm.ModelResponse): - call_type = new_kwargs.get("call_type") or CallTypes.completion.value - - new_kwargs.setdefault("original_function", self.original_function) - if call_type is not None: - new_kwargs.setdefault("call_type", call_type) + call_type: Final = ( + CallTypes.embedding.value + if isinstance(result, litellm.EmbeddingResponse) + else new_kwargs.get("call_type") + ) if self._should_store_result_in_cache( original_function=self.original_function, kwargs=new_kwargs, call_type=call_type @@ -1124,15 +1107,15 @@ class LLMCachingHandler: Returns: bool: True if the result should be stored in the cache, False otherwise. """ - kwargs = kwargs or {} + kwargs_dict: Final = kwargs or {} if litellm.cache is None: return False - if kwargs.get("cache", {}).get("no-store", False) is True: + if kwargs_dict.get("cache", {}).get("no-store", False) is True: return False return self._is_call_type_supported_by_cache( original_function=original_function, call_type=call_type, - kwargs=kwargs, + kwargs=kwargs_dict, ) def wrap_streaming_result_for_cache( @@ -1178,30 +1161,22 @@ class LLMCachingHandler: if litellm.cache.supported_call_types is None: return True - candidates: set[str] = set() - - if call_type is not None: - val = getattr(call_type, "value", str(call_type)) - candidates.add(str(val)) - candidates.add(str(val).lstrip("_")) - - for fn in (original_function, getattr(self, "original_function", None)): - if fn is not None: - fn_name = getattr(fn, "__name__", str(fn)) - candidates.add(fn_name) - candidates.add(fn_name.lstrip("_")) - - if kwargs: - kw_call_type = kwargs.get("call_type") or kwargs.get("route_type") - if kw_call_type: - kw_val = getattr(kw_call_type, "value", str(kw_call_type)) - candidates.add(str(kw_val)) - candidates.add(str(kw_val).lstrip("_")) - kw_fn = kwargs.get("original_function") - if kw_fn is not None: - kw_fn_name = getattr(kw_fn, "__name__", str(kw_fn)) - candidates.add(kw_fn_name) - candidates.add(kw_fn_name.lstrip("_")) + kwargs_dict: Final = kwargs or {} + raw_candidates: Final = ( + call_type, + original_function, + getattr(self, "original_function", None), + kwargs_dict.get("call_type"), + kwargs_dict.get("route_type"), + kwargs_dict.get("original_function"), + ) + candidates: Final = frozenset( + variant + for item in raw_candidates + if item is not None + for name in (getattr(item, "value", None) or getattr(item, "__name__", None) or str(item),) + for variant in (name, name.lstrip("_")) + ) if not candidates: return False diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index fa6a0a2c73f..335b34548d5 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -697,7 +697,6 @@ async def test_cache_hit_records_the_looked_up_key_as_the_preset_cache_key(monke @pytest.mark.asyncio async def test_supported_call_types_router_prefix_and_filtering(monkeypatch): - """Test that router functions with leading underscores match supported_call_types, and excluded call types are not cached (#41003).""" import litellm from litellm.caching.caching import Cache from litellm.types.utils import CallTypes @@ -708,7 +707,6 @@ async def test_supported_call_types_router_prefix_and_filtering(monkeypatch): async def _acompletion(**kwargs): return None - # 1. When supported_call_types has completions only (embedding excluded) cache_no_embed = Cache(type="local", supported_call_types=["completion", "acompletion"]) monkeypatch.setattr(litellm, "cache", cache_no_embed) @@ -717,29 +715,36 @@ async def test_supported_call_types_router_prefix_and_filtering(monkeypatch): assert handler._is_call_type_supported_by_cache(original_function=_aembedding, call_type="aembedding") is False assert handler._is_call_type_supported_by_cache(call_type="aembedding") is False assert handler._is_call_type_supported_by_cache(original_function=_aembedding) is False + assert handler._is_call_type_supported_by_cache(kwargs={"route_type": "aembedding"}) is False + assert handler._is_call_type_supported_by_cache(kwargs={"call_type": "aembedding"}) is False + assert handler._is_call_type_supported_by_cache(kwargs={"original_function": _aembedding}) is False + assert handler._is_call_type_supported_by_cache() is False assert handler._should_store_result_in_cache(original_function=_aembedding, call_type="aembedding") is False assert cache_no_embed.should_use_cache(call_type="aembedding") is False + assert cache_no_embed.should_use_cache(route_type="aembedding") is False assert cache_no_embed.should_use_cache(original_function=_aembedding) is False - # completions should still be supported assert handler._is_call_type_supported_by_cache(original_function=_acompletion, call_type="acompletion") is True assert cache_no_embed.should_use_cache(call_type="acompletion") is True assert cache_no_embed.should_use_cache(original_function=_acompletion) is True - # 2. When supported_call_types has embeddings included, router's _aembedding should match cache_with_embed = Cache(type="local", supported_call_types=["embedding", "aembedding"]) monkeypatch.setattr(litellm, "cache", cache_with_embed) assert handler._is_call_type_supported_by_cache(original_function=_aembedding, call_type="aembedding") is True assert handler._is_call_type_supported_by_cache(original_function=_aembedding) is True + assert handler._is_call_type_supported_by_cache(kwargs={"route_type": "_aembedding"}) is True assert handler._should_store_result_in_cache(original_function=_aembedding, call_type="aembedding") is True assert cache_with_embed.should_use_cache(call_type="aembedding") is True assert cache_with_embed.should_use_cache(original_function=_aembedding) is True + cache_all = Cache(type="local", supported_call_types=None) + monkeypatch.setattr(litellm, "cache", cache_all) + assert handler._is_call_type_supported_by_cache(original_function=_aembedding, call_type="aembedding") is True + @pytest.mark.asyncio async def test_async_get_and_set_cache_respects_supported_call_types(monkeypatch): - """Test that _async_get_cache and async_set_cache respect supported_call_types (#41003).""" import litellm from litellm.caching.caching import Cache from litellm.types.utils import CallTypes @@ -747,7 +752,6 @@ async def test_async_get_and_set_cache_respects_supported_call_types(monkeypatch async def _aembedding(**kwargs): return None - # Exclude embeddings from supported_call_types cache = Cache(type="local", supported_call_types=["completion", "acompletion"]) monkeypatch.setattr(litellm, "cache", cache) @@ -756,7 +760,6 @@ async def test_async_get_and_set_cache_respects_supported_call_types(monkeypatch logging_obj = _build_logging_obj(CallTypes.aembedding.value, stream=False) logging_obj.async_success_handler = AsyncMock() - # Get cache should immediately return None because call_type is not supported res = await handler._async_get_cache( model="text-embedding-3-small", original_function=_aembedding, @@ -768,7 +771,6 @@ async def test_async_get_and_set_cache_respects_supported_call_types(monkeypatch ) assert res is None - # Set cache should not write anything to cache mock_embedding_response = litellm.EmbeddingResponse( data=[{"embedding": [0.1, 0.2], "index": 0, "object": "embedding"}], model="text-embedding-3-small", @@ -780,10 +782,46 @@ async def test_async_get_and_set_cache_respects_supported_call_types(monkeypatch kwargs=kwargs, ) - # Allow any background cache write tasks to run if any were scheduled await asyncio.sleep(0.05) - # Verify cache remains empty key = cache.get_cache_key(**kwargs) assert cache.get_cache(key) is None + +@pytest.mark.asyncio +async def test_cache_keys_consistent_with_optional_params(monkeypatch): + import litellm + from litellm.caching.caching import Cache + from litellm.types.utils import CallTypes + + async def _acompletion(**kwargs): + return None + + monkeypatch.setattr(litellm, "enable_caching_on_provider_specific_optional_params", True) + cache = Cache(type="local", supported_call_types=["completion", "acompletion"]) + monkeypatch.setattr(litellm, "cache", cache) + + kwargs = {"model": "gpt-4o", "messages": [{"role": "user", "content": "ping"}], "caching": True} + handler = LLMCachingHandler(original_function=_acompletion, request_kwargs=kwargs, start_time=datetime.now()) + logging_obj = _build_logging_obj(CallTypes.acompletion.value, stream=False) + logging_obj.async_success_handler = AsyncMock() + + mock_response = litellm.ModelResponse(choices=[{"message": {"role": "assistant", "content": "pong"}}]) + await handler.async_set_cache( + result=mock_response, + original_function=_acompletion, + kwargs=kwargs, + ) + await asyncio.sleep(0.05) + + hit = await handler._async_get_cache( + model="gpt-4o", + original_function=_acompletion, + logging_obj=logging_obj, + start_time=datetime.now(), + call_type=CallTypes.acompletion.value, + kwargs=kwargs, + args=(), + ) + assert hit is not None and hit.cached_result is not None + From cb283c355bfe85f17d804b32817d3dcb9e7f75c9 Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:14:07 +0300 Subject: [PATCH 3/8] style: apply ruff format to caching_handler and tests --- litellm/caching/caching_handler.py | 12 +++---- .../caching/test_caching_handler.py | 34 +++++-------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index abf9b68a6cc..6e4e0c4a238 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -332,7 +332,9 @@ class LLMCachingHandler: cached_result: Any | None = None # Check if caching should be performed BEFORE doing expensive kwargs copy - if litellm.cache is not None and self._is_call_type_supported_by_cache(original_function=original_function, call_type=call_type, kwargs=kwargs): + if litellm.cache is not None and self._is_call_type_supported_by_cache( + original_function=original_function, call_type=call_type, kwargs=kwargs + ): args = args or () # Now that we confirmed caching will happen, prepare kwargs new_kwargs: Final = kwargs.copy() @@ -1025,9 +1027,7 @@ class LLMCachingHandler: new_kwargs["parent_otel_span"] = parent_otel_span call_type: Final = ( - CallTypes.aembedding.value - if isinstance(result, litellm.EmbeddingResponse) - else new_kwargs.get("call_type") + CallTypes.aembedding.value if isinstance(result, litellm.EmbeddingResponse) else new_kwargs.get("call_type") ) # [OPTIONAL] ADD TO CACHE @@ -1083,9 +1083,7 @@ class LLMCachingHandler: return call_type: Final = ( - CallTypes.embedding.value - if isinstance(result, litellm.EmbeddingResponse) - else new_kwargs.get("call_type") + CallTypes.embedding.value if isinstance(result, litellm.EmbeddingResponse) else new_kwargs.get("call_type") ) if self._should_store_result_in_cache( diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index 335b34548d5..7fd8fe0a6f9 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -232,18 +232,10 @@ def test_combine_usage_handles_none_details(): def test_is_chat_completion_cached_dict(): from litellm.caching.caching_handler import _is_chat_completion_cached_dict - assert _is_chat_completion_cached_dict( - {"id": "chatcmpl-abc", "object": "chat.completion", "choices": []} - ) - assert _is_chat_completion_cached_dict( - {"id": "other", "object": "chat.completion.chunk", "choices": []} - ) - assert _is_chat_completion_cached_dict( - {"id": "no-object", "choices": [{"index": 0}]} - ) - assert not _is_chat_completion_cached_dict( - {"id": "resp_abc", "object": "response", "output": []} - ) + assert _is_chat_completion_cached_dict({"id": "chatcmpl-abc", "object": "chat.completion", "choices": []}) + assert _is_chat_completion_cached_dict({"id": "other", "object": "chat.completion.chunk", "choices": []}) + assert _is_chat_completion_cached_dict({"id": "no-object", "choices": [{"index": 0}]}) + assert not _is_chat_completion_cached_dict({"id": "resp_abc", "object": "response", "output": []}) def _build_logging_obj(call_type: str, stream: bool): @@ -268,9 +260,7 @@ def test_convert_cached_aresponses_bridge_chat_completion_stream(): from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper from litellm.types.utils import CallTypes - caching_handler = LLMCachingHandler( - original_function=aresponses, request_kwargs={}, start_time=datetime.now() - ) + caching_handler = LLMCachingHandler(original_function=aresponses, request_kwargs={}, start_time=datetime.now()) cached_result = { "id": "chatcmpl-bridge-cache-test", "object": "chat.completion", @@ -307,9 +297,7 @@ def test_convert_cached_responses_bridge_chat_completion_nonstream(): from litellm import responses from litellm.types.utils import CallTypes, ModelResponse - caching_handler = LLMCachingHandler( - original_function=responses, request_kwargs={}, start_time=datetime.now() - ) + caching_handler = LLMCachingHandler(original_function=responses, request_kwargs={}, start_time=datetime.now()) cached_result = { "id": "chatcmpl-bridge-nonstream", "object": "chat.completion", @@ -348,9 +336,7 @@ def test_convert_cached_responses_legacy_nonstream_path(): from litellm.types.llms.openai import ResponsesAPIResponse from litellm.types.utils import CallTypes - caching_handler = LLMCachingHandler( - original_function=responses, request_kwargs={}, start_time=datetime.now() - ) + caching_handler = LLMCachingHandler(original_function=responses, request_kwargs={}, start_time=datetime.now()) cached_result = { "id": "resp_legacy_nonstream", "created_at": int(time.time()), @@ -395,9 +381,7 @@ def test_convert_cached_responses_legacy_stream_path(): ) from litellm.types.utils import CallTypes - caching_handler = LLMCachingHandler( - original_function=responses, request_kwargs={}, start_time=datetime.now() - ) + caching_handler = LLMCachingHandler(original_function=responses, request_kwargs={}, start_time=datetime.now()) cached_result = { "id": "resp_legacy_stream", "created_at": int(time.time()), @@ -699,7 +683,6 @@ async def test_cache_hit_records_the_looked_up_key_as_the_preset_cache_key(monke async def test_supported_call_types_router_prefix_and_filtering(monkeypatch): import litellm from litellm.caching.caching import Cache - from litellm.types.utils import CallTypes async def _aembedding(**kwargs): return None @@ -824,4 +807,3 @@ async def test_cache_keys_consistent_with_optional_params(monkeypatch): args=(), ) assert hit is not None and hit.cached_result is not None - From 62a81d4205ab9581f7498b48c37fb4c383d55e2b Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:17:12 +0300 Subject: [PATCH 4/8] fix(caching): use authoritative call_type with ordered fallback to prevent restriction bypass --- litellm/caching/caching.py | 28 ++++++++-------- litellm/caching/caching_handler.py | 31 +++++++++--------- .../caching/test_caching_handler.py | 32 +++++++++++++++++++ 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index e20e7ec349c..dae74e5bbff 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -887,20 +887,22 @@ class Cache: If cache is default_off then this is only true when user has opted in to use cache """ if self.supported_call_types is not None: - raw_candidates: Final = ( - kwargs.get("call_type"), - kwargs.get("route_type"), - kwargs.get("original_function"), + raw_source: Final = ( + kwargs.get("call_type") + if kwargs.get("call_type") is not None + else ( + kwargs.get("route_type") + if kwargs.get("route_type") is not None + else kwargs.get("original_function") + ) ) - candidates: Final = frozenset( - variant - for item in raw_candidates - if item is not None - for name in (getattr(item, "value", None) or getattr(item, "__name__", None) or str(item),) - for variant in (name, name.lstrip("_")) - ) - if candidates and not (candidates & set(self.supported_call_types)): - return False + if raw_source is not None: + val_name: Final = ( + getattr(raw_source, "value", None) or getattr(raw_source, "__name__", None) or str(raw_source) + ) + candidates: Final = frozenset((val_name, val_name.lstrip("_"))) + if not (candidates & set(self.supported_call_types)): + return False if self.mode == CacheMode.default_on: return True diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 6e4e0c4a238..a4e414274bd 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -1160,25 +1160,24 @@ class LLMCachingHandler: return True kwargs_dict: Final = kwargs or {} - raw_candidates: Final = ( - call_type, - original_function, - getattr(self, "original_function", None), - kwargs_dict.get("call_type"), - kwargs_dict.get("route_type"), - kwargs_dict.get("original_function"), + raw_source: Final = ( + call_type + if call_type is not None + else ( + original_function + if original_function is not None + else ( + getattr(self, "original_function", None) + if getattr(self, "original_function", None) is not None + else kwargs_dict.get("route_type") or kwargs_dict.get("call_type") + ) + ) ) - candidates: Final = frozenset( - variant - for item in raw_candidates - if item is not None - for name in (getattr(item, "value", None) or getattr(item, "__name__", None) or str(item),) - for variant in (name, name.lstrip("_")) - ) - - if not candidates: + if raw_source is None: return False + val_name: Final = getattr(raw_source, "value", None) or getattr(raw_source, "__name__", None) or str(raw_source) + candidates: Final = frozenset((val_name, val_name.lstrip("_"))) return bool(candidates & set(litellm.cache.supported_call_types)) async def _add_streaming_response_to_cache(self, processed_chunk: ModelResponse): diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index 7fd8fe0a6f9..3e59fc02534 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -807,3 +807,35 @@ async def test_cache_keys_consistent_with_optional_params(monkeypatch): args=(), ) assert hit is not None and hit.cached_result is not None + + +@pytest.mark.asyncio +async def test_call_type_restriction_cannot_be_bypassed_via_kwargs(monkeypatch): + import litellm + from litellm.caching.caching import Cache + + async def _aembedding(**kwargs): + return None + + cache = Cache(type="local", supported_call_types=["completion", "acompletion"]) + monkeypatch.setattr(litellm, "cache", cache) + + handler = LLMCachingHandler(original_function=_aembedding, request_kwargs={}, start_time=datetime.now()) + + assert ( + handler._is_call_type_supported_by_cache( + original_function=_aembedding, + call_type="aembedding", + kwargs={"call_type": "acompletion"}, + ) + is False + ) + assert ( + handler._should_store_result_in_cache( + original_function=_aembedding, + call_type="aembedding", + kwargs={"call_type": "acompletion"}, + ) + is False + ) + assert cache.should_use_cache(call_type="aembedding", route_type="acompletion") is False From 63fbd57f619f5e3c5bf6ea4eb5e098cbcde3f8ec Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:27:34 +0300 Subject: [PATCH 5/8] fix(caching): retain explicit cache guard for async reads --- litellm/caching/caching_handler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index a4e414274bd..0e7a1d58f0f 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -208,10 +208,8 @@ class LLMCachingHandler: """ # Check if caching should be performed BEFORE doing expensive operations if ( - ( - (kwargs.get("caching", None) is None and litellm.cache is not None) - or kwargs.get("caching", False) is True - ) + litellm.cache is not None + and (kwargs.get("caching", None) is None or kwargs.get("caching", False) is True) and (kwargs.get("cache", {}).get("no-cache", False) is not True) and self._is_call_type_supported_by_cache( original_function=original_function, call_type=call_type, kwargs=kwargs From 2cde09d29ee91d1000d4cc6ad8d699b391de6355 Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:13:47 +0300 Subject: [PATCH 6/8] fix(caching): preserve explicit cache keys in async lookups --- litellm/caching/caching_handler.py | 2 +- .../caching/test_caching_handler.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 0e7a1d58f0f..198660be139 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -768,8 +768,8 @@ class LLMCachingHandler: if all(result is None for result in cached_result): cached_result = None else: + request_cache_key: Final = _request_cache_key(new_kwargs) request_kwargs: Final = {k: v for k, v in new_kwargs.items() if k != "cache_key"} - request_cache_key: Final = _request_cache_key(request_kwargs) if litellm.cache._supports_async() is True: ## check if dual cache is supported ## self.preset_cache_key = request_cache_key or litellm.cache.get_cache_key(**request_kwargs) diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index 3e59fc02534..807cdaae611 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -1,6 +1,7 @@ import asyncio import json import time +from typing import Final from unittest.mock import MagicMock, patch import httpx @@ -839,3 +840,31 @@ async def test_call_type_restriction_cannot_be_bypassed_via_kwargs(monkeypatch): is False ) assert cache.should_use_cache(call_type="aembedding", route_type="acompletion") is False + + +@pytest.mark.asyncio +@pytest.mark.parametrize("supports_async", [True, False]) +async def test_async_lookup_preserves_explicit_cache_partition(monkeypatch, supports_async: bool): + import litellm + from litellm.caching.caching import Cache + + class CacheWithSelectedBackend(Cache): + def _supports_async(self) -> bool: + return supports_async + + cache: Final = CacheWithSelectedBackend(type="local") + monkeypatch.setattr(litellm, "cache", cache) + request: Final = {"model": "test-model", "messages": [{"role": "user", "content": "hello"}]} + derived_key: Final = cache.get_cache_key(**request) + cache.add_cache({"partition": "derived"}, cache_key=derived_key, **request) + cache.add_cache({"partition": "isolated"}, cache_key="isolated-key", **request) + handler: Final = LLMCachingHandler( + original_function=litellm.acompletion, request_kwargs=request, start_time=datetime.now() + ) + + hit: Final = await handler._retrieve_from_cache( + call_type="acompletion", kwargs={**request, "cache_key": "isolated-key"}, args=() + ) + + assert hit is not None and hit["partition"] == "isolated" + assert handler.preset_cache_key == "isolated-key" From 900b222f83bdffb07e3f8d4cba13bb2fe8bb87a5 Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:01:05 +0300 Subject: [PATCH 7/8] fix(caching): expose the shared call type matcher Signed-off-by: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> --- litellm/caching/caching.py | 4 ++-- litellm/caching/caching_handler.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index b35ea2d5db6..89208159bf0 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -94,7 +94,7 @@ def _request_message_count(kwargs: Mapping[str, object]) -> int: return sum(1 for item in items if _is_conversation_item(item)) -def _is_cache_call_type_supported(call_type: str, supported_call_types: Sequence[str] | None) -> bool: +def is_cache_call_type_supported(call_type: str, supported_call_types: Sequence[str] | None) -> bool: if supported_call_types is None: return True normalised_call_type: Final = call_type.lstrip("_") @@ -994,7 +994,7 @@ class Cache: if raw_source is not None: raw_name: Final[object] = getattr(raw_source, "value", None) or getattr(raw_source, "__name__", None) call_type_name: Final = raw_name if isinstance(raw_name, str) else str(raw_source) - if not _is_cache_call_type_supported(call_type_name, self.supported_call_types): + if not is_cache_call_type_supported(call_type_name, self.supported_call_types): return False if self.max_messages is not None and _request_message_count(kwargs) > self.max_messages: return False diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 44ee31d727c..38c4b9cb391 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -27,7 +27,7 @@ import litellm from litellm._internal_context import is_internal_call, post_response_phase from litellm._logging import print_verbose, verbose_logger from litellm.caching import InMemoryCache -from litellm.caching.caching import S3Cache, _is_cache_call_type_supported, response_cache_phase +from litellm.caching.caching import S3Cache, is_cache_call_type_supported, response_cache_phase from litellm.constants import CACHE_WRITE_SHUTDOWN_FLUSH_TIMEOUT_SECONDS, RESPONSE_CACHE_EXCLUDED_PROVIDERS from litellm.litellm_core_utils.hidden_params import get_hidden_params from litellm.litellm_core_utils.llm_response_utils.response_metadata import ( @@ -1292,7 +1292,7 @@ class LLMCachingHandler: """ if litellm.cache is None: return False - return _is_cache_call_type_supported(original_function.__name__, litellm.cache.supported_call_types) + return is_cache_call_type_supported(original_function.__name__, litellm.cache.supported_call_types) async def add_streaming_response_to_cache(self, processed_chunk: ModelResponse) -> None: """ From bc6029d56785c1b00c650b9fb02ef80925f4eec8 Mon Sep 17 00:00:00 2001 From: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:29:59 +0300 Subject: [PATCH 8/8] fix(caching): keep unrestricted calls within cache-safe types Signed-off-by: Yusuf Efe <120668197+yuefdev@users.noreply.github.com> --- litellm/caching/caching.py | 34 ++++++++++------------ tests/unit/caching/test_caching_handler.py | 28 ++++++++++++++---- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index 89208159bf0..f92c6fbc85d 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -95,13 +95,14 @@ def _request_message_count(kwargs: Mapping[str, object]) -> int: def is_cache_call_type_supported(call_type: str, supported_call_types: Sequence[str] | None) -> bool: - if supported_call_types is None: - return True + allowed_call_types: Final = ( + DEFAULT_CACHING_SUPPORTED_CALL_TYPES if supported_call_types is None else supported_call_types + ) normalised_call_type: Final = call_type.lstrip("_") covering_call_types: Final = ( ("aresponses", "responses") if normalised_call_type == "aresponses" else (normalised_call_type,) ) - return any(name in supported_call_types for name in covering_call_types) + return any(name in allowed_call_types for name in covering_call_types) class Cache: @@ -205,7 +206,7 @@ class Cache: gcs_path (str, optional): Folder path inside the bucket to store cache files. # Common Cache Args - supported_call_types (list, optional): List of call types to cache for. Defaults to cache == on for all call types. + supported_call_types (list, optional): Call types to cache. None uses DEFAULT_CACHING_SUPPORTED_CALL_TYPES. **kwargs: Additional keyword arguments for redis.Redis() cache Raises: @@ -981,21 +982,16 @@ class Cache: If cache is default_off then this is only true when user has opted in to use cache Always False once the request carries more than `max_messages` messages """ - if self.supported_call_types is not None: - raw_source: Final = ( - kwargs.get("call_type") - if kwargs.get("call_type") is not None - else ( - kwargs.get("route_type") - if kwargs.get("route_type") is not None - else kwargs.get("original_function") - ) - ) - if raw_source is not None: - raw_name: Final[object] = getattr(raw_source, "value", None) or getattr(raw_source, "__name__", None) - call_type_name: Final = raw_name if isinstance(raw_name, str) else str(raw_source) - if not is_cache_call_type_supported(call_type_name, self.supported_call_types): - return False + raw_source: Final = ( + kwargs.get("call_type") + if kwargs.get("call_type") is not None + else (kwargs.get("route_type") if kwargs.get("route_type") is not None else kwargs.get("original_function")) + ) + if raw_source is not None: + raw_name: Final[object] = getattr(raw_source, "value", None) or getattr(raw_source, "__name__", None) + call_type_name: Final = raw_name if isinstance(raw_name, str) else str(raw_source) + if not is_cache_call_type_supported(call_type_name, self.supported_call_types): + return False if self.max_messages is not None and _request_message_count(kwargs) > self.max_messages: return False if self.mode == CacheMode.default_on: diff --git a/tests/unit/caching/test_caching_handler.py b/tests/unit/caching/test_caching_handler.py index a5a58a5af50..b48400d1960 100644 --- a/tests/unit/caching/test_caching_handler.py +++ b/tests/unit/caching/test_caching_handler.py @@ -3279,16 +3279,34 @@ def test_call_type_filter_preserves_responses_alias(monkeypatch: pytest.MonkeyPa assert handler.should_store_result_in_cache(original_function=litellm.aresponses, kwargs={}) is True -@pytest.mark.parametrize("allow_all", [True, False]) -def test_unrestricted_and_empty_call_type_lists(monkeypatch: pytest.MonkeyPatch, allow_all: bool) -> None: - cache: Final = Cache(type="local", supported_call_types=None if allow_all else []) +@pytest.mark.parametrize("allow_inference", [True, False]) +def test_none_and_empty_call_type_lists(monkeypatch: pytest.MonkeyPatch, allow_inference: bool) -> None: + cache: Final = Cache(type="local", supported_call_types=None if allow_inference else []) monkeypatch.setattr(litellm, "cache", cache) handler: Final = LLMCachingHandler( original_function=Router._aembedding, request_kwargs={}, start_time=datetime(2026, 1, 1) ) - assert cache.should_use_cache(call_type="_aembedding") is allow_all - assert handler.should_store_result_in_cache(original_function=Router._aembedding, kwargs={}) is allow_all + assert cache.should_use_cache(call_type="_aembedding") is allow_inference + assert handler.should_store_result_in_cache(original_function=Router._aembedding, kwargs={}) is allow_inference + + +def test_none_call_types_do_not_read_or_store_file_listings(monkeypatch: pytest.MonkeyPatch) -> None: + cache: Final = Cache(type="local", supported_call_types=None) + monkeypatch.setattr(litellm, "cache", cache) + handler: Final = LLMCachingHandler( + original_function=litellm.afile_list, request_kwargs={}, start_time=datetime(2026, 1, 1) + ) + response: Final = {"object": "list", "data": [{"id": "private-file", "object": "file"}]} + + handler.sync_set_cache(result=response, kwargs={"cache_key": "file-list"}) + assert cache.get_cache(cache_key="file-list") is None + + cache.add_cache(response, cache_key="existing") + assert cache.get_cache(cache_key="existing", call_type="afile_list") is None + + cache.add_cache(response, cache_key="direct-file-list", call_type="afile_list") + assert cache.get_cache(cache_key="direct-file-list") is None def test_call_type_filter_preserves_message_limit_and_opt_in() -> None: