fix(openai): force reasoning_effort=none even when the caller sent none at all

The learned fast path only rewrote `reasoning_effort` when it was already present
and not "none", so a learned model with the parameter absent kept sending nothing
-- which these models reject exactly like "high", because they apply a server-side
default. The cache recorded the conflict but every call still paid the 400.

Reproduced before fixing:

    LLM(model='openai/gpt-5.6-sol')          # no reasoning_effort anywhere
    1st call  -> 400, recovered, model learned
    2nd call  -> reasoning_effort ABSENT     # 400 again, forever

Now "none" is forced whenever the model is known and the value isn't already
"none". Same call after the fix sends reasoning_effort=none.

`_retry_params_without_reasoning_effort` already handled the absent case correctly;
only the fast path was wrong.

tests/llms/openai -> 160 passed, 1 skipped. Ruff + mypy clean.
This commit is contained in:
alex-clawd
2026-07-26 03:30:33 -07:00
parent 155ba11c4a
commit 4625305a30
2 changed files with 19 additions and 4 deletions

View File

@@ -1929,14 +1929,15 @@ class OpenAICompletion(BaseLLM):
# request; the first occurrence is recovered in `_call_completions`.
# Checked after the tools block on purpose: `reasoning_effort` can also
# arrive through `additional_params`, which bypasses the typed field.
# An absent `reasoning_effort` is not safe either: these models apply a
# server-side default, so sending nothing is rejected exactly like sending
# "high". Only an explicit "none" is accepted alongside tools.
if (
params.get("tools")
and params.get("reasoning_effort") not in (None, "none")
and params.get("reasoning_effort") != "none"
and not self._supports_reasoning_effort_with_tools(self.model)
):
requested = params["reasoning_effort"]
# Explicit "none" rather than removing the key: gpt-5.6-* apply a
# server-side default, so omitting it fails the same way.
requested = params.get("reasoning_effort")
params["reasoning_effort"] = "none"
logging.debug(
'Forcing reasoning_effort="none" (requested %r) for model %r: '

View File

@@ -108,6 +108,20 @@ class TestLearnedFastPath:
assert params["tools"]
assert params["tool_choice"] == "auto"
def test_absent_reasoning_effort_is_also_forced_to_none(self):
"""Sending nothing is rejected exactly like sending "high".
These models apply a server-side default, so a learned model with no
`reasoning_effort` set would 400 on every call and never benefit from the
cache.
"""
llm = build("gpt-5.6-sol")
llm._remember_reasoning_effort_conflict()
params = llm._prepare_completion_params(MESSAGES, tools=TOOLS)
assert params["reasoning_effort"] == "none"
def test_learning_applies_to_the_additional_params_leak(self):
"""additional_params bypasses the typed field, so it must be checked too."""
llm = build("gpt-5.5", additional_params={"reasoning_effort": "medium"})