Skip to content

Commit 8b02267

Browse files
committed
�[38;5;8m 1�[0m �[37mfix: fall back to reasoning_content for reasoning models�[0m
�[38;5;8m 2�[0m �[38;5;8m 3�[0m �[37mReasoning models (e.g. MiniMax-M2.7, DeepSeek-R1) return their output�[0m �[38;5;8m 4�[0m �[37min `reasoning_content` instead of `content`. Previously all content�[0m �[38;5;8m 5�[0m �[37mextraction sites assumed `content` was populated, returning an empty�[0m �[38;5;8m 6�[0m �[37mstring for these models.�[0m �[38;5;8m 7�[0m �[38;5;8m 8�[0m �[37mAdd `_extract_content` / `_extract_content_from_dict` helpers that�[0m �[38;5;8m 9�[0m �[37mcheck `reasoning_content` when `content` is empty/None. Applied to:�[0m �[38;5;8m 10�[0m �[37m- openai_sdk.py: chat(), summarize(), vision()�[0m �[38;5;8m 11�[0m �[37m- HTTP backends: openai, doubao, openrouter parse_summary_response()�[0m �[38;5;8m 12�[0m �[38;5;8m 13�[0m �[37mCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>�[0m
1 parent 357aefc commit 8b02267

5 files changed

Lines changed: 41 additions & 15 deletions

File tree

src/memu/llm/backends/base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
from typing import Any
44

55

6+
def _extract_content_from_dict(data: dict[str, Any]) -> str:
7+
"""Extract text content from a raw API response dict.
8+
9+
Falls back to ``reasoning_content`` for reasoning models (e.g. MiniMax-M2.7,
10+
DeepSeek-R1) that put their output there instead of ``content``.
11+
"""
12+
msg = data["choices"][0]["message"]
13+
content = msg.get("content")
14+
if not content:
15+
content = msg.get("reasoning_content")
16+
return content or ""
17+
18+
619
class LLMBackend:
720
"""Defines how to talk to a specific HTTP LLM provider."""
821

src/memu/llm/backends/doubao.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from typing import Any, cast
3+
from typing import Any
44

5-
from memu.llm.backends.base import LLMBackend
5+
from memu.llm.backends.base import LLMBackend, _extract_content_from_dict
66

77

88
class DoubaoLLMBackend(LLMBackend):
@@ -29,7 +29,7 @@ def build_summary_payload(
2929
return payload
3030

3131
def parse_summary_response(self, data: dict[str, Any]) -> str:
32-
return cast(str, data["choices"][0]["message"]["content"])
32+
return _extract_content_from_dict(data)
3333

3434
def build_vision_payload(
3535
self,

src/memu/llm/backends/openai.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from typing import Any, cast
3+
from typing import Any
44

5-
from memu.llm.backends.base import LLMBackend
5+
from memu.llm.backends.base import LLMBackend, _extract_content_from_dict
66

77

88
class OpenAILLMBackend(LLMBackend):
@@ -26,7 +26,7 @@ def build_summary_payload(
2626
}
2727

2828
def parse_summary_response(self, data: dict[str, Any]) -> str:
29-
return cast(str, data["choices"][0]["message"]["content"])
29+
return _extract_content_from_dict(data)
3030

3131
def build_vision_payload(
3232
self,

src/memu/llm/backends/openrouter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from typing import Any, cast
3+
from typing import Any
44

5-
from memu.llm.backends.base import LLMBackend
5+
from memu.llm.backends.base import LLMBackend, _extract_content_from_dict
66

77

88
class OpenRouterLLMBackend(LLMBackend):
@@ -30,7 +30,7 @@ def build_summary_payload(
3030

3131
def parse_summary_response(self, data: dict[str, Any]) -> str:
3232
"""Parse OpenRouter response (OpenAI-compatible format)."""
33-
return cast(str, data["choices"][0]["message"]["content"])
33+
return _extract_content_from_dict(data)
3434

3535
def build_vision_payload(
3636
self,

src/memu/llm/openai_sdk.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@
1717
logger = logging.getLogger(__name__)
1818

1919

20+
def _extract_content(response: ChatCompletion) -> str:
21+
"""Extract text content from a chat completion, with fallback for reasoning models.
22+
23+
Some reasoning models (e.g. MiniMax-M2.7, DeepSeek-R1) return their output in
24+
``reasoning_content`` instead of ``content``. This helper checks both fields.
25+
"""
26+
msg = response.choices[0].message
27+
content = msg.content
28+
if not content:
29+
content = getattr(msg, "reasoning_content", None)
30+
return content or ""
31+
32+
2033
class OpenAISDKClient:
2134
"""OpenAI LLM client that relies on the official Python SDK."""
2235

@@ -59,9 +72,9 @@ async def chat(
5972
temperature=temperature,
6073
max_tokens=max_tokens,
6174
)
62-
content = response.choices[0].message.content
75+
content = _extract_content(response)
6376
logger.debug("OpenAI chat response: %s", response)
64-
return content or "", response
77+
return content, response
6578

6679
async def summarize(
6780
self,
@@ -82,9 +95,9 @@ async def summarize(
8295
temperature=1,
8396
max_tokens=max_tokens,
8497
)
85-
content = response.choices[0].message.content
98+
content = _extract_content(response)
8699
logger.debug("OpenAI summarize response: %s", response)
87-
return content or "", response
100+
return content, response
88101

89102
async def vision(
90103
self,
@@ -148,9 +161,9 @@ async def vision(
148161
temperature=1,
149162
max_tokens=max_tokens,
150163
)
151-
content = response.choices[0].message.content
164+
content = _extract_content(response)
152165
logger.debug("OpenAI vision response: %s", response)
153-
return content or "", response
166+
return content, response
154167

155168
async def embed(self, inputs: list[str]) -> tuple[list[list[float]], CreateEmbeddingResponse | None]:
156169
"""Create text embeddings via the official SDK."""

0 commit comments

Comments
 (0)