Render carried thinking as a bare `<assistant_thinking>` tag so UI round trips keep the prompt cache
Follow-up to #6692 (unreleased), which carries a ThinkingPart that can't use Anthropic's native reasoning channel in a user turn, wrapped in <assistant_thinking model="..." provider="...">. The two attributes came from ModelResponse.model_name and provider_name.
The bug. VercelAIAdapter and AGUIAdapter deliberately don't send those fields to the frontend (#5279, #5174): a model_name can hold a Bedrock ARN with the AWS account id, or an ft: model id with the org name. So a history that went through dump_messages ā client ā load_messages renders <assistant_thinking> without attributes, the server's own copy renders it with them, and the request bytes differ. The prompt cache misses from the first carried thinking block onward. The UI round-trip harness in #10560 found it.
The fix. The wrapper never carries attributes now, so it renders the same from either copy of the history.
Rendering only provider= from ThinkingPart.provider_name, which both adapters do round-trip, was the first option. Live runs ruled it out: given a provider attribute without model, claude-opus-4-5 copies the wrapper into its user-visible answer every time, which is the #5869 leak #6692 exists to stop.
Evidence. Leaks per live run: the model wrote a thinking tag in its visible answer. History: the six prior turns from test_thinking_tag_mimicry, with the reasoning carried in a user turn. Recorded 2026-10-19, n=8 per cell; second-run counts in parentheses.
| wrapper | claude-opus-4-5 | claude-sonnet-5-5 | claude-haiku-4-5 |
|---|---|---|---|
<assistant_thinking model="gpt-5.6" provider="openai"> (#6692) | 0/8 | 0/8 | 0/8 |
<assistant_thinking provider="openai"> | 8/8 (6/6) | 0/8 | 0/8 (0/6) |
<assistant_thinking provider="anthropic"> | 8/8 | 0/8 | 0/8 |
<assistant_thinking> (this PR) | 0/8 (0/6) | 0/8 | 0/8 (0/6) |
Opus copies the wrapper itself, <assistant_thinking>ā¦</assistant_thinking>, into the answer.
Trade-off. The tag no longer names the source. Per #6692's own measurements, that never changed what Claude says when asked who wrote the reasoning, so the attributes were provenance for the record only. They were never stored in history, so nothing persisted is affected.
Changes
render_replayed_thinkingdrops theModelResponseparameter and the attributes.AnthropicModelandBedrockConverseModelcall it with the content only.- Docs (
capabilities/thinking.md), themimics_assistant_message_formattingdocstring and the agent skill reference no longer describe the attributes.
Tests
tests/test_cache_prefix_stability.py::test_anthropic_foreign_thinking_attribution_roundtrip_wire_stable: another model's thinking re-sends to Anthropic with an identical body after a Vercel AI, Vercel AI stream-shaped and AG-UI round trip. Fails onmainfor all three.tests/test_thinking_placement.py,tests/models/test_anthropic.py: snapshots updated to the bare tag. The escaping case keeps its closing-tag payload; it no longer needs an attribute-injection payload, since nothing from the history is rendered into the tag.tests/models/anthropic/test_thinking_tag_mimicry.py: new recorded casesopus-provider-carried-in-user-turn-leaksandopus-unattributed-carried-in-user-turn-clean, andsonnet-5-5-carried-in-user-turn-cleanre-recorded with the bare wrapper. Themodel+providercase stays as the recorded control for the channel finding.
No compatibility label: #6692 is not in a release yet.
Checklist
- No breaking changes in accordance with the version policy.
- Any permitted compatibility impact has a label, warning, migration, release note, and exact API-check waiver.
- PR title is fit for the release changelog.
š¤ Generated with Claude Code