diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 457bf4a2..4f726df2 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -387,16 +387,9 @@ class OpenAICompatProvider(LLMProvider): kwargs.update(overrides) break - # Semantic vs. wire distinction for reasoning_effort. - # - semantic_effort is nanobot's internal canonical form (OpenAI's - # vocabulary: "minimal" / "low" / "medium" / "high"). It drives - # decisions like whether to disable provider thinking modes. - # - wire_effort is what we actually serialize to the provider; some - # providers (notably DashScope) reject "minimal" and require - # "minimum" instead. We accept either spelling on input and - # always compare on the semantic form so a user who configured - # "minimum" (DashScope's native spelling) still gets thinking - # disabled instead of accidentally enabled. + # Normalize reasoning_effort into a semantic form (OpenAI vocab) + # used for internal decisions, and a wire form actually sent out. + # "minimum" is accepted as a DashScope-native alias for "minimal". semantic_effort: str | None = None if isinstance(reasoning_effort, str): semantic_effort = reasoning_effort.lower() @@ -405,9 +398,7 @@ class OpenAICompatProvider(LLMProvider): wire_effort = reasoning_effort if spec and spec.name == "dashscope" and semantic_effort == "minimal": - # DashScope's reasoning_effort.effort enum accepts: none / - # minimum / low / medium / high / xhigh. Literal "minimal" - # returns 400 invalid_value; translate on the outbound side. + # DashScope accepts none/minimum/low/medium/high/xhigh; "minimal" 400s. wire_effort = "minimum" if wire_effort: diff --git a/tests/providers/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py index 5067f094..41188c72 100644 --- a/tests/providers/test_litellm_kwargs.py +++ b/tests/providers/test_litellm_kwargs.py @@ -731,28 +731,21 @@ def test_dashscope_thinking_enabled_with_reasoning_effort() -> None: def test_dashscope_thinking_disabled_for_minimal() -> None: - """OpenAI-style 'minimal' → DashScope wire value 'minimum' + thinking off. - DashScope rejects the literal string 'minimal' (invalid_value), so we - must translate on the outbound side while still honouring the 'no - thinking' intent via extra_body.""" + """'minimal' → wire 'minimum' + thinking off on DashScope.""" kw = _build_kwargs_for("dashscope", "qwen3-plus", reasoning_effort="minimal") assert kw["reasoning_effort"] == "minimum" assert kw["extra_body"] == {"enable_thinking": False} def test_dashscope_thinking_disabled_for_minimum_alias() -> None: - """Users who read DashScope docs may configure the native 'minimum' - spelling. Internally it's the same semantic as 'minimal' → thinking - must still be disabled (not enabled just because the string isn't - literally 'minimal').""" + """Native 'minimum' spelling must also disable thinking, not enable it.""" kw = _build_kwargs_for("dashscope", "qwen3-plus", reasoning_effort="minimum") assert kw["reasoning_effort"] == "minimum" assert kw["extra_body"] == {"enable_thinking": False} def test_non_dashscope_minimal_not_retranslated() -> None: - """The DashScope-specific translation must not leak to other providers; - OpenAI / Anthropic / etc. speak 'minimal' natively.""" + """DashScope-specific translation must not leak to other providers.""" kw = _build_kwargs_for("openai", "gpt-5", reasoning_effort="minimal") assert kw["reasoning_effort"] == "minimal"