Skip to content

fix(anthropic): support the Claude 4.7+ and 5 model generations - #6868

Open
sharkfabri wants to merge 3 commits into
livekit:mainfrom
sharkfabri:fix/anthropic-claude-5-compat
Open

fix(anthropic): support the Claude 4.7+ and 5 model generations#6868
sharkfabri wants to merge 3 commits into
livekit:mainfrom
sharkfabri:fix/anthropic-claude-5-compat

Conversation

@sharkfabri

Copy link
Copy Markdown

Problem

The Anthropic plugin decides what to put in a request from hardcoded model ids, and Anthropic has changed the request contract twice since Claude 4.6. Every model released after that list was written now sends a request the API rejects.

  1. Prefilling. _NO_PREFILL_PATTERNS lists claude-sonnet-4-6 and claude-opus-4-6 literally, so on Opus 4.7/4.8, Opus 5 and Sonnet 5 the plugin still sends a trailing assistant message and Anthropic 400 Error on Claude 4.6 - "Prefilling assistant messages is no longer supported" #4907 ("Prefilling assistant messages is no longer supported") comes back.

  2. Sampling parameters. temperature, top_p and top_k are rejected with a 400 from Claude 4.7 onwards, so a documented constructor argument breaks every request:

    anthropic.LLM(model="claude-opus-5", temperature=0.7)
  3. Thinking. On Sonnet 5 and Opus 5 thinking is on unless the request turns it off, and max_tokens caps thinking and the reply together — with the plugin's 1024 default a voice turn can spend most of its budget reasoning and come back truncated. On the way out, thinking_delta and signature_delta fall through _parse_event, so the reasoning is generated and billed but never surfaces anywhere.

Separately, LLMStream._run is re-entered on the same instance on every retry and nothing clears the state of the failed attempt.

Fix

Two commits.

reset per-attempt state before a stream retry — the framework only retries when the attempt emitted no chunk, which is exactly when a <thinking> block or a tool call was left open. _ignoring_cot left set makes the retry's answer get swallowed as chain of thought; a half-read tool call keeps collecting arguments from the next attempt. Both are cleared at the top of _run.

support the Claude 4.7+ and 5 generations — the hardcoded id list is replaced by the generation read out of the model id, and the three behaviours are gated on it:

applies from behaviour
prefilling 4.6 a trailing assistant message is closed with a user turn
sampling params 4.7 temperature/top_p/top_k are dropped, warned once per instance
thinking 4.6 thinking: {"type": "disabled"} is sent; extra_kwargs still wins

Version parsing anchors on the family name, so gateway and snapshot ids keep resolving to the underlying model: anthropic.claude-opus-5, claude-opus-4-5@20251101, claude-opus-5[1m], gw-1-claude-opus-5. When the family cannot be found in the id — a proxy alias such as my-claude-proxy — no version is returned and every guard keeps the behaviour it had before, rather than guessing at the model's capabilities.

thinking_delta and signature_delta are now recognised: the reasoning is kept out of the caller's text (a voice agent would speak it) and logged at debug level, with a one-time warning if thinking appears when it was not requested.

models.py gains Opus 4.5/4.7/4.8, Opus 5, Sonnet 4.5, Sonnet 5 and Haiku 4.5, and marks the ids that now return 404. claude-fable-5 and claude-mythos-5 are deliberately left out — thinking cannot be turned off on them and a single request can run for minutes, which does not fit a voice session — but the runtime guard still recognises them, since model accepts any string.

Behaviour change

Requests to 4.6+ models now carry thinking: {"type": "disabled"} where they previously carried nothing. That is a no-op on 4.6/4.7/4.8, where thinking was already off by default, and a real change on Sonnet 5 / Opus 5, where it was on. Callers who want the model to reason opt back in with chat(extra_kwargs={"thinking": {"type": "adaptive"}}); note that thinking blocks are not kept in the chat context, so a turn carrying a tool call is replayed without them, which Anthropic can reject — the plugin logs a warning when that combination is used.

Tests

tests/test_plugin_anthropic.py moves from the plugin category to unit. It is hermetic — fake client, no API key, no network — and no CI job runs --plugin, so nothing in it was running; the other hermetic plugin tests (cerebras, aws, openai, google) are already marked unit.

uv run pytest tests/test_plugin_anthropic.py --unit   # 94 passed
uv run pytest --unit                                  # 2055 passed, 5 skipped
make format-check && make lint
uv run mypy -p livekit.plugins.anthropic

Coverage: version parsing including gateway/snapshot/unparseable ids, the prefill guard as a regression test for #4907, sampling parameters from both the constructor and extra_kwargs, the thinking configuration per model family, max_tokens defaults, the stream-side thinking blocks, and the per-attempt state reset.

@sharkfabri
sharkfabri requested a review from a team as a code owner August 15, 2026 09:37
@CLAassistant

CLAassistant commented Aug 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

devin-ai-integration[bot]

This comment was marked as resolved.

`LLMStream._run` is re-entered on the same instance on every retry, and the
framework only retries when the failed attempt emitted no chunk — which is
exactly the case where that attempt left state behind. A stream that dies while
Claude is inside a `<thinking>` block leaves `_ignoring_cot` set, and the retry's
answer is then swallowed as chain of thought; one that dies mid tool call leaves
a half-read call the next attempt keeps appending arguments to.

The test module also moves from the `plugin` category to `unit`: it is hermetic
(fake client, no API key, no network) and no CI job runs `--plugin`, so nothing
in it ever runs. The other hermetic plugin tests (cerebras, aws, openai, google)
are already marked `unit`.
The plugin carries assumptions that Anthropic has broken twice since Claude 4.6,
and that have to be patched again on every release:

- Prefilling. `_NO_PREFILL_PATTERNS` listed two model ids by hand, so Opus
  4.7/4.8, Opus 5 and Sonnet 5 still sent a trailing assistant message and got
  the 400 from livekit#4907 back. The guard now reads the generation out of the model
  id and applies from 4.6 onwards.

- Sampling parameters. `temperature` and `top_k` are rejected with a 400 from
  4.7 onwards, so `LLM(model="claude-opus-5", temperature=0.7)` failed outright
  even though `temperature` is a documented constructor argument. They are now
  dropped from the request on those models, with a warning logged once per
  instance, so only the model name has to change.

- Thinking. On Sonnet 5 and Opus 5 thinking is on unless it is turned off
  explicitly, and `max_tokens` caps thinking and the reply together: with the
  1024 default a voice agent could spend the whole budget reasoning and return a
  truncated turn. The request now carries `thinking: {"type": "disabled"}`
  wherever the model accepts it; `extra_kwargs` still wins for callers that do
  want the model to reason.

Thinking blocks were also dropped silently on the way out: `thinking_delta` and
`signature_delta` fell through `_parse_event` while still being generated and
billed. They are now recognised, kept out of the caller's text (a voice agent
would speak them) and logged, with a one-time warning when thinking shows up
although it was not requested.

`models.py` gains the models released since the list was last touched (Opus
4.5/4.7/4.8, Opus 5, Sonnet 4.5, Sonnet 5, Haiku 4.5) and marks the ids that now
return 404.
@sharkfabri
sharkfabri force-pushed the fix/anthropic-claude-5-compat branch from 9fee89c to d600ab7 Compare August 15, 2026 10:32
devin-ai-integration[bot]

This comment was marked as resolved.

The request now carries `thinking` for every model that parses to generation 4.6
or later, including the default `claude-sonnet-4-6`. `messages.create()` takes
explicit keyword arguments and no `**kwargs`, so an Anthropic client older than
0.47 raises `TypeError` before the call leaves the process; `_run` turns that
into an `APIConnectionError`, and the user gets an error on every request rather
than an answer.

`anthropic>=0.41` predates extended thinking. 0.47.0 is the first release whose
`messages.create()` and `beta.messages.create()` accept `thinking`, and the same
release adds the `thinking_delta`, `signature_delta` and `redacted_thinking`
event shapes `_parse_event` reads, so the floor moves there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants