From 655dff8dd235b19c01e55e983b025f4fa37d4456 Mon Sep 17 00:00:00 2001 From: Fazeel Usmani Date: Mon, 26 Oct 2026 18:40:43 +0530 Subject: [PATCH 1/3] fix(responses): reject a previous_response_id the chat bridge cannot replay --- .../handler.py | 7 ++ .../transformation.py | 9 ++ .../test_handler.py | 84 ++++++++++++++++--- .../test_litellm_completion_responses.py | 1 + 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/handler.py b/litellm/responses/litellm_completion_transformation/handler.py index c9d800421f7..bd5aea8e1e9 100644 --- a/litellm/responses/litellm_completion_transformation/handler.py +++ b/litellm/responses/litellm_completion_transformation/handler.py @@ -73,6 +73,13 @@ class LiteLLMCompletionTransformationHandler: **kwargs, ) + if previous_response_client_ids: + raise litellm.BadRequestError( + message="previous_response_id on the chat completions bridge is only supported by litellm.aresponses()", + model=model, + llm_provider=custom_llm_provider or "", + ) + completion_args: Final = {} completion_args.update(kwargs) completion_args.update(litellm_completion_request) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index a9c7818b577..9d1db5cd2ce 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -1223,6 +1223,15 @@ class LiteLLMCompletionResponsesConfig: _messages: Final = litellm_completion_request.get("messages") or [] route: Final = CompletionRoute(model=model, custom_llm_provider=provider) session_history: Final = chat_completion_session.get("messages") or [] + if previous_response_id and not session_history and not input_replaces_history: + raise litellm.BadRequestError( + message=( + "previous_response_id was not found. The chat completions bridge rebuilds earlier turns from " + "spend logs, so they must be written to a database with store_prompts_in_spend_logs enabled" + ), + model=model, + llm_provider=provider or "", + ) stored_messages: Final = LiteLLMCompletionResponsesConfig._with_compaction_blocks_the_route_takes( _instruction_messages(session_history) if input_replaces_history else session_history, route, diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index d731bf5f41c..b18ce2d21ca 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -22,7 +22,7 @@ import httpx import pytest import litellm -from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler +from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.responses.litellm_completion_transformation.handler import ( LiteLLMCompletionTransformationHandler, ) @@ -209,15 +209,19 @@ async def test_bridged_follow_up_turn_keeps_the_addressed_response_id_off_the_pr client: Final = AsyncHTTPHandler() client.client = httpx.AsyncClient(transport=httpx.MockTransport(provider)) - response = await litellm.aresponses( - model="azure_ai/claude-sonnet-4-6", - api_base="https://fake-foundry-resource.services.ai.azure.com", - api_key="fake-api-key", - input="Double it", - previous_response_id="resp_turn_one", - client=client, - **{ADDRESSED_RESPONSE_ID_FIELD: "resp_turn_one"}, - ) + with patch( + "litellm.proxy.proxy_server.prisma_client", + SimpleNamespace(db=_StoredTurns((_stored_mcp_turn("resp_turn_one"),))), + ): + response = await litellm.aresponses( + model="azure_ai/claude-sonnet-4-6", + api_base="https://fake-foundry-resource.services.ai.azure.com", + api_key="fake-api-key", + input="Double it", + previous_response_id="resp_turn_one", + client=client, + **{ADDRESSED_RESPONSE_ID_FIELD: "resp_turn_one"}, + ) assert provider.request_body is not None, "the bridged turn never reached the provider" assert ADDRESSED_RESPONSE_ID_FIELD not in provider.request_body, ( @@ -310,3 +314,63 @@ async def test_bridged_next_turn_replays_a_turn_stored_under_the_id_the_client_h ("assistant", json.dumps([{"type": "text", "text": "Noted."}])), ("user", json.dumps([{"type": "text", "text": "Double it"}])), ] + + +def _turn_stored_without_prompts(request_id: str) -> Mapping[str, object]: + return { + "request_id": request_id, + "call_type": "aresponses", + "session_id": "turn-one-session", + "proxy_server_request": "{}", + "response": "{}", + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "prisma_client", + [ + pytest.param(None, id="no_database"), + pytest.param( + SimpleNamespace(db=_StoredTurns((_turn_stored_without_prompts("resp_turn_one"),))), + id="prompts_not_stored", + ), + ], +) +async def test_bridged_follow_up_to_a_response_it_cannot_replay_is_rejected(prisma_client: SimpleNamespace | None): + provider: Final = _RecordingAnthropicHandler(_ANTHROPIC_MESSAGE_PAYLOAD) + client: Final = AsyncHTTPHandler() + client.client = httpx.AsyncClient(transport=httpx.MockTransport(provider)) + + with ( + patch("litellm.proxy.proxy_server.prisma_client", prisma_client), + pytest.raises(litellm.BadRequestError, match="previous_response_id was not found"), + ): + await litellm.aresponses( + model="azure_ai/claude-sonnet-4-6", + api_base="https://fake-foundry-resource.services.ai.azure.com", + api_key="fake-api-key", + input="Double it", + previous_response_id="resp_turn_one", + client=client, + ) + + assert provider.request_body is None + + +def test_sync_bridged_follow_up_is_rejected_instead_of_dropping_the_history(): + provider: Final = _RecordingAnthropicHandler(_ANTHROPIC_MESSAGE_PAYLOAD) + client: Final = HTTPHandler() + client.client = httpx.Client(transport=httpx.MockTransport(provider)) + + with pytest.raises(litellm.BadRequestError, match="only supported by litellm.aresponses"): + litellm.responses( + model="azure_ai/claude-sonnet-4-6", + api_base="https://fake-foundry-resource.services.ai.azure.com", + api_key="fake-api-key", + input="Double it", + previous_response_id="resp_turn_one", + client=client, + ) + + assert provider.request_body is None diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 023d0ea26fb..119590a1cc4 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -6808,6 +6808,7 @@ def test_compaction_replay_invents_no_call_for_an_output_whose_call_was_never_st litellm_completion_request=chat_request, instructions=None, session_lookup=no_stored_turns, + input_replaces_history=True, ) ) From 3d13c21f896214d1f39aa4d3313b88d198fce0cc Mon Sep 17 00:00:00 2001 From: Fazeel Usmani Date: Mon, 26 Oct 2026 19:05:34 +0530 Subject: [PATCH 2/3] test(responses): escape the match pattern in the sync bridge test --- .../responses/litellm_completion_transformation/test_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index b18ce2d21ca..29e141494f9 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -363,7 +363,7 @@ def test_sync_bridged_follow_up_is_rejected_instead_of_dropping_the_history(): client: Final = HTTPHandler() client.client = httpx.Client(transport=httpx.MockTransport(provider)) - with pytest.raises(litellm.BadRequestError, match="only supported by litellm.aresponses"): + with pytest.raises(litellm.BadRequestError, match=r"only supported by litellm\.aresponses\(\)"): litellm.responses( model="azure_ai/claude-sonnet-4-6", api_base="https://fake-foundry-resource.services.ai.azure.com", From e469e79138a92461f19b0859eeed2650b5f12e9b Mon Sep 17 00:00:00 2001 From: Fazeel Usmani Date: Mon, 26 Oct 2026 20:03:14 +0530 Subject: [PATCH 3/3] test(responses): cover a compaction turn with no stored history --- .../test_handler.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index 29e141494f9..883292696e3 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -374,3 +374,34 @@ def test_sync_bridged_follow_up_is_rejected_instead_of_dropping_the_history(): ) assert provider.request_body is None + + +@pytest.mark.asyncio +async def test_bridged_compaction_turn_without_stored_history_sends_only_its_own_input(): + provider: Final = _RecordingAnthropicHandler(_ANTHROPIC_MESSAGE_PAYLOAD) + client: Final = AsyncHTTPHandler() + client.client = httpx.AsyncClient(transport=httpx.MockTransport(provider)) + summary: Final = {"type": "compaction", "content": "My favorite number is 7."} + + with patch("litellm.proxy.proxy_server.prisma_client", None): + await litellm.aresponses( + model="azure_ai/claude-sonnet-4-6", + api_base="https://fake-foundry-resource.services.ai.azure.com", + api_key="fake-api-key", + input=[ + {"type": "compaction", "id": "cmp_turn_one", "encrypted_content": json.dumps(summary)}, + {"role": "user", "content": "Double it"}, + ], + previous_response_id="resp_turn_one", + client=client, + ) + + assert provider.request_body is not None, "the compaction turn never reached the provider" + assert [ + (message["role"], json.dumps(message["content"])) for message in provider.request_body["messages"] + ] == [ + ( + "user", + json.dumps([{"type": "text", "text": "My favorite number is 7."}, {"type": "text", "text": "Double it"}]), + ), + ]