From 9ed13a71126b839aa0087f35561e22cb383308cc Mon Sep 17 00:00:00 2001 From: chenmoneygithub Date: Tue, 30 Sep 2025 22:03:26 -0700 Subject: [PATCH 1/2] fix responses API --- dspy/clients/base_lm.py | 36 ++++++++++++++++++++-------- tests/clients/test_lm.py | 51 ++++++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/dspy/clients/base_lm.py b/dspy/clients/base_lm.py index 68fc43cf75..41c67550b1 100644 --- a/dspy/clients/base_lm.py +++ b/dspy/clients/base_lm.py @@ -217,26 +217,42 @@ def _extract_citations_from_response(self, choice): def _process_response(self, response): """Process the response of OpenAI Response API and extract outputs. - + Args: response: OpenAI Response API response https://platform.openai.com/docs/api-reference/responses/object - + Returns: - List of processed outputs + List of processed outputs, which is always of size 1 because the Response API only supports one output. """ - outputs = [] + text_outputs = [] tool_calls = [] + reasoning_contents = [] + for output_item in response.output: - if output_item.type == "message": + output_item_type = output_item.type + if output_item_type == "message": for content_item in output_item.content: - outputs.append(content_item.text) - elif output_item.type == "function_call": + text_outputs.append(content_item.text) + elif output_item_type == "function_call": tool_calls.append(output_item.model_dump()) + elif output_item_type == "reasoning": + if getattr(output_item, "content", None) and len(output_item.content) > 0: + for content_item in output_item.content: + reasoning_contents.append(content_item.text) + elif getattr(output_item, "summary", None) and len(output_item.summary) > 0: + for summary_item in output_item.summary: + reasoning_contents.append(summary_item.text) + + result = {} + if len(text_outputs) > 0: + result["text"] = "".join(text_outputs) + if len(tool_calls) > 0: + result["tool_calls"] = tool_calls + if len(reasoning_contents) > 0: + result["reasoning_content"] = "".join(reasoning_contents) + return [result] - if tool_calls: - outputs.append({"tool_calls": tool_calls}) - return outputs def inspect_history(n: int = 1): diff --git a/tests/clients/test_lm.py b/tests/clients/test_lm.py index e1d7815853..5c7d5e5a09 100644 --- a/tests/clients/test_lm.py +++ b/tests/clients/test_lm.py @@ -10,6 +10,8 @@ from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse from litellm.utils import Choices, Message, ModelResponse from openai import RateLimitError +from openai.types.responses import ResponseOutputMessage, ResponseReasoningItem +from openai.types.responses.response_reasoning_item import Summary import dspy from dspy.utils.dummies import DummyLM @@ -505,36 +507,49 @@ def test_disable_history(): model="openai/gpt-4o-mini", ) -def test_responses_api(litellm_test_server): - api_base, _ = litellm_test_server - expected_text = "This is a test answer from responses API." - +def test_responses_api(): api_response = make_response( output_blocks=[ - { - "id": "msg_1", - "type": "message", - "role": "assistant", - "status": "completed", - "content": [ - {"type": "output_text", "text": expected_text, "annotations": []} - ], - } + ResponseOutputMessage( + **{ + "id": "msg_1", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [ + {"type": "output_text", "text": "This is a test answer from responses API.", "annotations": []} + ], + }, + ), + ResponseReasoningItem( + **{ + "id": "reasoning_1", + "type": "reasoning", + "summary": [Summary(**{"type": "summary_text", "text": "This is a dummy reasoning."})], + }, + ), ] ) with mock.patch("litellm.responses", autospec=True, return_value=api_response) as dspy_responses: lm = dspy.LM( - model="openai/dspy-test-model", - api_base=api_base, - api_key="fakekey", + model="openai/gpt-5-mini", model_type="responses", cache=False, + temperature=1.0, + max_tokens=16000, ) - assert lm("openai query") == [expected_text] + lm_result = lm("openai query") + + assert lm_result == [ + { + "text": "This is a test answer from responses API.", + "reasoning_content": "This is a dummy reasoning.", + } + ] dspy_responses.assert_called_once() - assert dspy_responses.call_args.kwargs["model"] == "openai/dspy-test-model" + assert dspy_responses.call_args.kwargs["model"] == "openai/gpt-5-mini" def test_lm_replaces_system_with_developer_role(): From 8c6b4f479700bb365fa6e5c7e9cf2c6e6a8a9ede Mon Sep 17 00:00:00 2001 From: chenmoneygithub Date: Wed, 1 Oct 2025 11:40:53 -0700 Subject: [PATCH 2/2] comments --- dspy/clients/base_lm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dspy/clients/base_lm.py b/dspy/clients/base_lm.py index 41c67550b1..723080845f 100644 --- a/dspy/clients/base_lm.py +++ b/dspy/clients/base_lm.py @@ -251,6 +251,7 @@ def _process_response(self, response): result["tool_calls"] = tool_calls if len(reasoning_contents) > 0: result["reasoning_content"] = "".join(reasoning_contents) + # All `response.output` items map to one answer, so we return a list of size 1. return [result]