From 59e98be1df29ab869203dda0216fa77cba96a853 Mon Sep 17 00:00:00 2001 From: RerankerGuo <121015044+RerankerGuo@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:00:19 +0800 Subject: [PATCH 1/2] feat: add enable_thinking support for OpenAI-compatible providers Closes #2149 Adds enable_thinking configuration parameter for OpenAI-compatible providers (qwen, deepseek, minimax) to control whether the model produces reasoning blocks before the actual response. Changes: - Added enable_thinking field (bool | None) to OpenAILLMConfig - None (default): preserve provider's default behavior - True: explicitly enable thinking mode - False: explicitly disable thinking mode (prevents JSON breaking) - OpenAILLM.generate / generate_stream now pass enable_thinking to API calls when configured, via enable_thinking body param - AzureLLM.generate / generate_stream also support the parameter (gated by getattr for backward compatibility with older configs) - Per-call override supported via kwargs (enable_thinking=True/False) - Added tests covering default, config-level, and kwarg-level enable_thinking behavior in test_enable_thinking.py This is a non-breaking change: when enable_thinking is unset, request bodies are identical to previous versions. Test: python3 -m py_compile src/memos/configs/llm.py Test: python3 -m py_compile src/memos/llms/openai.py Test: python3 -m py_compile tests/llms/test_enable_thinking.py --- src/memos/configs/llm.py | 5 ++ src/memos/llms/openai.py | 73 +++++++++++++++++---------- tests/llms/test_enable_thinking.py | 80 ++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 27 deletions(-) create mode 100644 tests/llms/test_enable_thinking.py diff --git a/src/memos/configs/llm.py b/src/memos/configs/llm.py index 81f7038fa..627a7daed 100644 --- a/src/memos/configs/llm.py +++ b/src/memos/configs/llm.py @@ -28,6 +28,11 @@ class OpenAILLMConfig(BaseLLMConfig): default="https://api.openai.com/v1", description="Base URL for OpenAI API" ) extra_body: Any = Field(default=None, description="extra body") + enable_thinking: bool | None = Field( + default=None, + description="Enable/disable thinking mode for models that support it (e.g. Qwen3, DeepSeek-R1). " + "When None (default), the provider's default behavior is preserved.", + ) backup_client: bool = Field( default=False, description="Whether to enable backup client for fallback on primary failure", diff --git a/src/memos/llms/openai.py b/src/memos/llms/openai.py index 9a29ea68a..e4c45eca3 100644 --- a/src/memos/llms/openai.py +++ b/src/memos/llms/openai.py @@ -61,15 +61,7 @@ def _parse_response(self, response) -> str: return reasoning_content + (response_content or "") return response_content or "" - @timed_with_status( - log_prefix="OpenAI LLM", - log_extra_args=lambda self, messages, **kwargs: { - "model_name_or_path": kwargs.get("model_name_or_path", self.config.model_name_or_path), - "messages": messages, - }, - ) - def generate(self, messages: MessageList, **kwargs) -> str: - """Generate a response from OpenAI LLM, optionally overriding generation params.""" + def _build_request_body(self, messages: MessageList, **kwargs) -> dict: request_body = { "model": kwargs.get("model_name_or_path", self.config.model_name_or_path), "messages": messages, @@ -79,6 +71,21 @@ def generate(self, messages: MessageList, **kwargs) -> str: "extra_body": kwargs.get("extra_body", self.config.extra_body), "tools": kwargs.get("tools", NOT_GIVEN), } + enable_thinking = kwargs.get("enable_thinking", self.config.enable_thinking) + if enable_thinking is not None: + request_body["enable_thinking"] = enable_thinking + return request_body + + @timed_with_status( + log_prefix="OpenAI LLM", + log_extra_args=lambda self, messages, **kwargs: { + "model_name_or_path": kwargs.get("model_name_or_path", self.config.model_name_or_path), + "messages": messages, + }, + ) + def generate(self, messages: MessageList, **kwargs) -> str: + """Generate a response from OpenAI LLM, optionally overriding generation params.""" + request_body = self._build_request_body(messages, **kwargs) start_time = time.perf_counter() logger.info(f"OpenAI LLM Request body: {request_body}") @@ -132,6 +139,10 @@ def generate_stream(self, messages: MessageList, **kwargs) -> Generator[str, Non "tools": kwargs.get("tools", NOT_GIVEN), } + enable_thinking = kwargs.get("enable_thinking", self.config.enable_thinking) + if enable_thinking is not None: + request_body["enable_thinking"] = enable_thinking + logger.info(f"OpenAI LLM Stream Request body: {request_body}") response = self.client.chat.completions.create(**request_body) @@ -184,15 +195,19 @@ def __init__(self, config: AzureLLMConfig): def generate(self, messages: MessageList, **kwargs) -> str: """Generate a response from Azure OpenAI LLM.""" - response = self.client.chat.completions.create( - model=self.config.model_name_or_path, - messages=messages, - temperature=kwargs.get("temperature", self.config.temperature), - max_tokens=kwargs.get("max_tokens", self.config.max_tokens), - top_p=kwargs.get("top_p", self.config.top_p), - tools=kwargs.get("tools", NOT_GIVEN), - extra_body=kwargs.get("extra_body", self.config.extra_body), - ) + request_body = { + "model": self.config.model_name_or_path, + "messages": messages, + "temperature": kwargs.get("temperature", self.config.temperature), + "max_tokens": kwargs.get("max_tokens", self.config.max_tokens), + "top_p": kwargs.get("top_p", self.config.top_p), + "tools": kwargs.get("tools", NOT_GIVEN), + "extra_body": kwargs.get("extra_body", self.config.extra_body), + } + enable_thinking = kwargs.get("enable_thinking", getattr(self.config, "enable_thinking", None)) + if enable_thinking is not None: + request_body["enable_thinking"] = enable_thinking + response = self.client.chat.completions.create(**request_body) logger.info(f"Response from Azure OpenAI: {response.model_dump_json()}") if not response.choices: logger.warning("Azure OpenAI response has no choices") @@ -212,15 +227,19 @@ def generate_stream(self, messages: MessageList, **kwargs) -> Generator[str, Non logger.info("stream api not support tools") return - response = self.client.chat.completions.create( - model=self.config.model_name_or_path, - messages=messages, - stream=True, - temperature=kwargs.get("temperature", self.config.temperature), - max_tokens=kwargs.get("max_tokens", self.config.max_tokens), - top_p=kwargs.get("top_p", self.config.top_p), - extra_body=kwargs.get("extra_body", self.config.extra_body), - ) + request_body = { + "model": self.config.model_name_or_path, + "messages": messages, + "stream": True, + "temperature": kwargs.get("temperature", self.config.temperature), + "max_tokens": kwargs.get("max_tokens", self.config.max_tokens), + "top_p": kwargs.get("top_p", self.config.top_p), + "extra_body": kwargs.get("extra_body", self.config.extra_body), + } + enable_thinking = kwargs.get("enable_thinking", getattr(self.config, "enable_thinking", None)) + if enable_thinking is not None: + request_body["enable_thinking"] = enable_thinking + response = self.client.chat.completions.create(**request_body) reasoning_started = False diff --git a/tests/llms/test_enable_thinking.py b/tests/llms/test_enable_thinking.py new file mode 100644 index 000000000..ffe17e5b5 --- /dev/null +++ b/tests/llms/test_enable_thinking.py @@ -0,0 +1,80 @@ +"""Tests for enable_thinking parameter in OpenAILLM.""" + +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +from memos.configs.llm import OpenAILLMConfig +from memos.llms.openai import AzureLLM, OpenAILLM + + +def _make_config(**overrides): + defaults = { + "provider": "openai", + "api_key": "test-key", + "model_name_or_path": "gpt-4o", + } + defaults.update(overrides) + return OpenAILLMConfig(**defaults) + + +class TestEnableThinkingConfig: + def test_enable_thinking_defaults_to_none(self): + config = _make_config() + assert config.enable_thinking is None + + def test_enable_thinking_can_be_set(self): + config = _make_config(enable_thinking=True) + assert config.enable_thinking is True + + def test_enable_thinking_false(self): + config = _make_config(enable_thinking=False) + assert config.enable_thinking is False + + +class TestEnableThinkingInRequest: + def test_build_request_body_without_enable_thinking(self): + config = _make_config() + embedder = OpenAILLM.__new__(OpenAILLM) + embedder.config = config + + body = embedder._build_request_body([{"role": "user", "content": "hi"}]) + assert "enable_thinking" not in body + + def test_build_request_body_with_enable_thinking_from_config(self): + config = _make_config(enable_thinking=True) + embedder = OpenAILLM.__new__(OpenAILLM) + embedder.config = config + + body = embedder._build_request_body([{"role": "user", "content": "hi"}]) + assert body["enable_thinking"] is True + + def test_build_request_body_with_enable_thinking_from_kwargs(self): + config = _make_config(enable_thinking=True) + embedder = OpenAILLM.__new__(OpenAILLM) + embedder.config = config + + body = embedder._build_request_body( + [{"role": "user", "content": "hi"}], enable_thinking=False + ) + assert body["enable_thinking"] is False + + def test_build_request_body_with_enable_thinking_false(self): + config = _make_config(enable_thinking=False) + embedder = OpenAILLM.__new__(OpenAILLM) + embedder.config = config + + body = embedder._build_request_body([{"role": "user", "content": "hi"}]) + assert body["enable_thinking"] is False + + def test_build_request_body_preserves_other_params(self): + config = _make_config(enable_thinking=True, temperature=0.5, max_tokens=100) + embedder = OpenAILLM.__new__(OpenAILLM) + embedder.config = config + + body = embedder._build_request_body([{"role": "user", "content": "hi"}]) + assert body["enable_thinking"] is True + assert body["temperature"] == 0.5 + assert body["max_tokens"] == 100 + assert body["model"] == "gpt-4o" From f84ec3f43454d5c25a157d2e56bbc9fcdb334737 Mon Sep 17 00:00:00 2001 From: RerankerGuo <121015044+RerankerGuo@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:02:34 +0800 Subject: [PATCH 2/2] style(test): ruff cleanups for enable_thinking tests --- tests/llms/test_enable_thinking.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/llms/test_enable_thinking.py b/tests/llms/test_enable_thinking.py index ffe17e5b5..be3441faa 100644 --- a/tests/llms/test_enable_thinking.py +++ b/tests/llms/test_enable_thinking.py @@ -1,12 +1,7 @@ """Tests for enable_thinking parameter in OpenAILLM.""" -from types import SimpleNamespace -from unittest.mock import patch - -import pytest - from memos.configs.llm import OpenAILLMConfig -from memos.llms.openai import AzureLLM, OpenAILLM +from memos.llms.openai import OpenAILLM def _make_config(**overrides):