From 496c2a55133fa8375c4955d30cb90a4e30804f4a Mon Sep 17 00:00:00 2001 From: yucheng Date: Tue, 15 Sep 2026 08:35:13 +0000 Subject: [PATCH] fix(caching): replay agentic loop follow-up cache hits as plain objects Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/caching/caching_handler.py | 4 +- .../caching/test_caching_handler.py | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index a0ddbdb37ec..50426ea89ea 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -109,7 +109,9 @@ def _is_chat_completion_cached_dict(cached_result: dict) -> bool: def _stream_replay_requested(kwargs: Mapping[str, object]) -> bool: - return kwargs.get("stream", False) is True or converted_stream_requested(kwargs) + if kwargs.get("stream", False) is True: + return True + return converted_stream_requested(kwargs) and not kwargs.get("_agentic_loop_depth") def _should_defer_streaming_cache_hit_callbacks(*, cached_result: object) -> bool: diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index dd826d80208..39018dca41d 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -738,3 +738,45 @@ async def test_converted_stream_cache_hit_replayed_as_plain_object_logs_at_hit_t assert hit is not None and hit.cached_result == cached_message 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 + + +@pytest.mark.asyncio +async def test_agentic_loop_followup_cache_hit_with_converted_stream_marker_replays_as_plain_object(monkeypatch): + import litellm + from litellm.caching.caching import Cache + from litellm.types.utils import CallTypes + + async def acompletion(**kwargs): + return None + + monkeypatch.setattr(litellm, "cache", Cache(type="local")) + kwargs = { + "model": "gpt-5.6", + "messages": [{"role": "user", "content": "run the code"}], + "caching": True, + "stream": False, + "_code_interpreter_interception_converted_stream": True, + "_agentic_loop_depth": 1, + } + await litellm.cache.async_add_cache( + litellm.ModelResponse(choices=[{"message": {"role": "assistant", "content": "done"}}]), **kwargs + ) + 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() + logging_obj.handle_sync_success_callbacks_for_async_calls = MagicMock() + + hit = await handler._async_get_cache( + model="gpt-5.6", + 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 isinstance(hit.cached_result, litellm.ModelResponse) + 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