mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 16:12:30 +00:00
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.
203 lines
7.5 KiB
Python
203 lines
7.5 KiB
Python
"""Tests for Xiaomi MiMo thinking-mode toggle via reasoning_effort.
|
|
|
|
The hosted Xiaomi MiMo API (api.xiaomimimo.com) accepts
|
|
``{"thinking": {"type": "enabled"|"disabled"}}`` in the request body
|
|
to toggle reasoning. Source: https://platform.xiaomimimo.com/docs/en-US/api/chat/openai-api
|
|
|
|
The thinking_type style already exists in _THINKING_STYLE_MAP and
|
|
produces exactly this shape, so MiMo just needs to opt in via its
|
|
ProviderSpec.thinking_style.
|
|
|
|
Default thinking behavior per Xiaomi docs:
|
|
- mimo-v2-flash: disabled
|
|
- mimo-v2.5-pro, mimo-v2.5, mimo-v2-pro, mimo-v2-omni: enabled
|
|
|
|
Without an explicit reasoning_effort, nanobot must not send the
|
|
thinking field so the provider default is preserved (issue #3585).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from nanobot.config.schema import ProvidersConfig
|
|
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
|
from nanobot.providers.registry import PROVIDERS
|
|
|
|
|
|
def _mimo_spec():
|
|
"""Return the registered xiaomi_mimo ProviderSpec."""
|
|
specs = {s.name: s for s in PROVIDERS}
|
|
return specs["xiaomi_mimo"]
|
|
|
|
|
|
def _openrouter_spec():
|
|
"""Return the registered OpenRouter ProviderSpec (no thinking_style)."""
|
|
specs = {s.name: s for s in PROVIDERS}
|
|
return specs["openrouter"]
|
|
|
|
|
|
def _mimo_provider() -> OpenAICompatProvider:
|
|
return OpenAICompatProvider(
|
|
api_key="test-key",
|
|
default_model="mimo-v2.5-pro",
|
|
spec=_mimo_spec(),
|
|
)
|
|
|
|
|
|
def _openrouter_provider(default_model: str) -> OpenAICompatProvider:
|
|
"""Provider configured as OpenRouter (gateway, no thinking_style on spec)."""
|
|
return OpenAICompatProvider(
|
|
api_key="sk-or-test",
|
|
default_model=default_model,
|
|
spec=_openrouter_spec(),
|
|
)
|
|
|
|
|
|
def _simple_messages() -> list[dict[str, Any]]:
|
|
return [{"role": "user", "content": "hello"}]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_xiaomi_mimo_config_field_exists():
|
|
"""ProvidersConfig should expose a xiaomi_mimo field."""
|
|
config = ProvidersConfig()
|
|
assert hasattr(config, "xiaomi_mimo")
|
|
|
|
|
|
def test_xiaomi_mimo_uses_thinking_type_style():
|
|
"""MiMo hosted API uses {"thinking": {"type": ...}}, the thinking_type style."""
|
|
spec = _mimo_spec()
|
|
assert spec.thinking_style == "thinking_type"
|
|
assert spec.backend == "openai_compat"
|
|
assert spec.default_api_base == "https://api.xiaomimimo.com/v1"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_kwargs wire-format
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_mimo_reasoning_effort_none_disables_thinking():
|
|
"""reasoning_effort="none" should send thinking.type="disabled"."""
|
|
provider = _mimo_provider()
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="none", tool_choice=None,
|
|
)
|
|
# reasoning_effort itself must NOT be sent when value is "none"
|
|
assert "reasoning_effort" not in kwargs
|
|
# The disable signal must be in extra_body
|
|
assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}}
|
|
|
|
|
|
def test_mimo_reasoning_effort_medium_enables_thinking():
|
|
"""reasoning_effort="medium" should send thinking.type="enabled"."""
|
|
provider = _mimo_provider()
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="medium", tool_choice=None,
|
|
)
|
|
assert kwargs.get("reasoning_effort") == "medium"
|
|
assert kwargs["extra_body"] == {"thinking": {"type": "enabled"}}
|
|
|
|
|
|
def test_mimo_reasoning_effort_low_enables_thinking():
|
|
"""Any non-none/minimal effort enables thinking."""
|
|
provider = _mimo_provider()
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="low", tool_choice=None,
|
|
)
|
|
assert kwargs["extra_body"] == {"thinking": {"type": "enabled"}}
|
|
|
|
|
|
def test_mimo_reasoning_effort_unset_preserves_provider_default():
|
|
"""When reasoning_effort is None, no thinking field is sent.
|
|
|
|
This preserves the provider default (varies by model per Xiaomi docs).
|
|
Required so that omitting the config field behaves the same as before
|
|
this fix — no behavior change for users who never set reasoning_effort.
|
|
"""
|
|
provider = _mimo_provider()
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort=None, tool_choice=None,
|
|
)
|
|
assert "reasoning_effort" not in kwargs
|
|
assert "extra_body" not in kwargs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gateway path: MiMo routed through OpenRouter (no spec.thinking_style)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_mimo_via_openrouter_reasoning_effort_none_disables_thinking():
|
|
"""OpenRouter routes MiMo as "xiaomi/mimo-v2.5-pro"; the openrouter spec
|
|
has no thinking_style, so the disable signal must come from the
|
|
model-name path (#3845)."""
|
|
provider = _openrouter_provider("xiaomi/mimo-v2.5-pro")
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="none", tool_choice=None,
|
|
)
|
|
assert "reasoning_effort" not in kwargs
|
|
assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}}
|
|
|
|
|
|
def test_mimo_via_openrouter_reasoning_effort_medium_enables_thinking():
|
|
"""Same as the direct path: any non-none/minimal effort enables thinking."""
|
|
provider = _openrouter_provider("xiaomi/mimo-v2.5-pro")
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="medium", tool_choice=None,
|
|
)
|
|
assert kwargs.get("reasoning_effort") == "medium"
|
|
assert kwargs["extra_body"] == {"thinking": {"type": "enabled"}}
|
|
|
|
|
|
def test_mimo_via_openrouter_bare_slug_also_matches():
|
|
"""Bare "mimo-v2.5-pro" (no publisher prefix) must also match the
|
|
allowlist, since gateways sometimes accept either form."""
|
|
provider = _openrouter_provider("mimo-v2.5-pro")
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="none", tool_choice=None,
|
|
)
|
|
assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}}
|
|
|
|
|
|
def test_mimo_flash_via_openrouter_does_not_inject_thinking():
|
|
"""mimo-v2-flash has no thinking mode per Xiaomi docs; the allowlist
|
|
excludes it, so no thinking field should be injected on the gateway path."""
|
|
provider = _openrouter_provider("xiaomi/mimo-v2-flash")
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="none", tool_choice=None,
|
|
)
|
|
assert "extra_body" not in kwargs
|
|
|
|
|
|
def test_non_mimo_model_via_openrouter_unaffected():
|
|
"""Sanity: a non-MiMo, non-Kimi model through OpenRouter is untouched."""
|
|
provider = _openrouter_provider("openai/gpt-4o")
|
|
kwargs = provider._build_kwargs(
|
|
messages=_simple_messages(),
|
|
tools=None, model=None, max_tokens=100,
|
|
temperature=0.7, reasoning_effort="none", tool_choice=None,
|
|
)
|
|
assert "extra_body" not in kwargs
|