feat(provider): enable Kimi thinking via extra_body for k2.5 and k2.6
- Inject `thinking={"type": "enabled|disabled"}` via extra_body for
Kimi thinking-capable models (kimi-k2.5, k2.6-code-preview).
- Add _is_kimi_thinking_model helper to handle both bare slugs and
OpenRouter-style prefixed names (e.g. moonshotai/kimi-k2.5).
- reasoning_effort="minimal" maps to disabled; any other value enables it.
- Add tests for enabled/disabled states and OpenRouter prefix handling.
This commit is contained in:
@@ -50,6 +50,29 @@ _DEFAULT_OPENROUTER_HEADERS = {
|
|||||||
"X-OpenRouter-Title": "nanobot",
|
"X-OpenRouter-Title": "nanobot",
|
||||||
"X-OpenRouter-Categories": "cli-agent,personal-agent",
|
"X-OpenRouter-Categories": "cli-agent,personal-agent",
|
||||||
}
|
}
|
||||||
|
_KIMI_THINKING_MODELS: frozenset[str] = frozenset({
|
||||||
|
"kimi-k2.5",
|
||||||
|
"k2.6-code-preview",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _is_kimi_thinking_model(model_name: str) -> bool:
|
||||||
|
"""Return True if model_name refers to a Kimi thinking-capable model.
|
||||||
|
|
||||||
|
Supports two forms:
|
||||||
|
- Exact match: kimi-k2.5 in _KIMI_THINKING_MODELS
|
||||||
|
- Slug match: moonshotai/kimi-k2.5 -> the part after the last "/"
|
||||||
|
is checked against _KIMI_THINKING_MODELS
|
||||||
|
|
||||||
|
This covers both the native Moonshot provider (bare slug) and
|
||||||
|
OpenRouter-style names (``"publisher/slug"``).
|
||||||
|
"""
|
||||||
|
name = model_name.lower()
|
||||||
|
if name in _KIMI_THINKING_MODELS:
|
||||||
|
return True
|
||||||
|
if "/" in name and name.rsplit("/", 1)[1] in _KIMI_THINKING_MODELS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _short_tool_id() -> str:
|
def _short_tool_id() -> str:
|
||||||
@@ -363,6 +386,16 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
if extra:
|
if extra:
|
||||||
kwargs.setdefault("extra_body", {}).update(extra)
|
kwargs.setdefault("extra_body", {}).update(extra)
|
||||||
|
|
||||||
|
# Model-level thinking injection for Kimi thinking-capable models.
|
||||||
|
# Strip any provider prefix (e.g. "moonshotai/") before the set lookup
|
||||||
|
# so that OpenRouter-style names like "moonshotai/kimi-k2.5" are handled
|
||||||
|
# identically to bare names like "kimi-k2.5".
|
||||||
|
if reasoning_effort is not None and _is_kimi_thinking_model(model_name):
|
||||||
|
thinking_enabled = reasoning_effort.lower() != "minimal"
|
||||||
|
kwargs.setdefault("extra_body", {}).update(
|
||||||
|
{"thinking": {"type": "enabled" if thinking_enabled else "disabled"}}
|
||||||
|
)
|
||||||
|
|
||||||
if tools:
|
if tools:
|
||||||
kwargs["tools"] = tools
|
kwargs["tools"] = tools
|
||||||
kwargs["tool_choice"] = tool_choice or "auto"
|
kwargs["tool_choice"] = tool_choice or "auto"
|
||||||
|
|||||||
@@ -730,3 +730,50 @@ def test_openai_no_thinking_extra_body() -> None:
|
|||||||
"""Non-thinking providers should never get extra_body for thinking."""
|
"""Non-thinking providers should never get extra_body for thinking."""
|
||||||
kw = _build_kwargs_for("openai", "gpt-4o", reasoning_effort="medium")
|
kw = _build_kwargs_for("openai", "gpt-4o", reasoning_effort="medium")
|
||||||
assert "extra_body" not in kw
|
assert "extra_body" not in kw
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k25_thinking_enabled() -> None:
|
||||||
|
"""kimi-k2.5 with reasoning_effort set should opt in to thinking."""
|
||||||
|
kw = _build_kwargs_for("moonshot", "kimi-k2.5", reasoning_effort="medium")
|
||||||
|
assert kw.get("extra_body") == {"thinking": {"type": "enabled"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k25_thinking_disabled_for_minimal() -> None:
|
||||||
|
"""reasoning_effort='minimal' maps to thinking disabled for kimi-k2.5."""
|
||||||
|
kw = _build_kwargs_for("moonshot", "kimi-k2.5", reasoning_effort="minimal")
|
||||||
|
assert kw.get("extra_body") == {"thinking": {"type": "disabled"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k25_no_extra_body_when_reasoning_effort_none() -> None:
|
||||||
|
"""Without reasoning_effort the thinking param must not be injected."""
|
||||||
|
kw = _build_kwargs_for("moonshot", "kimi-k2.5", reasoning_effort=None)
|
||||||
|
assert "extra_body" not in kw
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k25_thinking_enabled_with_openrouter_prefix() -> None:
|
||||||
|
"""OpenRouter-style model names like moonshotai/kimi-k2.5 must trigger thinking."""
|
||||||
|
kw = _build_kwargs_for("openrouter", "moonshotai/kimi-k2.5", reasoning_effort="medium")
|
||||||
|
assert kw.get("extra_body") == {"thinking": {"type": "enabled"}}
|
||||||
|
|
||||||
|
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)
|
||||||
|
assert "extra_body" not in kw
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k26_code_preview_thinking_enabled() -> None:
|
||||||
|
"""k2.6-code-preview also supports thinking; should behave like k2.5."""
|
||||||
|
kw = _build_kwargs_for("moonshot", "k2.6-code-preview", reasoning_effort="high")
|
||||||
|
assert kw.get("extra_body") == {"thinking": {"type": "enabled"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k2_series_no_thinking_injection() -> None:
|
||||||
|
"""kimi-k2 (non-thinking) models must NOT receive extra_body.thinking."""
|
||||||
|
kw = _build_kwargs_for("moonshot", "kimi-k2", reasoning_effort="high")
|
||||||
|
assert "extra_body" not in kw
|
||||||
|
|
||||||
|
|
||||||
|
def test_kimi_k2_thinking_series_no_thinking_injection() -> None:
|
||||||
|
"""kimi-k2-thinking series models must NOT receive extra_body.thinking."""
|
||||||
|
kw = _build_kwargs_for("moonshot", "kimi-k2-thinking", reasoning_effort="high")
|
||||||
|
assert "extra_body" not in kw
|
||||||
|
|||||||
Reference in New Issue
Block a user