core/todo-nag: inject reasoning_content on synthetic assistant messages

DeepSeek's reasoning API enforces: if an assistant with tool_calls sits
between two user messages, its reasoning_content must be passed back.
The synthetic todo nag pair creates an AssistantChatMessage with tool_calls
but no reasoning_content, which triggered a 400 error:
"The reasoning_content in the thinking mode must be passed back to the API."

Fix:
1. _append_synthetic_todo_list now sets reasoning_content="" so NEW pairs
   carry the field.
2. refresh_synthetic_todo_nags runs _inject_reasoning_content_for_synthetic
   to fix EXISTING pairs (in DB) even if reasoning_content was UNSET.
3. The earlier _inject_thinking change is reverted in the next commit since
   the real root cause was the missing reasoning_content on forged tool_call
   messages, not the thinking parameter itself.
This commit is contained in:
2026-07-29 00:03:04 +08:00
parent 0ce9ed84a0
commit 200bde0bfb
+31
View File
@@ -69,6 +69,7 @@ def _append_synthetic_todo_list(messages: list[AnyChatMessage], body: str) -> st
messages.append(
AssistantChatMessage(
content=UNSET,
reasoning_content="",
tool_calls=[
AssistantFunctionToolCall(
id=call_id,
@@ -130,9 +131,39 @@ def refresh_synthetic_todo_nags(
continue
messages[index] = ToolChatMessage(tool_call_id=msg.tool_call_id, content=body)
updated += 1
# Synthetic assistant messages (tool calls but no reasoning_content) trip
# DeepSeek's rule: "if the model performed a tool call between two user
# messages, the assistant's reasoning_content must be passed back."
# Inject empty reasoning_content so the API doesn't reject the request.
updated += _inject_reasoning_content_for_synthetic(messages)
return updated
def _inject_reasoning_content_for_synthetic(
messages: list[AnyChatMessage],
) -> int:
"""Inject ``reasoning_content=\"\"`` into synthetic nag assistants that lack it."""
injected = 0
for index, msg in enumerate(messages):
if not isinstance(msg, AssistantChatMessage):
continue
tool_calls = msg.tool_calls
if tool_calls is UNSET or not tool_calls:
continue
if not any(
isinstance(c, AssistantFunctionToolCall) and is_synthetic_todo_nag_call_id(c.id) for c in tool_calls
):
continue
if msg.reasoning_content is not UNSET:
continue
from msgspec import structs
messages[index] = structs.replace(msg, reasoning_content="")
injected += 1
return injected
def inject_todo_nag(
messages: list[AnyChatMessage],
body: str,