Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3628,12 +3628,16 @@ def _needs_deepseek_tool_reasoning(self) -> bool:
DeepSeek V4 thinking mode requires ``reasoning_content`` on every
assistant tool-call turn; omitting it causes HTTP 400 when the
message is replayed in a subsequent API request (#15250).

Detection is host-driven, not model-name-driven: aggregators like
OpenRouter that re-export DeepSeek models speak their own protocol
and reject ``reasoning_content`` echoes. We only enable the
deepseek-reasoning replay when the request actually targets the
official DeepSeek endpoint or the dedicated deepseek provider.
"""
provider = (self.provider or "").lower()
model = (self.model or "").lower()
return (
provider == "deepseek"
or "deepseek" in model
or base_url_host_matches(self.base_url, "api.deepseek.com")
)

Expand All @@ -3643,12 +3647,16 @@ def _needs_mimo_tool_reasoning(self) -> bool:
MiMo thinking mode requires ``reasoning_content`` on every assistant
tool-call message when replaying history; omitting it causes HTTP 400.
Refs: https://platform.xiaomimimo.com/docs/zh-CN/usage-guide/passing-back-reasoning_content

Detection is host-driven, not model-name-driven: aggregators that
re-export MiMo models speak their own protocol and reject
``reasoning_content`` echoes. We only enable the replay when the
request actually targets an official MiMo endpoint or the dedicated
xiaomi provider.
"""
provider = (self.provider or "").lower()
model = (self.model or "").lower()
return (
provider == "xiaomi"
or "mimo" in model
or base_url_host_matches(self.base_url, "api.xiaomimimo.com")
or base_url_host_matches(self.base_url, "xiaomimimo.com")
)
Expand Down
22 changes: 15 additions & 7 deletions tests/run_agent/test_deepseek_reasoning_content_echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
persisted poisoned.
2. ``_copy_reasoning_content_for_api`` — already-poisoned history replays
with ``reasoning_content=" "`` injected defensively.
3. Detection covers three signals: ``provider == "deepseek"``,
``"deepseek" in model``, and ``api.deepseek.com`` host match. The third
catches custom-provider setups pointing at DeepSeek.
3. Detection is host-driven, not model-name-driven: two signals suffice —
``provider == "deepseek"`` and ``api.deepseek.com`` host match. The host
match catches custom-provider setups pointing at the official DeepSeek
endpoint. Aggregators like OpenRouter that re-export DeepSeek models are
intentionally excluded — they speak their own protocol and reject
``reasoning_content`` echoes.

The placeholder is a single space (not empty string) because DeepSeek V4 Pro
tightened validation and rejects empty-string reasoning_content with a
Expand Down Expand Up @@ -78,10 +81,15 @@ def test_provider_deepseek(self) -> None:
agent = _make_agent(provider="deepseek", model="deepseek-v4-flash")
assert agent._needs_deepseek_tool_reasoning() is True

def test_model_substring(self) -> None:
# Custom provider pointing at DeepSeek with provider='custom'
agent = _make_agent(provider="custom", model="deepseek-v4-pro")
assert agent._needs_deepseek_tool_reasoning() is True
def test_openrouter_deepseek_model_not_detected(self) -> None:
# OpenRouter re-exports deepseek models but speaks its own protocol —
# must NOT trigger reasoning_content echo or it causes HTTP 400.
agent = _make_agent(
provider="openrouter",
model="deepseek/deepseek-v3",
base_url="https://openrouter.ai/api/v1",
)
assert agent._needs_deepseek_tool_reasoning() is False

def test_base_url_host(self) -> None:
agent = _make_agent(
Expand Down
Loading