From c6b7a9524c0cc5ec0a438a82f81ab75c752b0b1a Mon Sep 17 00:00:00 2001 From: Alfredo Arenas Date: Sun, 10 May 2026 22:43:54 -0600 Subject: [PATCH] fix(providers): wire MiMo to thinking_type to allow disabling reasoning (#3585) The hosted Xiaomi MiMo API accepts {"thinking": {"type": "enabled"|"disabled"}} to toggle reasoning, which is exactly the shape produced by the existing thinking_type style. The xiaomi_mimo ProviderSpec just needed to opt in. Before this fix, setting reasoning_effort="none" had no effect on MiMo because no thinking_style was configured, so the disable signal never reached the server. Default-on models (mimo-v2.5-pro and friends) kept reasoning regardless of user configuration. Source: https://platform.xiaomimimo.com/docs/en-US/api/chat/openai-api Co-authored with Claude Opus 4.7. Strategy and review via Claude Desktop, implementation via Claude Code. --- nanobot/providers/registry.py | 3 + tests/providers/test_xiaomi_mimo_thinking.py | 121 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 tests/providers/test_xiaomi_mimo_thinking.py diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index 0f4c9869a..eb025e771 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -368,6 +368,8 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( reasoning_as_content=True, ), # Xiaomi MIMO (小米): OpenAI-compatible API + # Hosted API (api.xiaomimimo.com) accepts {"thinking": {"type": "enabled"|"disabled"}} + # to toggle reasoning, matching the existing thinking_type style. ProviderSpec( name="xiaomi_mimo", keywords=("xiaomi_mimo", "mimo"), @@ -375,6 +377,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( display_name="Xiaomi MIMO", backend="openai_compat", default_api_base="https://api.xiaomimimo.com/v1", + thinking_style="thinking_type", ), # LongCat: OpenAI-compatible API ProviderSpec( diff --git a/tests/providers/test_xiaomi_mimo_thinking.py b/tests/providers/test_xiaomi_mimo_thinking.py new file mode 100644 index 000000000..30ebf0601 --- /dev/null +++ b/tests/providers/test_xiaomi_mimo_thinking.py @@ -0,0 +1,121 @@ +"""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 _mimo_provider() -> OpenAICompatProvider: + return OpenAICompatProvider( + api_key="test-key", + default_model="mimo-v2.5-pro", + spec=_mimo_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