An unset agent.reasoning_effort already resolves to medium on the Nous Portal,
OpenRouter, AI Gateway and Copilot routes (each profile fills it in
build_api_kwargs_extras). The custom / OpenAI-compatible profile — every
`providers.<name>` block and `--provider custom` — omitted the field instead,
so the endpoint's own default applied; for moonshotai/kimi-k3 that is `max`:
3x the reasoning tokens and ~3x the latency of medium, measured live.
The default is resolved at request time in _reasoning_config_for_wire via
ProviderProfile.default_reasoning_config (the custom profile answers medium),
so it is recorded as what actually went out and the reasoning-rejection
ladder keeps working: a 400 on the field turns the rest of the session back
to "omit". It never touches an explicit effort (low stays low, none stays
none), stays off non chat-completions transports (the Anthropic adapter's
unset = no thinking kwargs stands), off models the catalog or model_overrides
mark supports_reasoning: false, and off local Ollama models pulled without
the thinking capability. Auxiliary calls are untouched: they hand the profile
reasoning_config=None directly, which still omits the field.
Live wire capture (token-injecting proxy, providers.probe -> kimi-k3):
before req_reasoning: {}
after req_reasoning: {'reasoning_effort': 'medium'}
agent.reasoning_effort: low -> {'reasoning_effort': 'low'} (unchanged)
92 lines
4.4 KiB
Python
92 lines
4.4 KiB
Python
"""An unset ``agent.reasoning_effort`` resolves to ``medium`` on custom / OpenAI-compatible routes.
|
|
|
|
The Nous, OpenRouter and AI Gateway profiles already fill medium when no effort is configured; the
|
|
custom profile (``providers.<name>`` blocks, ``--provider custom``) omitted the field, so the
|
|
endpoint's own default applied — for kimi-k3 that is ``max``: 3x the reasoning tokens and ~3x the
|
|
latency of medium. The default is decided at request time in ``_reasoning_config_for_wire`` so the
|
|
rejection ladder sees exactly what went out, and it never overrides an explicit effort.
|
|
"""
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from agent.chat_completion_helpers import _build_api_kwargs_for_mode
|
|
from agent.models_dev import ModelCapabilities
|
|
from agent.transports.chat_completions import ChatCompletionsTransport
|
|
from providers import get_provider_profile
|
|
|
|
|
|
class _Agent:
|
|
"""The attributes ``_build_api_kwargs_for_mode`` reads before handing off to the transport builder."""
|
|
|
|
def __init__(self, reasoning_config, *, provider="custom:relay", api_mode="chat_completions"):
|
|
self.reasoning_config = reasoning_config
|
|
self.provider = provider
|
|
self.model = "moonshotai/kimi-k3"
|
|
self.api_mode = api_mode
|
|
self.base_url = "http://relay.example/v1"
|
|
self.tools = []
|
|
self.request_overrides = None
|
|
self.service_tier = None
|
|
self._fast_until = 0.0
|
|
self._ephemeral_reasoning_off = False
|
|
self._reasoning_effort_rejected = False
|
|
self._reasoning_disable_rejected = False
|
|
self._ollama_num_ctx: int | None = None
|
|
|
|
def __getattr__(self, name):
|
|
return lambda *args, **kwargs: None
|
|
|
|
|
|
def _wire_reasoning_config(agent):
|
|
"""The ``reasoning_config`` the main loop hands the transport for this agent's next request."""
|
|
def _capture(agent, api_messages, tools_for_api, reasoning_config, request_overrides, cache_scope_id):
|
|
return {"reasoning_config": reasoning_config}
|
|
|
|
with patch("agent.chat_completion_helpers._build_chat_completions_kwargs", _capture):
|
|
return _build_api_kwargs_for_mode(agent, [], [])["reasoning_config"]
|
|
|
|
|
|
def _custom_wire_field(reasoning_config):
|
|
kwargs = ChatCompletionsTransport().build_kwargs(
|
|
"moonshotai/kimi-k3", [{"role": "user", "content": "hi"}], tools=None,
|
|
provider_profile=get_provider_profile("custom:relay"), reasoning_config=reasoning_config,
|
|
base_url="http://relay.example/v1",
|
|
)
|
|
return kwargs.get("reasoning_effort")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"configured, expected_wire",
|
|
[(None, "medium"), ({"enabled": True, "effort": "low"}, "low"), ({"enabled": False}, "none")],
|
|
)
|
|
def test_unset_effort_goes_out_as_medium_and_explicit_efforts_are_untouched(configured, expected_wire):
|
|
agent = _Agent(configured)
|
|
with patch("agent.models_dev.get_model_capabilities", return_value=None):
|
|
sent = _wire_reasoning_config(agent)
|
|
assert _custom_wire_field(sent) == expected_wire
|
|
# The rejection ladder classifies the NEXT 400 from what was recorded as sent.
|
|
assert agent._wire_reasoning_config == sent
|
|
|
|
|
|
def test_unset_effort_default_keeps_the_field_off_where_it_would_be_wrong():
|
|
# A model the catalog / model_overrides mark as non-reasoning.
|
|
with patch("agent.models_dev.get_model_capabilities", return_value=ModelCapabilities(supports_reasoning=False)):
|
|
assert _wire_reasoning_config(_Agent(None)) is None
|
|
with patch("agent.models_dev.get_model_capabilities", return_value=None):
|
|
# The route already rejected the reasoning field this session (#112781 ladder → route default).
|
|
rejected = _Agent(None)
|
|
rejected._reasoning_effort_rejected = True
|
|
assert _wire_reasoning_config(rejected) is None
|
|
# A local Ollama model pulled without the thinking capability 400s on reasoning_effort.
|
|
ollama = _Agent(None)
|
|
ollama._ollama_num_ctx = 8192
|
|
ollama._ollama_supports_thinking_cached = lambda: False
|
|
assert _wire_reasoning_config(ollama) is None
|
|
# Profiles that decide inside build_api_kwargs_extras (Nous fills medium itself) get None as before.
|
|
assert _wire_reasoning_config(_Agent(None, provider="nous")) is None
|
|
# Not a chat-completions wire: the Anthropic adapter's "unset = no thinking kwargs" stands.
|
|
from agent.reasoning_params import unset_reasoning_default
|
|
assert unset_reasoning_default(_Agent(None, api_mode="anthropic_messages")) is None
|
|
|