From 84709a00d979c396dfb34f5d60b2992efb5a9284 Mon Sep 17 00:00:00 2001 From: Husam Date: Thu, 30 Jul 2026 13:30:00 +0300 Subject: [PATCH] fix(llm): omit temperature for major-only Opus ids (claude-opus-5) (#5761) The version pattern in _anthropic_rejects_temperature() required a minor component, so major-only ids like `claude-opus-5` never matched and the guard reported that the model accepts `temperature`. Anthropic rejects the field outright on Opus 4.7+, so every such call returned HTTP 400 and the stream aborted with zero tokens ("the model returned an empty response"). Make the minor optional and read a missing minor as `.0`. The major is also capped at 1-2 digits with a no-trailing-digit lookahead, mirroring the minor: once the minor is optional, a greedy major would swallow the date in `claude-3-opus-20240229` and read it as version 20240229, dropping temperature from a model that accepts it. Fixes #5753 Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/llm_core.py | 26 ++++++++++++++++------ tests/test_llm_core_anthropic_temp_omit.py | 26 +++++++++++++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/llm_core.py b/src/llm_core.py index 4dec32376..3e84c1060 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -1237,15 +1237,27 @@ def _anthropic_rejects_temperature(model: str) -> bool: return False # `(?= 4.7. Dated 4.7+ snapshots (`claude-opus-4-7- - # 20260201`) keep their explicit minor and are still matched. - match = re.search(r"(?= 4.7 (issue #5753). Without + # this, every Opus 5 call kept `temperature` and failed with HTTP 400 — visible + # only on paths that pass a temperature, e.g. scheduled tasks inheriting + # `stream_agent_loop`'s 0.3 default, which returned empty responses. + match = re.search( + r"(?= (4, 7) + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + return (major, minor) >= (4, 7) # Reasoning effort level sent to Mistral thinking-capable models. Mistral's # API accepts "high", "medium", "low", "none" — see diff --git a/tests/test_llm_core_anthropic_temp_omit.py b/tests/test_llm_core_anthropic_temp_omit.py index 2274f1dc9..f7d26aef0 100644 --- a/tests/test_llm_core_anthropic_temp_omit.py +++ b/tests/test_llm_core_anthropic_temp_omit.py @@ -29,6 +29,13 @@ from src.llm_core import _anthropic_rejects_temperature, _build_anthropic_payloa "anthropic/claude-opus-4-7", # tolerate a provider-prefixed id "claude-opus-4-10", # future minor still >= 4.7 "claude-opus-5-0", # future major + # Major-only ids: a missing minor reads as `.0`, so these are >= 4.7 too + # (issue #5753). Before the fix the version pattern required a minor, so + # these fell through to "accepts temperature" and every call 400'd. + "claude-opus-5", + "claude-opus-5-20260101", # major-only + dated snapshot + "anthropic/claude-opus-5", # major-only behind a provider prefix + "claude-opus-6", # future major-only ], ) def test_opus_47_plus_rejects_temperature(model): @@ -48,7 +55,10 @@ def test_opus_47_plus_rejects_temperature(model): "claude-opus-4-6-20251201", # dated 4.6 snapshot — older, still keeps temperature "claude-sonnet-4-6", "claude-3-5-sonnet", - "claude-3-opus-20240229", # legacy Claude 3 Opus — no opus-N-M pattern, kept + "claude-3-opus-20240229", # legacy Claude 3 Opus — date directly after + # "opus-", so the major must not swallow it as version 20240229 (that is + # what makes capping the major at 1-2 digits necessary once the minor + # became optional in #5753). "claude-haiku-4-5", "claude-x", "octopus-4-8", # "opus" only as a substring of another word — must not match @@ -87,6 +97,20 @@ def test_payload_keeps_temperature_for_older_models(): assert _payload("claude-3-5-sonnet", 1.2)["temperature"] == 1.0 +def test_payload_omits_temperature_for_major_only_opus_5(): + # Issue #5753: the scheduled-task path calls stream_agent_loop() without a + # temperature and inherits its 0.3 default, so `claude-opus-5` 400'd on every + # run and surfaced as "the model returned an empty response". Interactive chat + # leaves temperature None and never hit it. + assert "temperature" not in _payload("claude-opus-5", 0.3) + + +def test_payload_keeps_temperature_for_legacy_claude_3_opus(): + # Guards the major-digit cap: `opus-20240229` must not parse as version + # 20240229, or Claude 3 Opus would silently lose the caller's temperature. + assert _payload("claude-3-opus-20240229", 0.5)["temperature"] == 0.5 + + def test_payload_keeps_temperature_for_dated_opus_4_0(): # Anthropic's dated id for Opus 4.0 (claude-opus-4-20250514) is in this repo's # ANTHROPIC_MODELS list. The date must not be misread as a >= 4.7 minor, or the