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>
This commit is contained in:
ryan-crabbe-berri 2026-10-27 23:47:42 -07:00 • committed by GitHub
parent c7c895db45
commit df9091deb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 35 additions and 1 deletions

View file

@ -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"))

View file

@ -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,