Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 52 additions & 25 deletions agent/transports/chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,12 @@ def build_kwargs(
# Temperature
fixed_temperature: Any — from _fixed_temperature_for_model()
omit_temperature: bool
# Reasoning
supports_reasoning: bool
# Reasoning / Thinking (mode string unifies multiple providers)
# "generic" — extra_body.reasoning (OpenRouter, Nous, GitHub Models)
# "kimi" — top-level reasoning_effort + extra_body.thinking (Kimi/Moonshot)
# "deepseek" — top-level reasoning_effort + extra_body.thinking (DeepSeek direct API)
supports_reasoning: bool # kept for backward compat; consumers should prefer thinking_mode
thinking_mode: str | None # one of None, "generic", "kimi", "deepseek"
github_reasoning_extra: dict | None
# Claude on OpenRouter/Nous max output
anthropic_max_output: int | None
Expand Down Expand Up @@ -203,20 +207,12 @@ def build_kwargs(
elif anthropic_max_out is not None:
api_kwargs["max_tokens"] = anthropic_max_out

# Kimi: top-level reasoning_effort (unless thinking disabled)
if is_kimi:
_kimi_thinking_off = bool(
reasoning_config
and isinstance(reasoning_config, dict)
and reasoning_config.get("enabled") is False
)
if not _kimi_thinking_off:
_kimi_effort = "medium"
if reasoning_config and isinstance(reasoning_config, dict):
_e = (reasoning_config.get("effort") or "").strip().lower()
if _e in ("low", "medium", "high"):
_kimi_effort = _e
api_kwargs["reasoning_effort"] = _kimi_effort
# ── Thinking / Reasoning parameters ────────────────────────────
# Unified via thinking_mode so each provider uses a single branch.
# Backward compat: if thinking_mode is not set, falls back to
# is_kimi / supports_reasoning flags.
thinking_mode = params.get("thinking_mode")
supports_reasoning = params.get("supports_reasoning", False)

# extra_body assembly
extra_body: Dict[str, Any] = {}
Expand All @@ -229,18 +225,49 @@ def build_kwargs(
if provider_prefs and is_openrouter:
extra_body["provider"] = provider_prefs

# Kimi extra_body.thinking
if is_kimi:
_kimi_thinking_enabled = True
if reasoning_config and isinstance(reasoning_config, dict):
if reasoning_config.get("enabled") is False:
_kimi_thinking_enabled = False
# ── Kimi: top-level reasoning_effort + extra_body.thinking ─────
if thinking_mode == "kimi" or (thinking_mode is None and is_kimi):
_k_thinking_off = bool(
reasoning_config
and isinstance(reasoning_config, dict)
and reasoning_config.get("enabled") is False
)
if not _k_thinking_off:
_k_effort = "medium"
if reasoning_config and isinstance(reasoning_config, dict):
_e = (reasoning_config.get("effort") or "").strip().lower()
if _e in ("low", "medium", "high"):
_k_effort = _e
api_kwargs["reasoning_effort"] = _k_effort
extra_body["thinking"] = {
"type": "enabled" if not _k_thinking_off else "disabled",
}

# ── DeepSeek: top-level reasoning_effort + extra_body.thinking ─
# DeepSeek V4-Pro/V4-Flash use {high, max} for effort and require
# thinking.type in extra_body. Temperature/top_p are not supported
# while thinking is active.
elif thinking_mode == "deepseek":
_ds_thinking_off = bool(
reasoning_config
and isinstance(reasoning_config, dict)
and reasoning_config.get("enabled") is False
)
if not _ds_thinking_off:
_ds_effort = "max"
if reasoning_config and isinstance(reasoning_config, dict):
_e = (reasoning_config.get("effort") or "").strip().lower()
if _e in ("high", "max"):
_ds_effort = _e
api_kwargs["reasoning_effort"] = _ds_effort
# DeepSeek does not support temperature/top_p with thinking
api_kwargs.pop("temperature", None)
extra_body["thinking"] = {
"type": "enabled" if _kimi_thinking_enabled else "disabled",
"type": "enabled" if not _ds_thinking_off else "disabled",
}

# Reasoning
if params.get("supports_reasoning", False):
# ── Generic reasoning (OpenRouter / Nous / GitHub Models) ─────
if supports_reasoning:
if is_github_models:
gh_reasoning = params.get("github_reasoning_extra")
if gh_reasoning is not None:
Expand Down
27 changes: 27 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7401,11 +7401,15 @@ def _build_api_kwargs(self, api_messages: list) -> dict:
)
_is_nous = "nousresearch" in self._base_url_lower
_is_nvidia = "integrate.api.nvidia.com" in self._base_url_lower
# thinking_mode replaces is_kimi — the transport branch selects Kimi
# or DeepSeek semantics from a single string value. is_kimi is kept
# here only for the max_tokens default (32000) in build_kwargs.
_is_kimi = (
base_url_host_matches(self.base_url, "api.kimi.com")
or base_url_host_matches(self.base_url, "moonshot.ai")
or base_url_host_matches(self.base_url, "moonshot.cn")
)
_thinking_mode = self._thinking_provider()

# Temperature: _fixed_temperature_for_model may return OMIT_TEMPERATURE
# sentinel (temperature omitted entirely), a numeric override, or None.
Expand Down Expand Up @@ -7465,6 +7469,7 @@ def _build_api_kwargs(self, api_messages: list) -> dict:
ephemeral_max_output_tokens=_ephemeral_out,
max_tokens_param_fn=self._max_tokens_param,
reasoning_config=self.reasoning_config,
thinking_mode=_thinking_mode,
request_overrides=self.request_overrides,
session_id=getattr(self, "session_id", None),
model_lower=(self.model or "").lower(),
Expand All @@ -7487,6 +7492,28 @@ def _build_api_kwargs(self, api_messages: list) -> dict:
anthropic_max_output=_ant_max,
)

def _thinking_provider(self) -> str | None:
"""Return a thinking_mode identifier for directly-routed providers
that use thinking/reasoning_effort (Kimi, DeepSeek, etc.).

Returns None for aggregator routes (OpenRouter, Nous, GitHub Models)
that use the generic extra_body.reasoning path instead.

Design intent:
Each provider uses exactly one branch for thinking params.
Adding a new provider means one elif here + one elif in the
transport's build_kwargs. No flag proliferation.
"""
if (
base_url_host_matches(self._base_url_lower, "api.kimi.com")
or base_url_host_matches(self._base_url_lower, "moonshot.ai")
or base_url_host_matches(self._base_url_lower, "moonshot.cn")
):
return "kimi"
if base_url_host_matches(self._base_url_lower, "api.deepseek.com"):
return "deepseek"
return None

def _supports_reasoning_extra_body(self) -> bool:
"""Return True when reasoning extra_body is safe to send for this route/model.

Expand Down
94 changes: 94 additions & 0 deletions tests/agent/transports/test_chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,100 @@ def test_non_moonshot_tools_are_not_mutated(self, transport):
assert "type" not in kw["tools"][0]["function"]["parameters"]["properties"]["q"]


class TestChatCompletionsDeepSeek:
"""DeepSeek thinking mode via the thinking_mode='deepseek' parameter."""

def test_deepseek_thinking_enabled(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro",
messages=[{"role": "user", "content": "Hi"}],
thinking_mode="deepseek",
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["extra_body"]["thinking"] == {"type": "enabled"}
assert kw["reasoning_effort"] == "max"

def test_deepseek_thinking_disabled(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro",
messages=[{"role": "user", "content": "Hi"}],
thinking_mode="deepseek",
reasoning_config={"enabled": False},
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["extra_body"]["thinking"] == {"type": "disabled"}
assert "reasoning_effort" not in kw

def test_deepseek_thinking_custom_effort(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro",
messages=[{"role": "user", "content": "Hi"}],
thinking_mode="deepseek",
reasoning_config={"effort": "high"},
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["extra_body"]["thinking"] == {"type": "enabled"}
assert kw["reasoning_effort"] == "high"

def test_deepseek_thinking_removes_temperature(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro",
messages=[{"role": "user", "content": "Hi"}],
thinking_mode="deepseek",
fixed_temperature=0.7,
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert "temperature" not in kw
assert kw["extra_body"]["thinking"] == {"type": "enabled"}


class TestChatCompletionsThinkingModeUnified:
"""Test that thinking_mode='kimi' works the same as the old is_kimi=True flag."""

def test_kimi_via_thinking_mode_effort(self, transport):
kw = transport.build_kwargs(
model="kimi-k2",
messages=[{"role": "user", "content": "Hi"}],
thinking_mode="kimi",
reasoning_config={"effort": "high"},
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["reasoning_effort"] == "high"
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_kimi_via_thinking_mode_disabled(self, transport):
kw = transport.build_kwargs(
model="kimi-k2",
messages=[{"role": "user", "content": "Hi"}],
thinking_mode="kimi",
reasoning_config={"enabled": False},
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert "reasoning_effort" not in kw
assert kw["extra_body"]["thinking"] == {"type": "disabled"}

def test_thinking_mode_none_falls_back_to_is_kimi(self, transport):
"""When thinking_mode is not set, is_kimi flag still works."""
kw = transport.build_kwargs(
model="kimi-k2",
messages=[{"role": "user", "content": "Hi"}],
is_kimi=True,
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["reasoning_effort"] == "medium"
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_generic_thinking_mode_none_with_supports_reasoning(self, transport):
"""When thinking_mode is None, supports_reasoning flag still works."""
kw = transport.build_kwargs(
model="deepseek/deepseek-v4",
messages=[{"role": "user", "content": "Hi"}],
supports_reasoning=True,
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw.get("extra_body", {}).get("reasoning") == {"enabled": True, "effort": "medium"}


class TestChatCompletionsValidate:

def test_none(self, transport):
Expand Down