mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-22 01:22:48 +00:00
238 lines
8.7 KiB
Python
238 lines
8.7 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."""
|
|
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"
|
|
|
|
|
|
def test_openrouter_declares_gateway_reasoning_style():
|
|
"""OpenRouter uses its own reasoning.effort field for routed thinking models."""
|
|
spec = _openrouter_spec()
|
|
assert spec.thinking_style == ""
|
|
assert spec.gateway_reasoning_style == "reasoning_effort"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _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" and does NOT forward
|
|
extra_body.thinking to upstream, so a disable signal must also reach OR
|
|
in its own `reasoning.effort` shape. Verifies both the upstream-MiMo
|
|
payload (#3845) and the OR-native payload (#3851 follow-up) are sent.
|
|
"""
|
|
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"},
|
|
"reasoning": {"effort": "none"},
|
|
}
|
|
|
|
|
|
def test_mimo_via_openrouter_reasoning_effort_medium_enables_thinking():
|
|
"""Non-none/minimal effort enables thinking and the OR `reasoning.effort`
|
|
field mirrors the requested effort level."""
|
|
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"},
|
|
"reasoning": {"effort": "medium"},
|
|
}
|
|
|
|
|
|
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"},
|
|
"reasoning": {"effort": "none"},
|
|
}
|
|
|
|
|
|
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 neither the upstream `thinking` field nor OR's
|
|
`reasoning.effort` 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
|
|
|
|
|
|
def test_kimi_via_openrouter_also_injects_reasoning_effort():
|
|
"""Kimi has the same gateway problem as MiMo: OR drops the upstream
|
|
`thinking` field. The same OR-reasoning injection should fire."""
|
|
provider = _openrouter_provider("moonshotai/kimi-k2.5")
|
|
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"},
|
|
"reasoning": {"effort": "none"},
|
|
}
|