fix: normalize DashScope reasoning_effort (minimal vs minimum)

DashScope rejects the OpenAI-style value "minimal" with
`'reasoning_effort.effort' must be one of: 'none', 'minimum', 'low',
'medium', 'high', 'xhigh'`, but nanobot was passing the string through
verbatim. Users who tried the documented "minimal" to disable thinking
got a 400; users who tried the DashScope-native "minimum" to work
around it got `enable_thinking=True` because the internal comparison
was a hard string match on "minimal".

Introduce a semantic/wire split in `_build_kwargs`:

- `semantic_effort` is the internal canonical form (OpenAI vocabulary).
  "minimum" on the way in is normalized to "minimal" here so both
  spellings share one meaning.
- `wire_effort` is what we actually serialize. For DashScope with
  semantic_effort == "minimal" we translate to "minimum" on the way
  out; other providers are unchanged.
- `thinking_enabled` and the Kimi thinking branch now compare on
  `semantic_effort`, so either user spelling correctly disables
  provider-side thinking.

Tests:

- Strengthen `test_dashscope_thinking_disabled_for_minimal` to assert
  the wire value is "minimum" in addition to the extra_body signal;
  the original version only checked extra_body and let the
  invalid-value bug slip through.
- Add `test_dashscope_thinking_disabled_for_minimum_alias` so a user
  who read the DashScope docs and configured "minimum" still gets
  thinking off.
- Add `test_non_dashscope_minimal_not_retranslated` to pin down that
  the DashScope-specific translation does not leak to OpenAI et al.
This commit is contained in:
hlg
2026-04-22 12:49:55 +08:00
committed by Xubin Ren
parent f6a417e77d
commit 28c42628b0
2 changed files with 49 additions and 4 deletions
+22
View File
@@ -731,10 +731,32 @@ 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."""
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')."""
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."""
kw = _build_kwargs_for("openai", "gpt-5", reasoning_effort="minimal")
assert kw["reasoning_effort"] == "minimal"
def test_dashscope_no_extra_body_when_reasoning_effort_none() -> None:
kw = _build_kwargs_for("dashscope", "qwen-turbo", reasoning_effort=None)
assert "extra_body" not in kw