From 9b4fea36cf26102b10f07cec40e8389180e7e2ab Mon Sep 17 00:00:00 2001 From: keeponlight Date: Sat, 17 Oct 2026 19:54:09 +0800 Subject: [PATCH] fix(llm): prevent IndexError when reasoning items are absent in responses API --- src/memos/llms/openai_new.py | 4 +- tests/llms/test_openai_responses_llm.py | 104 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 tests/llms/test_openai_responses_llm.py diff --git a/src/memos/llms/openai_new.py b/src/memos/llms/openai_new.py index 766a17fda..50c87b78f 100644 --- a/src/memos/llms/openai_new.py +++ b/src/memos/llms/openai_new.py @@ -50,7 +50,7 @@ def generate(self, messages: MessageList, **kwargs) -> str: output_reasoning = [ item for item in response.output if isinstance(item, ResponseReasoningItem) ] - summary = output_reasoning[0].summary + summary = output_reasoning[0].summary if output_reasoning else None if self.config.remove_think_prefix: return remove_thinking_tags(output_text) @@ -138,7 +138,7 @@ def generate(self, messages: MessageList, **kwargs) -> str: output_reasoning = [ item for item in response.output if isinstance(item, ResponseReasoningItem) ] - summary = output_reasoning[0].summary + summary = output_reasoning[0].summary if output_reasoning else None if self.config.remove_think_prefix: return remove_thinking_tags(output_text) diff --git a/tests/llms/test_openai_responses_llm.py b/tests/llms/test_openai_responses_llm.py new file mode 100644 index 000000000..d6915b122 --- /dev/null +++ b/tests/llms/test_openai_responses_llm.py @@ -0,0 +1,104 @@ +"""Tests for OpenAIResponsesLLM and AzureResponsesLLM generate reasoning handling.""" + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + +from openai.types.responses.response_reasoning_item import ResponseReasoningItem, Summary + +from memos.configs.llm import AzureLLMConfig, OpenAIResponsesLLMConfig +from memos.llms.openai_new import AzureResponsesLLM, OpenAIResponsesLLM + + +MESSAGES = [{"role": "user", "content": "Hello"}] + + +def _make_openai_llm( + *, enable_thinking: bool = False, remove_think_prefix: bool = False +) -> OpenAIResponsesLLM: + llm = OpenAIResponsesLLM.__new__(OpenAIResponsesLLM) + llm.config = OpenAIResponsesLLMConfig( + api_key="test-key", + model_name_or_path="gpt-4o", + enable_thinking=enable_thinking, + remove_think_prefix=remove_think_prefix, + ) + llm.client = MagicMock() + return llm + + +def _make_azure_llm( + *, enable_thinking: bool = False, remove_think_prefix: bool = False +) -> AzureResponsesLLM: + llm = AzureResponsesLLM.__new__(AzureResponsesLLM) + llm.config = AzureLLMConfig( + api_key="test-key", + model_name_or_path="gpt-4o", + enable_thinking=enable_thinking, + remove_think_prefix=remove_think_prefix, + ) + llm.client = MagicMock() + return llm + + +@pytest.mark.parametrize( + "make_llm", + [_make_openai_llm, _make_azure_llm], + ids=["openai", "azure"], +) +class TestResponsesLLMGenerate: + def test_generate_without_reasoning_item_returns_output_text(self, make_llm): + llm = make_llm(enable_thinking=False) + llm.client.responses.create.return_value = SimpleNamespace( + output=[SimpleNamespace(type="message")], + output_text="plain answer", + ) + + result = llm.generate(MESSAGES) + + assert result == "plain answer" + + def test_generate_with_empty_reasoning_summary_returns_output_text(self, make_llm): + llm = make_llm(enable_thinking=True) + reasoning_item = ResponseReasoningItem(id="rs_1", summary=[], type="reasoning") + llm.client.responses.create.return_value = SimpleNamespace( + output=[reasoning_item], + output_text="plain answer", + ) + + result = llm.generate(MESSAGES) + + assert result == "plain answer" + + def test_generate_with_reasoning_summary_prepends_think_tag(self, make_llm): + llm = make_llm(enable_thinking=True, remove_think_prefix=False) + reasoning_item = ResponseReasoningItem( + id="rs_1", + summary=[Summary(text="step by step", type="summary_text")], + type="reasoning", + ) + llm.client.responses.create.return_value = SimpleNamespace( + output=[reasoning_item], + output_text="final answer", + ) + + result = llm.generate(MESSAGES) + + assert result == "step by stepfinal answer" + + def test_generate_with_remove_think_prefix_strips_thinking(self, make_llm): + llm = make_llm(enable_thinking=True, remove_think_prefix=True) + reasoning_item = ResponseReasoningItem( + id="rs_1", + summary=[Summary(text="step by step", type="summary_text")], + type="reasoning", + ) + llm.client.responses.create.return_value = SimpleNamespace( + output=[reasoning_item], + output_text="inline thoughtfinal answer", + ) + + result = llm.generate(MESSAGES) + + assert result == "final answer"