mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(responses): harden reasoning replay edge cases
This commit is contained in:
parent
d1535e446d
commit
af84525843
4 changed files with 138 additions and 3 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue