From 25a55fe1c78f30ebf1e2f1cde26a5e7389bbd9ea Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:32:51 +0800 Subject: [PATCH] fix(providers): enable thinking for Kimi K2.7 models --- nanobot/providers/openai_compat_provider.py | 18 ++++++++-- nanobot/providers/registry.py | 5 ++- tests/providers/test_litellm_kwargs.py | 37 +++++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 5aa91e07..f0023854 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -60,8 +60,15 @@ _DEFAULT_OPENROUTER_HEADERS = { _KIMI_THINKING_MODELS: frozenset[str] = frozenset({ "kimi-k2.5", "kimi-k2.6", + "kimi-k2.7", + "kimi-k2.7-code", + "kimi-k2.7-code-highspeed", "k2.6-code-preview", }) +_KIMI_ALWAYS_THINKING_MODELS: frozenset[str] = frozenset({ + "kimi-k2.7-code", + "kimi-k2.7-code-highspeed", +}) # Thinking-capable MiMo models per Xiaomi docs (see # tests/providers/test_xiaomi_mimo_thinking.py). mimo-v2-flash is omitted # because it does not support thinking. @@ -692,13 +699,20 @@ class OpenAICompatProvider(LLMProvider): # Only send thinking controls when reasoning_effort is explicit so # omitting the config preserves each provider's default. if reasoning_effort is not None: + slug = _model_slug(model_name) thinking_enabled = semantic_effort not in ("none", "minimal") for thinking_style in _thinking_styles_for(spec, model_name): + if not thinking_enabled and slug in _KIMI_ALWAYS_THINKING_MODELS: + continue extra = _thinking_extra_body(thinking_style, thinking_enabled) if extra: kwargs.setdefault("extra_body", {}).update(extra) gateway_style = getattr(spec, "gateway_reasoning_style", "") if spec else "" - if gateway_style and _model_thinking_style(model_name): + if ( + gateway_style + and _model_thinking_style(model_name) + and (thinking_enabled or slug not in _KIMI_ALWAYS_THINKING_MODELS) + ): extra = _gateway_reasoning_extra_body(gateway_style, semantic_effort) if extra: kwargs.setdefault("extra_body", {}).update(extra) @@ -708,7 +722,7 @@ class OpenAICompatProvider(LLMProvider): # user's intent via the provider-native shape, so drop the # redundant wire-level kwarg. Only kimi models need this — # Xiaomi's API accepts both params. - if _model_slug(model_name) in _KIMI_THINKING_MODELS: + if slug in _KIMI_THINKING_MODELS: kwargs.pop("reasoning_effort", None) if tools: diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index c3d792d9..cf64cb6b 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -352,7 +352,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( default_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", thinking_style="enable_thinking", ), - # Moonshot (月之暗面): Kimi K2.5 / K2.6 enforce temperature >= 1.0. + # Moonshot (月之暗面): Kimi K2.5+ enforce temperature >= 1.0. ProviderSpec( name="moonshot", keywords=("moonshot", "kimi"), @@ -363,6 +363,9 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( model_overrides=( ("kimi-k2.5", {"temperature": 1.0}), ("kimi-k2.6", {"temperature": 1.0}), + ("kimi-k2.7", {"temperature": 1.0}), + ("kimi-k2.7-code", {"temperature": 1.0}), + ("kimi-k2.7-code-highspeed", {"temperature": 1.0}), ), ), # MiniMax: OpenAI-compatible API diff --git a/tests/providers/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py index 27896e58..872e1b51 100644 --- a/tests/providers/test_litellm_kwargs.py +++ b/tests/providers/test_litellm_kwargs.py @@ -1573,12 +1573,49 @@ def test_kimi_k26_thinking_enabled_with_openrouter_prefix() -> None: assert "reasoning_effort" not in kw +def test_kimi_k27_code_thinking_enabled() -> None: + """Kimi K2.7 Code supports native thinking controls.""" + kw = _build_kwargs_for("moonshot", "kimi-k2.7-code", reasoning_effort="medium") + assert kw.get("extra_body") == {"thinking": {"type": "enabled"}} + assert "reasoning_effort" not in kw + + +def test_kimi_k27_code_thinking_enabled_with_openrouter_prefix() -> None: + """OpenRouter-routed Kimi K2.7 Code should carry both thinking shapes.""" + kw = _build_kwargs_for("openrouter", "moonshotai/kimi-k2.7-code", reasoning_effort="high") + assert kw.get("extra_body") == { + "thinking": {"type": "enabled"}, + "reasoning": {"effort": "high"}, + } + assert "reasoning_effort" not in kw + + +def test_kimi_k27_code_thinking_none_omits_disabled() -> None: + """Kimi K2.7 Code is always-thinking; disabled thinking is invalid upstream.""" + kw = _build_kwargs_for("moonshot", "kimi-k2.7-code", reasoning_effort="none") + assert "extra_body" not in kw + assert "reasoning_effort" not in kw + + +def test_kimi_k27_code_thinking_none_with_openrouter_prefix_omits_disabled() -> None: + """OpenRouter-routed Kimi K2.7 Code should not request disabled thinking.""" + kw = _build_kwargs_for("openrouter", "moonshotai/kimi-k2.7-code", reasoning_effort="none") + assert "extra_body" not in kw + assert "reasoning_effort" not in kw + + def test_moonshot_kimi_k26_temperature_override() -> None: """Moonshot registry forces temperature 1.0 for kimi-k2.6 (API requirement).""" kw = _build_kwargs_for("moonshot", "kimi-k2.6", reasoning_effort=None) assert kw["temperature"] == 1.0 +def test_moonshot_kimi_k27_code_temperature_override() -> None: + """Moonshot registry should force temperature 1.0 for Kimi K2.7 Code.""" + kw = _build_kwargs_for("moonshot", "kimi-k2.7-code", reasoning_effort=None) + assert kw["temperature"] == 1.0 + + def test_kimi_k25_thinking_disabled_with_openrouter_prefix() -> None: """OpenRouter names must NOT trigger thinking without reasoning_effort.""" kw = _build_kwargs_for("openrouter", "moonshotai/kimi-k2.5", reasoning_effort=None)