Skip to content

fix(llm): omit temperature for major-only Opus ids (claude-opus-5) - #5761

Open
husamemadH wants to merge 1 commit into
odysseus-dev:devfrom
husamemadH:fix/5753-opus-major-only-temperature-guard
Open

fix(llm): omit temperature for major-only Opus ids (claude-opus-5)#5761
husamemadH wants to merge 1 commit into
odysseus-dev:devfrom
husamemadH:fix/5753-opus-major-only-temperature-guard

Conversation

@husamemadH

Copy link
Copy Markdown

Summary

_anthropic_rejects_temperature() decides whether to send temperature on the native Anthropic path — Opus 4.7+ returns HTTP 400 if the field is present at all. Its version pattern required a minor component (opus-(\d+)[-_.](\d{1,2})), so a major-only id like claude-opus-5 never matched, the guard reported "this model accepts temperature", and the field was sent. Every such call fails with `temperature` is deprecated for this model, aborting the stream in ~0.4s with zero tokens — surfaced in the UI as "The model returned an empty response."

This makes the minor optional and treats a missing minor as .0, so claude-opus-5 parses as (5, 0) and is correctly gated as >= 4.7.

The major is also capped at 1–2 digits with a no-trailing-digit lookahead, mirroring the minor. This is required once the minor becomes optional: with a greedy (\d+) major, claude-3-opus-20240229 (legacy Claude 3 Opus, date directly after opus-) would parse as version 20240229 and silently lose its temperature — a model that accepts it. The old pattern avoided that only as a side effect of requiring a minor.

Interactive chat never hit this: it leaves temperature as None so nothing is sent. The scheduled-task path calls stream_agent_loop() without a temperature and inherits its temperature: float = 0.3 default, which is sent — so background tasks broke while chat looked healthy.

Target branch

  • This PR targets dev, not main.

Linked Issue

Fixes #5753

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app and verified the change works end-to-end. Verified at the payload level instead — see Verification below.

How to Test

  1. Reproduce on dev (the guard answers False for a model that is newer than 4.7):

    docker compose run --rm --no-deps -T --entrypoint python odysseus -c '
    from src.llm_core import _anthropic_rejects_temperature, _build_anthropic_payload
    print("rejects:", _anthropic_rejects_temperature("claude-opus-5"))
    p = _build_anthropic_payload("claude-opus-5", [{"role":"user","content":"hi"}], 0.3, 100)
    print("temperature in payload:", "temperature" in p)'

    Before: rejects: False / temperature in payload: True (the 0.3 is the scheduled-task default).
    After: rejects: True / temperature in payload: False.

  2. Full predicate table from the issue, plus the legacy Claude 3 Opus case:

    model id before after expected
    claude-opus-5 False True True
    claude-opus-5-20260101 False True True
    claude-opus-4-8 True True True
    claude-opus-4-7 True True True
    claude-opus-4-6 False False False
    claude-opus-4-20250514 False False False
    octopus-4-8 False False False
    claude-3-opus-20240229 False False False
  3. Tests:

    python -m pytest tests/test_llm_core_anthropic_temp_omit.py tests/test_llm_core_anthropic_temp_clamp.py -q

    42 passed. Also ran python -m pytest tests/ -k "llm_core or temperature or anthropic" — 235 passed.

Note on the fix suggested in the issue

The issue proposes making the minor optional while leaving the major as (\d+). That regresses claude-3-opus-20240229 to True (the greedy major swallows the date as version 20240229), which fails the existing case at tests/test_llm_core_anthropic_temp_omit.py:51. Capping the major at 1–2 digits handles both; the issue's table didn't include a legacy Claude 3 Opus id, which is why it looked clean.

Verification

Verified at the payload level, which is where this bug lives — the field is either in the request body or it isn't:

  • Ran _anthropic_rejects_temperature() and _build_anthropic_payload() against the real module in a container built from dev, before and after the change. Before: claude-opus-5 keeps temperature. After: it is omitted.
  • Confirmed the full predicate table above, including the two dated-id edge cases and octopus-4-8.
  • tests/test_llm_core_anthropic_temp_omit.py + tests/test_llm_core_anthropic_temp_clamp.py: 42 passed.
  • pytest tests/ -k "llm_core or temperature or anthropic": 235 passed, no regressions.

The HTTP 400 itself is as described in #5753; this change removes the field that triggers it.

Disclosure

The patch was drafted with Claude Code. I reproduced the bug myself, reviewed the regex change line by line, and ran the tests before opening this. Flagging it per CONTRIBUTING.md's note on agent-generated PRs — the issue (#5753) was filed independently and this is a single targeted fix, not a bulk submission.

Files changed

  • src/llm_core.py — version pattern in _anthropic_rejects_temperature(), plus comments explaining both digit caps.
  • tests/test_llm_core_anthropic_temp_omit.py — major-only ids added to the "rejects" set (claude-opus-5, dated, provider-prefixed, future claude-opus-6); payload test for the scheduled-task path; explicit payload guard for legacy Claude 3 Opus.

No UI/rendering changes.

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 odysseus-dev#5753
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

temperature guard misses major-only Anthropic model ids (claude-opus-5): scheduled tasks silently return empty responses

1 participant