Skip to content

Commit 0ca0fe2

Browse files
olgagagaRe-bin
authored andcommitted
fix(providers): wire MiMo thinking control on gateway providers (#3845)
The xiaomi_mimo ProviderSpec carries thinking_style="thinking_type", but gateway providers (OpenRouter etc.) route MiMo under their own spec which has no thinking_style. As a result, `reasoning_effort="none"` was silently ignored: `{"thinking": {"type": "disabled"}}` was never injected and responses still contained reasoning_content. Mirror the Kimi pattern that already handles the same problem: add an explicit _MIMO_THINKING_MODELS allowlist (mimo-v2.5-pro, mimo-v2.5, mimo-v2-pro, mimo-v2-omni — per Xiaomi docs), an _is_mimo_thinking_model helper that strips publisher prefixes ("xiaomi/mimo-v2.5-pro" matches), and a sibling branch in _build_kwargs that injects the thinking payload by model name. mimo-v2-flash is intentionally excluded — it has no thinking mode. Also include MiMo in the explicit_thinking predicate so the reasoning_content backfill (#3554, #3584) covers the gateway path consistently with the direct path. Tests cover the gateway disable/enable signals, bare-slug fallback, flash exclusion, and a non-MiMo sanity check.
1 parent 8a819dd commit 0ca0fe2

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

nanobot/providers/openai_compat_provider.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@
5959
"kimi-k2.6",
6060
"k2.6-code-preview",
6161
})
62+
# Thinking-capable MiMo models per Xiaomi docs (see
63+
# tests/providers/test_xiaomi_mimo_thinking.py). mimo-v2-flash is omitted
64+
# because it does not support thinking.
65+
_MIMO_THINKING_MODELS: frozenset[str] = frozenset({
66+
"mimo-v2.5-pro",
67+
"mimo-v2.5",
68+
"mimo-v2-pro",
69+
"mimo-v2-omni",
70+
})
6271
_OPENAI_COMPAT_REQUEST_TIMEOUT_S = 120.0
6372

6473
# Maps ProviderSpec.thinking_style → extra_body builder.
@@ -90,6 +99,22 @@ def _is_kimi_thinking_model(model_name: str) -> bool:
9099
return False
91100

92101

102+
def _is_mimo_thinking_model(model_name: str) -> bool:
103+
"""Return True if model_name refers to a MiMo thinking-capable model.
104+
105+
Mirrors _is_kimi_thinking_model: gateway providers (e.g. OpenRouter
106+
routing ``xiaomi/mimo-v2.5-pro``) have no ``thinking_style`` on their
107+
spec, so the spec-driven branch in _build_kwargs misses them. The
108+
model-name path catches those cases.
109+
"""
110+
name = model_name.lower()
111+
if name in _MIMO_THINKING_MODELS:
112+
return True
113+
if "/" in name and name.rsplit("/", 1)[1] in _MIMO_THINKING_MODELS:
114+
return True
115+
return False
116+
117+
93118
def _openai_compat_timeout_s() -> float:
94119
"""Return the bounded request timeout used for OpenAI-compatible providers."""
95120
return _float_env("NANOBOT_OPENAI_COMPAT_TIMEOUT_S", _OPENAI_COMPAT_REQUEST_TIMEOUT_S)
@@ -548,6 +573,19 @@ def _build_kwargs(
548573
{"thinking": {"type": "enabled" if thinking_enabled else "disabled"}}
549574
)
550575

576+
# Model-level thinking injection for MiMo thinking-capable models.
577+
# Same shape as Kimi: gateway providers (OpenRouter, etc.) lack the
578+
# xiaomi_mimo spec's thinking_style, so the spec-driven branch above
579+
# misses them — match by model name to catch "xiaomi/mimo-v2.5-pro"
580+
# and friends. (Direct xiaomi_mimo requests are also covered here;
581+
# both branches write the same payload, so the dict update is a
582+
# safe no-op for already-handled cases.)
583+
if reasoning_effort is not None and _is_mimo_thinking_model(model_name):
584+
thinking_enabled = semantic_effort not in ("none", "minimal")
585+
kwargs.setdefault("extra_body", {}).update(
586+
{"thinking": {"type": "enabled" if thinking_enabled else "disabled"}}
587+
)
588+
551589
if tools:
552590
kwargs["tools"] = tools
553591
kwargs["tool_choice"] = tool_choice or "auto"
@@ -559,7 +597,11 @@ def _build_kwargs(
559597
explicit_thinking = (
560598
reasoning_effort is not None
561599
and semantic_effort not in ("none", "minimal")
562-
and ((spec and spec.thinking_style) or _is_kimi_thinking_model(model_name))
600+
and (
601+
(spec and spec.thinking_style)
602+
or _is_kimi_thinking_model(model_name)
603+
or _is_mimo_thinking_model(model_name)
604+
)
563605
)
564606
implicit_deepseek_thinking = (
565607
spec is not None

tests/providers/test_xiaomi_mimo_thinking.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ def _mimo_spec():
3131
return specs["xiaomi_mimo"]
3232

3333

34+
def _openrouter_spec():
35+
"""Return the registered OpenRouter ProviderSpec (no thinking_style)."""
36+
specs = {s.name: s for s in PROVIDERS}
37+
return specs["openrouter"]
38+
39+
3440
def _mimo_provider() -> OpenAICompatProvider:
3541
return OpenAICompatProvider(
3642
api_key="test-key",
@@ -39,6 +45,15 @@ def _mimo_provider() -> OpenAICompatProvider:
3945
)
4046

4147

48+
def _openrouter_provider(default_model: str) -> OpenAICompatProvider:
49+
"""Provider configured as OpenRouter (gateway, no thinking_style on spec)."""
50+
return OpenAICompatProvider(
51+
api_key="sk-or-test",
52+
default_model=default_model,
53+
spec=_openrouter_spec(),
54+
)
55+
56+
4257
def _simple_messages() -> list[dict[str, Any]]:
4358
return [{"role": "user", "content": "hello"}]
4459

@@ -119,3 +134,69 @@ def test_mimo_reasoning_effort_unset_preserves_provider_default():
119134
)
120135
assert "reasoning_effort" not in kwargs
121136
assert "extra_body" not in kwargs
137+
138+
139+
# ---------------------------------------------------------------------------
140+
# Gateway path: MiMo routed through OpenRouter (no spec.thinking_style)
141+
# ---------------------------------------------------------------------------
142+
143+
144+
def test_mimo_via_openrouter_reasoning_effort_none_disables_thinking():
145+
"""OpenRouter routes MiMo as "xiaomi/mimo-v2.5-pro"; the openrouter spec
146+
has no thinking_style, so the disable signal must come from the
147+
model-name path (#3845)."""
148+
provider = _openrouter_provider("xiaomi/mimo-v2.5-pro")
149+
kwargs = provider._build_kwargs(
150+
messages=_simple_messages(),
151+
tools=None, model=None, max_tokens=100,
152+
temperature=0.7, reasoning_effort="none", tool_choice=None,
153+
)
154+
assert "reasoning_effort" not in kwargs
155+
assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}}
156+
157+
158+
def test_mimo_via_openrouter_reasoning_effort_medium_enables_thinking():
159+
"""Same as the direct path: any non-none/minimal effort enables thinking."""
160+
provider = _openrouter_provider("xiaomi/mimo-v2.5-pro")
161+
kwargs = provider._build_kwargs(
162+
messages=_simple_messages(),
163+
tools=None, model=None, max_tokens=100,
164+
temperature=0.7, reasoning_effort="medium", tool_choice=None,
165+
)
166+
assert kwargs.get("reasoning_effort") == "medium"
167+
assert kwargs["extra_body"] == {"thinking": {"type": "enabled"}}
168+
169+
170+
def test_mimo_via_openrouter_bare_slug_also_matches():
171+
"""Bare "mimo-v2.5-pro" (no publisher prefix) must also match the
172+
allowlist, since gateways sometimes accept either form."""
173+
provider = _openrouter_provider("mimo-v2.5-pro")
174+
kwargs = provider._build_kwargs(
175+
messages=_simple_messages(),
176+
tools=None, model=None, max_tokens=100,
177+
temperature=0.7, reasoning_effort="none", tool_choice=None,
178+
)
179+
assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}}
180+
181+
182+
def test_mimo_flash_via_openrouter_does_not_inject_thinking():
183+
"""mimo-v2-flash has no thinking mode per Xiaomi docs; the allowlist
184+
excludes it, so no thinking field should be injected on the gateway path."""
185+
provider = _openrouter_provider("xiaomi/mimo-v2-flash")
186+
kwargs = provider._build_kwargs(
187+
messages=_simple_messages(),
188+
tools=None, model=None, max_tokens=100,
189+
temperature=0.7, reasoning_effort="none", tool_choice=None,
190+
)
191+
assert "extra_body" not in kwargs
192+
193+
194+
def test_non_mimo_model_via_openrouter_unaffected():
195+
"""Sanity: a non-MiMo, non-Kimi model through OpenRouter is untouched."""
196+
provider = _openrouter_provider("openai/gpt-4o")
197+
kwargs = provider._build_kwargs(
198+
messages=_simple_messages(),
199+
tools=None, model=None, max_tokens=100,
200+
temperature=0.7, reasoning_effort="none", tool_choice=None,
201+
)
202+
assert "extra_body" not in kwargs

0 commit comments

Comments
 (0)