From df9091deb666f0d689c7df01056374569019c03e Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 27 Oct 2026 23:47:42 -0700 Subject: [PATCH] fix(responses): skip failed spend-log turns when rebuilding a session from previous_response_id (#48647) * fix(responses): skip failed spend-log turns when rebuilding a session from previous_response_id Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(responses): guard the failed-turn status read and annotate the regression test locals Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../session_handler.py | 9 ++++++- .../test_session_handler.py | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/litellm/responses/litellm_completion_transformation/session_handler.py b/litellm/responses/litellm_completion_transformation/session_handler.py index d50e9a61edc..1ab6293b432 100644 --- a/litellm/responses/litellm_completion_transformation/session_handler.py +++ b/litellm/responses/litellm_completion_transformation/session_handler.py @@ -54,8 +54,15 @@ def _normalize_redacted_tool_call_arguments(message: Message) -> None: _MCP_TOOL_CALL_TYPES: Final = frozenset((CallTypes.list_mcp_tools.value, CallTypes.call_mcp_tool.value)) +_FAILED_TURN_STATUS: Final = "failure" + + +def _is_a_failed_turn(spend_log: "SpendLogsPayload") -> bool: + return "status" in spend_log and spend_log["status"] == _FAILED_TURN_STATUS + + def _is_a_conversation_turn(spend_log: "SpendLogsPayload") -> bool: - return spend_log.get("call_type") not in _MCP_TOOL_CALL_TYPES + return spend_log.get("call_type") not in _MCP_TOOL_CALL_TYPES and not _is_a_failed_turn(spend_log) _MCP_AUTO_EXECUTE_OUTPUT_TYPES: Final = frozenset(("mcp_tools_fetched", "tool_execution_results")) diff --git a/tests/unit/responses/litellm_completion_transformation/test_session_handler.py b/tests/unit/responses/litellm_completion_transformation/test_session_handler.py index 0eb1aa26a65..8d5befd9443 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_session_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_session_handler.py @@ -569,6 +569,33 @@ async def test_message_history_reconstructs_every_turn_of_the_session_in_order() assert result["litellm_session_id"] == session_id +@pytest.mark.asyncio +async def test_message_history_skips_the_turns_the_provider_rejected(): + session_id: Final = "5c5f9a3e-1c86-4c0e-9d7c-0a54b8a0f2f1" + answered_request_id: Final = "chatcmpl-1111" + rejected_request_id: Final = "chatcmpl-2222" + answered_turn: Final = { + **_spend_log(answered_request_id, session_id, "What is the weather in Paris?", "Checking."), + "status": "success", + } + rejected_turn: Final = { + **_spend_log(rejected_request_id, session_id, "Never mind. Just say hello", ""), + "status": "failure", + "response": {}, + } + fake_prisma_client: Final = _FakePrismaClient(results=[[answered_turn, rejected_turn]]) + + with patch("litellm.proxy.proxy_server.prisma_client", fake_prisma_client): + result: Final = await ResponsesSessionHandler.get_chat_completion_message_history_for_previous_response_id( + answered_request_id + ) + + assert [(message.get("role"), message.get("content")) for message in result["messages"]] == [ + ("user", "What is the weather in Paris?"), + ("assistant", "Checking."), + ] + + @pytest.mark.asyncio async def test_session_lookup_stops_retrying_once_the_budget_is_spent( instant_session_lookup_retries: None,