fix(provider): narrow DeepSeek reasoning history cleanup
Made-with: Cursor
This commit is contained in:
@@ -452,6 +452,7 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
def _drop_deepseek_incomplete_reasoning_history(
|
def _drop_deepseek_incomplete_reasoning_history(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
|
model_name: str,
|
||||||
reasoning_effort: str | None,
|
reasoning_effort: str | None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
if (
|
if (
|
||||||
@@ -460,13 +461,19 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
):
|
):
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
# For DeepSeek models, always check for incomplete reasoning history
|
semantic_effort = reasoning_effort.lower() if isinstance(reasoning_effort, str) else None
|
||||||
# when thinking mode might be active. reasoning_effort may not be set
|
if semantic_effort in {"none", "minimal", "minimum"}:
|
||||||
# explicitly but the model could still be using thinking mode by default.
|
|
||||||
# Only skip this check when reasoning_effort is explicitly set to "none".
|
|
||||||
if reasoning_effort is not None and reasoning_effort.lower() == "none":
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
# DeepSeek-V4 can require reasoning_content even when the config did
|
||||||
|
# not explicitly request reasoning_effort. Keep that implicit-thinking
|
||||||
|
# cleanup scoped to known thinking-capable DeepSeek models so normal
|
||||||
|
# deepseek-chat history is not trimmed.
|
||||||
|
if semantic_effort is None:
|
||||||
|
model_lower = model_name.lower()
|
||||||
|
if not any(token in model_lower for token in ("deepseek-v4", "deepseek-reasoner")):
|
||||||
|
return messages
|
||||||
|
|
||||||
bad_idx = None
|
bad_idx = None
|
||||||
for idx, msg in enumerate(messages):
|
for idx, msg in enumerate(messages):
|
||||||
if (
|
if (
|
||||||
@@ -537,6 +544,7 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
|
|
||||||
messages = self._drop_deepseek_incomplete_reasoning_history(
|
messages = self._drop_deepseek_incomplete_reasoning_history(
|
||||||
messages,
|
messages,
|
||||||
|
model_name,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
)
|
)
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
|
|||||||
@@ -913,8 +913,8 @@ def test_deepseek_backfills_reasoning_content_on_legacy_tool_call_messages() ->
|
|||||||
assert msg["reasoning_content"] == ""
|
assert msg["reasoning_content"] == ""
|
||||||
|
|
||||||
|
|
||||||
def test_backfill_does_not_touch_messages_when_thinking_off() -> None:
|
def test_backfill_does_not_touch_messages_when_thinking_explicitly_off() -> None:
|
||||||
"""When reasoning_effort is None or minimal, legacy messages must NOT be altered."""
|
"""When thinking is explicitly disabled, legacy messages must NOT be altered."""
|
||||||
spec = find_by_name("deepseek")
|
spec = find_by_name("deepseek")
|
||||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||||
p = OpenAICompatProvider(api_key="k", default_model="deepseek-v4-pro", spec=spec)
|
p = OpenAICompatProvider(api_key="k", default_model="deepseek-v4-pro", spec=spec)
|
||||||
@@ -926,7 +926,7 @@ def test_backfill_does_not_touch_messages_when_thinking_off() -> None:
|
|||||||
{"role": "tool", "tool_call_id": "tc1", "content": "result"},
|
{"role": "tool", "tool_call_id": "tc1", "content": "result"},
|
||||||
{"role": "user", "content": "thanks"},
|
{"role": "user", "content": "thanks"},
|
||||||
]
|
]
|
||||||
for effort in (None, "minimal"):
|
for effort in ("minimal", "none"):
|
||||||
kw = p._build_kwargs(
|
kw = p._build_kwargs(
|
||||||
messages=list(messages), tools=None, model="deepseek-v4-pro",
|
messages=list(messages), tools=None, model="deepseek-v4-pro",
|
||||||
max_tokens=1024, temperature=0.7,
|
max_tokens=1024, temperature=0.7,
|
||||||
@@ -937,6 +937,56 @@ def test_backfill_does_not_touch_messages_when_thinking_off() -> None:
|
|||||||
assert "reasoning_content" not in msg
|
assert "reasoning_content" not in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepseek_v4_drops_incomplete_reasoning_history_when_effort_implicit() -> None:
|
||||||
|
"""DeepSeek-V4 may default to thinking, so incomplete legacy history is trimmed."""
|
||||||
|
spec = find_by_name("deepseek")
|
||||||
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||||
|
p = OpenAICompatProvider(api_key="k", default_model="deepseek-v4-pro", spec=spec)
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "system"},
|
||||||
|
{"role": "user", "content": "hi"},
|
||||||
|
{"role": "assistant", "content": "", "tool_calls": [
|
||||||
|
{"id": "tc1", "type": "function", "function": {"name": "web_search", "arguments": "{}"}}
|
||||||
|
]},
|
||||||
|
{"role": "tool", "tool_call_id": "tc1", "content": "result"},
|
||||||
|
{"role": "user", "content": "thanks"},
|
||||||
|
]
|
||||||
|
|
||||||
|
kw = p._build_kwargs(
|
||||||
|
messages=list(messages), tools=None, model="deepseek-v4-pro",
|
||||||
|
max_tokens=1024, temperature=0.7,
|
||||||
|
reasoning_effort=None, tool_choice=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [msg["role"] for msg in kw["messages"]] == ["system", "user"]
|
||||||
|
assert kw["messages"][-1]["content"] == "thanks"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepseek_chat_keeps_tool_history_when_effort_implicit() -> None:
|
||||||
|
"""Implicit cleanup must not trim non-thinking DeepSeek chat models."""
|
||||||
|
spec = find_by_name("deepseek")
|
||||||
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||||
|
p = OpenAICompatProvider(api_key="k", default_model="deepseek-chat", spec=spec)
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": "hi"},
|
||||||
|
{"role": "assistant", "content": "", "tool_calls": [
|
||||||
|
{"id": "tc1", "type": "function", "function": {"name": "web_search", "arguments": "{}"}}
|
||||||
|
]},
|
||||||
|
{"role": "tool", "tool_call_id": "tc1", "content": "result"},
|
||||||
|
{"role": "user", "content": "thanks"},
|
||||||
|
]
|
||||||
|
|
||||||
|
kw = p._build_kwargs(
|
||||||
|
messages=list(messages), tools=None, model="deepseek-chat",
|
||||||
|
max_tokens=1024, temperature=0.7,
|
||||||
|
reasoning_effort=None, tool_choice=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
roles = [msg["role"] for msg in kw["messages"]]
|
||||||
|
assert roles == ["user", "assistant", "tool", "user"]
|
||||||
|
assert kw["messages"][1]["tool_calls"]
|
||||||
|
|
||||||
|
|
||||||
def test_deepseek_coerces_list_content_to_string() -> None:
|
def test_deepseek_coerces_list_content_to_string() -> None:
|
||||||
"""DeepSeek chat endpoint expects message.content to be a string."""
|
"""DeepSeek chat endpoint expects message.content to be a string."""
|
||||||
spec = find_by_name("deepseek")
|
spec = find_by_name("deepseek")
|
||||||
|
|||||||
Reference in New Issue
Block a user