fix(responses): harden reasoning replay edge cases

This commit is contained in:
Joshua Stubblefield 2026-05-07 19:45:38 -04:00
parent d1535e446d
commit af84525843
4 changed files with 138 additions and 3 deletions

View file

@ -152,9 +152,8 @@ class ResponsesSessionHandler:
and _response_output
and _response_output != {}
):
if (
_response_output.get("object") == "response"
or "output" in _response_output
if ResponsesSessionHandler._is_responses_api_output_response(
_response_output
):
chat_completion_message_history.extend(
LiteLLMCompletionResponsesConfig.transform_responses_api_output_to_chat_completion_messages(
@ -170,6 +169,35 @@ class ResponsesSessionHandler:
chat_completion_message_history.append(getattr(choice, "message"))
return chat_completion_message_history
@staticmethod
def _is_responses_api_output_response(response_output: dict) -> bool:
if response_output.get("object") == "response":
return True
if "choices" in response_output:
return False
output = response_output.get("output")
if not isinstance(output, list):
return False
responses_output_types = {
"reasoning",
"message",
"function_call",
"function_call_output",
"web_search_call",
"file_search_call",
"computer_call",
"image_generation_call",
"code_interpreter_call",
"mcp_call",
"custom_tool_call",
}
for output_item in output:
if not isinstance(output_item, dict):
continue
if output_item.get("type") in responses_output_types:
return True
return False
@staticmethod
async def get_proxy_server_request_from_spend_log(
spend_log: SpendLogsPayload,

View file

@ -420,6 +420,15 @@ class LiteLLMCompletionResponsesConfig:
)
if new_role != "assistant":
continue
reasoning_content = (
LiteLLMCompletionResponsesConfig._get_chat_message_reasoning_content(
new_msg
)
)
LiteLLMCompletionResponsesConfig._set_assistant_reasoning_content(
last_msg,
reasoning_content,
)
for tool_call in LiteLLMCompletionResponsesConfig._get_tool_calls_list(
new_msg
):

View file

@ -325,6 +325,58 @@ class TestReasoningContentFinalResponse:
assert tool_calls[0].get("id") == tool_call_id
def test_merge_consecutive_function_call_messages_preserves_reasoning_content():
messages = [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_first",
"type": "function",
"function": {
"name": "first_tool",
"arguments": "{}",
},
}
],
}
]
chat_completion_messages = [
{
"role": "assistant",
"content": None,
"reasoning_content": "Need a second tool before answering.",
"tool_calls": [
{
"id": "call_second",
"type": "function",
"function": {
"name": "second_tool",
"arguments": "{}",
},
}
],
}
]
merged = LiteLLMCompletionResponsesConfig._merge_consecutive_function_call_messages(
messages=messages,
chat_completion_messages=chat_completion_messages,
input_item={"type": "function_call", "call_id": "call_second"},
existing_tool_call_ids={"call_first"},
)
assert merged is True
assert messages[0].get("reasoning_content") == (
"Need a second tool before answering."
)
assert [tool_call.get("id") for tool_call in messages[0].get("tool_calls", [])] == [
"call_first",
"call_second",
]
def test_streaming_chunk_id_raw():
"""Test that streaming chunk IDs are raw (not encoded) to match OpenAI format"""
chunk = ModelResponseStream(

View file

@ -237,6 +237,52 @@ async def test_session_history_preserves_responses_reasoning_before_tool_call():
assert tool_calls[0].get("function", {}).get("arguments") == '{"location":"Boston"}'
@pytest.mark.asyncio
async def test_chat_completion_with_output_key_uses_choices_not_responses_output():
mock_spend_logs = [
{
"request_id": "chatcmpl-custom-output",
"call_type": "aresponses",
"session_id": "session-custom-output",
"proxy_server_request": {
"input": "Return a custom provider response.",
"model": "custom/provider-model",
},
"response": {
"id": "chatcmpl-custom-output",
"object": "chat.completion",
"model": "custom/provider-model",
"output": [{"custom_provider_payload": "not a Responses API item"}],
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Provider answer from choices.",
},
"finish_reason": "stop",
}
],
},
"status": "success",
}
]
with patch.object(
ResponsesSessionHandler,
"get_all_spend_logs_for_previous_response_id",
new_callable=AsyncMock,
) as mock_get_spend_logs:
mock_get_spend_logs.return_value = mock_spend_logs
result = await ResponsesSessionHandler.get_chat_completion_message_history_for_previous_response_id(
"chatcmpl-custom-output"
)
messages = result["messages"]
assert [message.get("role") for message in messages] == ["user", "assistant"]
assert messages[1].get("content") == "Provider answer from choices."
@pytest.mark.asyncio
async def test_previous_response_tool_output_continuation_replays_reasoning_content():
"""