diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 1d80d6952a4..97f8e59ce17 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -119,12 +119,14 @@ def _is_response_cache_excluded(model: str | None, kwargs: Mapping[str, object]) def _is_chat_completion_cached_dict(cached_result: dict) -> bool: - cached_id: Final = cached_result.get("id") - if isinstance(cached_id, str) and cached_id.startswith("chatcmpl"): - return True + # `object` is checked before the id: the Responses to chat completions bridge + # stores Responses API objects (`object: "response"`) under a `chatcmpl-` id. obj: Final = cached_result.get("object") if isinstance(obj, str): return obj.startswith("chat.completion") + cached_id: Final = cached_result.get("id") + if isinstance(cached_id, str) and cached_id.startswith("chatcmpl"): + return True return "choices" in cached_result diff --git a/tests/unit/caching/test_caching_handler.py b/tests/unit/caching/test_caching_handler.py index 51754c80dd6..e431e6b528f 100644 --- a/tests/unit/caching/test_caching_handler.py +++ b/tests/unit/caching/test_caching_handler.py @@ -1412,6 +1412,12 @@ def test_is_chat_completion_cached_dict(): assert not _is_chat_completion_cached_dict( {"id": "resp_abc", "object": "response", "output": []} ) + # The bridge stores a Responses API object under a chatcmpl- id; `object` decides. + assert not _is_chat_completion_cached_dict( + {"id": "chatcmpl-bridge", "object": "response", "output": []} + ) + # No `object` at all: the chatcmpl- id still marks a chat completion. + assert _is_chat_completion_cached_dict({"id": "chatcmpl-abc"}) def test_convert_cached_aresponses_bridge_chat_completion_stream(): @@ -2097,6 +2103,52 @@ def test_convert_cached_responses_legacy_nonstream_path(): assert result.id == "resp_legacy_nonstream" +def test_convert_cached_responses_bridge_responses_object_keeps_chatcmpl_id(): + """ + Responses to chat completions bridge (for example anthropic/): the stored object is a + Responses API response whose id is the bridge's chatcmpl- id. The cache hit must replay it + as a Responses API response, not rebuild it as a chat completion without `choices`. + """ + from litellm import responses + 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() + ) + cached_result = { + "id": "chatcmpl-bridge-responses-object", + "created_at": int(time.time()), + "status": "completed", + "model": "claude-sonnet-4-5", + "object": "response", + "output": [ + { + "type": "message", + "id": "msg_bridge", + "status": "completed", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "hello", "annotations": []} + ], + } + ], + } + + result = caching_handler._convert_cached_result_to_model_response( + cached_result=cached_result, + call_type=CallTypes.responses.value, + kwargs={"model": "anthropic/claude-sonnet-4-5", "input": "hi", "stream": False}, + logging_obj=_build_logging_obj(CallTypes.responses.value, stream=False), + model="anthropic/claude-sonnet-4-5", + args=(), + ) + + assert isinstance(result, ResponsesAPIResponse) + assert result.id == "chatcmpl-bridge-responses-object" + assert result.output_text == "hello" + + def test_convert_cached_responses_legacy_stream_path(): """Genuine ResponsesAPIResponse dict (no chatcmpl/choices) on stream falls through legacy path.""" from litellm import responses