From c348625986a89840f79f7ebd7dc6bef51806930f Mon Sep 17 00:00:00 2001 From: Tanvir Alam Date: Mon, 21 Sep 2026 00:28:35 -0400 Subject: [PATCH] fix(caching): tolerate missing fastapi on SDK cache hits Cache-hit callbacks imported Anthropic response_cache, which pulls in litellm.proxy (fastapi). Plain pip install litellm does not include the proxy extra, so the second completion crashed with ModuleNotFoundError. Guard that optional import so chat/text cache hits keep working (#42195). --- litellm/caching/caching_handler.py | 22 +++++++++++----- .../caching/test_caching_handler.py | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 50426ea89ea..68680a76316 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -124,15 +124,25 @@ def _should_defer_streaming_cache_hit_callbacks(*, cached_result: object) -> boo handlers when the stream finishes; firing them here too would double-count spend and callback records. A plain (non-stream) replay logs here, since nothing else will. + + The Anthropic Messages cache helper imports ``litellm.proxy`` (fastapi). That is + only available with the ``proxy`` extra, so a missing import must not break + plain SDK cache hits (#42195). """ - from litellm.llms.anthropic.experimental_pass_through.messages.response_cache import ( - CachedAnthropicMessagesStreamIterator, - ) from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator - return isinstance( - cached_result, (CustomStreamWrapper, BaseResponsesAPIStreamingIterator, CachedAnthropicMessagesStreamIterator) - ) + streaming_types: tuple[type, ...] = (CustomStreamWrapper, BaseResponsesAPIStreamingIterator) + try: + from litellm.llms.anthropic.experimental_pass_through.messages.response_cache import ( + CachedAnthropicMessagesStreamIterator, + ) + except ImportError: + # response_cache -> streaming_iterator -> litellm.proxy (fastapi / orjson). + pass + else: + streaming_types = (*streaming_types, CachedAnthropicMessagesStreamIterator) + + return isinstance(cached_result, streaming_types) def _prompt_tokens_details_as_mapping(details: "PromptTokensDetailsWrapper") -> Mapping[str, object]: diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index 39018dca41d..cab1600a004 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -780,3 +780,29 @@ async def test_agentic_loop_followup_cache_hit_with_converted_stream_marker_repl assert hit.cached_result.choices[0].message.content == "done" logging_obj.handle_sync_success_callbacks_for_async_calls.assert_called_once() assert logging_obj.handle_sync_success_callbacks_for_async_calls.call_args.kwargs["cache_hit"] is True + + +def test_should_defer_streaming_cache_hit_callbacks_without_proxy_extra(): + """Regression for #42195: cache-hit path must not require fastapi/proxy extras. + + ``response_cache`` imports proxy modules. On a plain ``pip install litellm`` + those deps are absent; marking the module as missing must not raise, and a + non-stream cached result must still return False. + """ + import sys + + from litellm.caching.caching_handler import _should_defer_streaming_cache_hit_callbacks + + mod_name = "litellm.llms.anthropic.experimental_pass_through.messages.response_cache" + had_module = mod_name in sys.modules + saved = sys.modules.get(mod_name) + # PEP 302: sys.modules[name] = None makes the next import raise ImportError. + sys.modules[mod_name] = None + try: + assert _should_defer_streaming_cache_hit_callbacks(cached_result={"choices": []}) is False + finally: + if had_module: + sys.modules[mod_name] = saved + else: + sys.modules.pop(mod_name, None) +