Compare commits

...

1 Commits

Author SHA1 Message Date
Teknium
06d94943d6 fix(xiaomi): replay MiMo thinking on supported routes
Salvages the Xiaomi MiMo thinking/replay fixes from #27886/#25379/#26802/#27363 into one provider cluster, with MiMo reasoning replay enabled for native Xiaomi endpoints plus Nous/OpenRouter Xiaomi slugs.

Co-authored-by: EloquentBrush0x <283442588+EloquentBrush0x@users.noreply.github.com>

Co-authored-by: Peterson <pppan2003@gmail.com>

Co-authored-by: Zhao Zhuoran <zhao.zr11@protonmail.com>

Co-authored-by: zccyman <zccyman@users.noreply.github.com>
2026-06-15 07:38:59 -07:00
7 changed files with 268 additions and 24 deletions

View File

@@ -528,6 +528,23 @@ def _is_deepseek_anthropic_endpoint(base_url: str | None) -> bool:
return "/anthropic" in normalized.rstrip("/").lower()
def _is_xiaomi_anthropic_endpoint(base_url: str | None) -> bool:
"""Return True for Xiaomi MiMo's Anthropic-compatible endpoint.
MiMo's ``/anthropic`` route speaks the Anthropic Messages protocol but
requires unsigned thinking blocks synthesized from ``reasoning_content``
to round-trip on replayed assistant tool-call messages. Pin the match to
Xiaomi's host and the ``/anthropic`` path so OpenAI-compatible MiMo bases
and aggregator ``xiaomi/mimo-*`` slugs do not get Anthropic replay semantics.
"""
if not base_url_host_matches(base_url or "", "xiaomimimo.com"):
return False
normalized = _normalize_base_url_text(base_url)
if not normalized:
return False
return "/anthropic" in normalized.rstrip("/").lower()
def _requires_bearer_auth(base_url: str | None) -> bool:
"""Return True for Anthropic-compatible providers that require Bearer auth.
@@ -2070,12 +2087,13 @@ def _manage_thinking_signatures(
"""
_THINKING_TYPES = frozenset(("thinking", "redacted_thinking"))
_is_third_party = _is_third_party_anthropic_endpoint(base_url)
# Kimi / DeepSeek share a contract: strip signed Anthropic blocks
# (neither upstream can validate Anthropic signatures), preserve unsigned
# ones synthesised from reasoning_content. See #13848, #16748.
# Kimi / DeepSeek / MiMo share a contract: strip signed Anthropic blocks
# (none of these upstreams can validate Anthropic signatures), preserve
# unsigned ones synthesised from reasoning_content. See #13848, #16748.
_preserve_unsigned_thinking = (
_is_kimi_family_endpoint(base_url, model)
or _is_deepseek_anthropic_endpoint(base_url)
or _is_xiaomi_anthropic_endpoint(base_url)
)
last_assistant_idx = None

View File

@@ -1,15 +1,37 @@
"""Xiaomi MiMo provider profile."""
from typing import Any
from providers import register_provider
from providers.base import ProviderProfile
xiaomi = ProviderProfile(
class XiaomiProfile(ProviderProfile):
"""Xiaomi MiMo — explicit thinking disable support."""
def build_api_kwargs_extras(
self,
*,
reasoning_config: dict | None = None,
**context: Any,
) -> tuple[dict[str, Any], dict[str, Any]]:
if not reasoning_config or not isinstance(reasoning_config, dict):
return {}, {}
effort = str(reasoning_config.get("effort") or "").strip().lower()
enabled = reasoning_config.get("enabled", True)
if enabled is False or effort == "none":
return {"thinking": {"type": "disabled"}}, {}
return {}, {}
xiaomi = XiaomiProfile(
name="xiaomi",
aliases=("mimo", "xiaomi-mimo"),
env_vars=("XIAOMI_API_KEY",),
base_url="https://api.xiaomimimo.com/v1",
supports_health_check=False, # /v1/models returns 401 even with valid key
supports_vision=True, # mimo-v2-omni is vision-capable
supports_vision=True, # mimo-v2.5 / omni variants are vision-capable
supports_vision_tool_messages=False, # rejects list-type tool content (400 "text is not set")
)

View File

@@ -4933,11 +4933,10 @@ class AIAgent:
``reasoning_content`` on every assistant tool-call message; omitting
it causes the next replay to fail with HTTP 400.
Detection is host-driven, not model-name-driven: aggregators like
OpenRouter that re-export Kimi/Moonshot models speak their own
protocol and reject ``reasoning_content`` echoes. We only enable the
kimi-reasoning replay when the request actually targets a
kimi/moonshot endpoint or the dedicated kimi-coding provider.
Detection is host/provider-driven, not model-name-driven: aggregators
like OpenRouter that re-export thinking models speak their own protocol
and reject ``reasoning_content`` echoes. We only enable replay when the
request actually targets the provider's native endpoint.
"""
return (
self.provider in {"kimi-coding", "kimi-coding-cn"}
@@ -4953,11 +4952,8 @@ class AIAgent:
assistant tool-call turn; omitting it causes HTTP 400 when the
message is replayed in a subsequent API request (#15250).
"""
provider = (self.provider or "").lower()
model = (self.model or "").lower()
return (
provider == "deepseek"
or "deepseek" in model
(getattr(self, "provider", "") or "").lower() == "deepseek"
or base_url_host_matches(self.base_url, "api.deepseek.com")
)
@@ -4968,11 +4964,12 @@ class AIAgent:
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
"""
provider = (self.provider or "").lower()
model = (self.model or "").lower()
provider = (getattr(self, "provider", "") or "").lower()
model = (getattr(self, "model", "") or "").lower()
aggregator_mimo = provider in {"openrouter", "nous"} and model.startswith("xiaomi/mimo-")
return (
provider == "xiaomi"
or "mimo" in model
or aggregator_mimo
or base_url_host_matches(self.base_url, "api.xiaomimimo.com")
or base_url_host_matches(self.base_url, "xiaomimimo.com")
)

View File

@@ -1374,6 +1374,9 @@ AUTHOR_MAP = {
# batch salvage (May 2026 LHF run, group 2)
"shellybotmoyer@example.com": "shellybotmoyer", # PR #26661 (kanban --severity >=)
"coulson@shellybotmoyer.com": "shellybotmoyer", # PR #25576 (credential_pool ISO rehydrate)
"pppan2003@gmail.com": "pppan2003", # PR #25379 salvage (Xiaomi MiMo /anthropic thinking replay)
"zhao.zr11@protonmail.com": "CallMe1101", # PR #26802 salvage (Xiaomi MiMo /anthropic thinking replay)
"morpheu5code@gmail.com": "Morpheu5Code", # PR #26803 (Slack file upload parsing)
"258858106+shellybotmoyer@users.noreply.github.com": "shellybotmoyer",
"33156212+ether-btc@users.noreply.github.com": "ether-btc", # PR #26632 (memory provider whitespace guard)
"Bloomtonjovish@gmail.com": "LifeJiggy", # PR #26516 (paste collapse logging)

View File

@@ -0,0 +1,124 @@
"""Regression guard: preserve thinking blocks on Xiaomi MiMo /anthropic.
Xiaomi MiMo's Anthropic-compatible route requires unsigned thinking blocks
synthesised from ``reasoning_content`` to round-trip on replayed assistant
tool-call messages. The generic third-party Anthropic path strips thinking
blocks, which breaks multi-turn tool-use replay on MiMo.
"""
from __future__ import annotations
import pytest
class TestXiaomiAnthropicPreservesThinking:
@pytest.mark.parametrize(
"base_url",
[
"https://token-plan-cn.xiaomimimo.com/anthropic",
"https://token-plan-sgp.xiaomimimo.com/anthropic/",
"https://api.xiaomimimo.com/anthropic/v1",
"https://API.XiaomiMiMo.com/anthropic",
],
)
def test_unsigned_thinking_block_survives_replay(self, base_url: str) -> None:
from agent.anthropic_adapter import convert_messages_to_anthropic
messages = [
{"role": "user", "content": "hi"},
{
"role": "assistant",
"reasoning_content": "planning the tool call",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "terminal", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "ok"},
]
_system, converted = convert_messages_to_anthropic(messages, base_url=base_url)
assistant_msg = next(m for m in converted if m["role"] == "assistant")
thinking_blocks = [
b for b in assistant_msg["content"]
if isinstance(b, dict) and b.get("type") == "thinking"
]
assert len(thinking_blocks) == 1
assert thinking_blocks[0]["thinking"] == "planning the tool call"
assert "signature" not in thinking_blocks[0]
def test_signed_anthropic_thinking_block_is_stripped(self) -> None:
from agent.anthropic_adapter import convert_messages_to_anthropic
messages = [
{"role": "user", "content": "hi"},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "anthropic-signed payload",
"signature": "anthropic-sig-xyz",
},
{"type": "text", "text": "hello"},
],
},
{"role": "user", "content": "again"},
]
_system, converted = convert_messages_to_anthropic(
messages,
base_url="https://token-plan-cn.xiaomimimo.com/anthropic",
)
assistant_msg = next(m for m in converted if m["role"] == "assistant")
thinking_blocks = [
b for b in assistant_msg["content"]
if isinstance(b, dict) and b.get("type") == "thinking"
]
assert thinking_blocks == []
def test_openai_compat_xiaomi_base_is_not_matched(self) -> None:
from agent.anthropic_adapter import _is_xiaomi_anthropic_endpoint
assert _is_xiaomi_anthropic_endpoint("https://api.xiaomimimo.com") is False
assert _is_xiaomi_anthropic_endpoint("https://api.xiaomimimo.com/v1") is False
assert _is_xiaomi_anthropic_endpoint("https://api.xiaomimimo.com/anthropic") is True
assert _is_xiaomi_anthropic_endpoint("https://token-plan-sgp.xiaomimimo.com/anthropic/v1") is True
def test_lookalike_host_is_not_matched(self) -> None:
from agent.anthropic_adapter import _is_xiaomi_anthropic_endpoint
assert _is_xiaomi_anthropic_endpoint("https://xiaomimimo.com.evil/anthropic") is False
assert _is_xiaomi_anthropic_endpoint("https://evil.example.com/xiaomimimo.com/anthropic") is False
def test_non_xiaomi_third_party_still_strips_all_thinking(self) -> None:
from agent.anthropic_adapter import convert_messages_to_anthropic
messages = [
{"role": "user", "content": "hi"},
{
"role": "assistant",
"reasoning_content": "r1",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "terminal", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "ok"},
]
_system, converted = convert_messages_to_anthropic(
messages,
base_url="https://api.minimax.io/anthropic",
)
assistant_msg = next(m for m in converted if m["role"] == "assistant")
thinking_blocks = [
b for b in assistant_msg["content"]
if isinstance(b, dict) and b.get("type") == "thinking"
]
assert thinking_blocks == []

View File

@@ -96,6 +96,41 @@ class TestKimiProfile:
assert "reasoning_effort" not in tl
class TestXiaomiProfile:
def test_alias_lookup(self):
p = get_provider_profile("mimo")
assert p is not None
assert p.name == "xiaomi"
def test_no_config_omits_thinking(self):
p = get_provider_profile("xiaomi")
assert p is not None
eb, tl = p.build_api_kwargs_extras(reasoning_config=None)
assert eb == {}
assert tl == {}
def test_disabled_reasoning_disables_thinking(self):
p = get_provider_profile("xiaomi")
assert p is not None
eb, tl = p.build_api_kwargs_extras(reasoning_config={"enabled": False})
assert eb["thinking"] == {"type": "disabled"}
assert tl == {}
def test_effort_none_disables_thinking(self):
p = get_provider_profile("xiaomi")
assert p is not None
eb, tl = p.build_api_kwargs_extras(reasoning_config={"enabled": True, "effort": "none"})
assert eb["thinking"] == {"type": "disabled"}
assert tl == {}
def test_enabled_reasoning_omits_thinking(self):
p = get_provider_profile("xiaomi")
assert p is not None
eb, tl = p.build_api_kwargs_extras(reasoning_config={"enabled": True, "effort": "high"})
assert eb == {}
assert tl == {}
class TestOpenRouterProfile:
def test_extra_body_with_prefs(self):
p = get_provider_profile("openrouter")

View File

@@ -14,9 +14,10 @@ Fix covers three paths:
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 provider/host driven: ``provider == "deepseek"`` or the
``api.deepseek.com`` host. Model-name-only matching is intentionally
rejected because aggregators re-export DeepSeek slugs but do not accept
native ``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
@@ -72,16 +73,16 @@ def _build_sdk_message(reasoning_content=_ATTR_ABSENT, **extra):
class TestNeedsDeepSeekToolReasoning:
"""_needs_deepseek_tool_reasoning() recognises all three detection signals."""
"""_needs_deepseek_tool_reasoning() recognises native endpoint signals."""
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'
def test_model_substring_without_native_provider_or_host_is_false(self) -> None:
# Aggregators can re-export DeepSeek slugs but reject native echoes.
agent = _make_agent(provider="custom", model="deepseek-v4-pro")
assert agent._needs_deepseek_tool_reasoning() is True
assert agent._needs_deepseek_tool_reasoning() is False
def test_base_url_host(self) -> None:
agent = _make_agent(
@@ -108,6 +109,50 @@ class TestNeedsDeepSeekToolReasoning:
assert agent._needs_deepseek_tool_reasoning() is False
class TestNeedsMiMoToolReasoning:
"""_needs_mimo_tool_reasoning() covers native hosts and known aggregators."""
def test_provider_xiaomi(self) -> None:
agent = _make_agent(provider="xiaomi", model="mimo-v2.5-pro")
assert agent._needs_mimo_tool_reasoning() is True
def test_xiaomi_base_url_host(self) -> None:
agent = _make_agent(
provider="custom",
model="some-alias",
base_url="https://token-plan-sgp.xiaomimimo.com/v1",
)
assert agent._needs_mimo_tool_reasoning() is True
def test_openrouter_xiaomi_slug_is_true(self) -> None:
agent = _make_agent(
provider="openrouter",
model="xiaomi/mimo-v2.5-pro",
base_url="https://openrouter.ai/api/v1",
)
assert agent._needs_mimo_tool_reasoning() is True
def test_nous_xiaomi_slug_is_true(self) -> None:
agent = _make_agent(
provider="nous",
model="xiaomi/mimo-v2.5-pro",
base_url="https://inference-api.nousresearch.com/v1",
)
assert agent._needs_mimo_tool_reasoning() is True
def test_custom_mimo_slug_without_native_host_is_false(self) -> None:
agent = _make_agent(provider="custom", model="mimo-v2.5-pro")
assert agent._needs_mimo_tool_reasoning() is False
def test_openrouter_non_mimo_xiaomi_slug_is_false(self) -> None:
agent = _make_agent(
provider="openrouter",
model="xiaomi/other-model",
base_url="https://openrouter.ai/api/v1",
)
assert agent._needs_mimo_tool_reasoning() is False
class TestCopyReasoningContentForApi:
"""_copy_reasoning_content_for_api pads reasoning_content for DeepSeek tool-calls."""