fix(anthropic): preserve ordered replay cache markers

This commit is contained in:
qWaitCrypto 2026-06-25 23:30:21 +08:00 committed by Teknium
parent 80d71e8d2e
commit e1ff736f26
2 changed files with 62 additions and 5 deletions

View file

@ -1891,6 +1891,18 @@ def _sanitize_replay_block(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return None
def _apply_assistant_cache_control_to_last_cacheable_block(
blocks: List[Dict[str, Any]],
cache_control: Any,
) -> None:
if not isinstance(cache_control, dict):
return
for block in reversed(blocks):
if isinstance(block, dict) and block.get("type") in {"text", "tool_use"}:
block.setdefault("cache_control", dict(cache_control))
break
def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]:
"""Convert an assistant message to Anthropic content blocks.
@ -1945,6 +1957,9 @@ def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]:
clean["input"] = redacted
replayed.append(clean)
if replayed:
_apply_assistant_cache_control_to_last_cacheable_block(
replayed, m.get("cache_control")
)
return {"role": "assistant", "content": replayed}
blocks = _extract_preserved_thinking_blocks(m)
@ -1970,11 +1985,9 @@ def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]:
"name": fn.get("name", ""),
"input": parsed_args,
})
if isinstance(m.get("cache_control"), dict):
for block in reversed(blocks):
if isinstance(block, dict) and block.get("type") in {"text", "tool_use"}:
block.setdefault("cache_control", dict(m["cache_control"]))
break
_apply_assistant_cache_control_to_last_cacheable_block(
blocks, m.get("cache_control")
)
# Kimi's /coding endpoint (Anthropic protocol) requires assistant
# tool-call messages to carry reasoning_content when thinking is
# enabled server-side. Preserve it as a thinking block so Kimi

View file

@ -1046,6 +1046,50 @@ class TestConvertMessages:
assert tool_use["id"] == "tc_1"
assert tool_use["cache_control"] == {"type": "ephemeral"}
def test_ordered_replay_tool_use_cache_control_is_preserved(self):
messages = apply_anthropic_cache_control([
{"role": "system", "content": "System prompt"},
{"role": "user", "content": "Run the tool"},
{
"role": "assistant",
"content": "",
"anthropic_content_blocks": [
{
"type": "thinking",
"thinking": "Need a tool.",
"signature": "sig_1",
},
{
"type": "tool_use",
"id": "tc_1",
"name": "test_tool",
"input": {"query": "raw"},
},
],
"tool_calls": [
{
"id": "tc_1",
"function": {
"name": "test_tool",
"arguments": '{"query":"redacted"}',
},
},
],
},
{"role": "tool", "tool_call_id": "tc_1", "content": "result"},
], native_anthropic=True)
_, result = convert_messages_to_anthropic(messages)
assistant_msg = [m for m in result if m["role"] == "assistant"][0]
thinking, tool_use = assistant_msg["content"]
assert thinking["type"] == "thinking"
assert "cache_control" not in thinking
assert tool_use["type"] == "tool_use"
assert tool_use["id"] == "tc_1"
assert tool_use["input"] == {"query": "redacted"}
assert tool_use["cache_control"] == {"type": "ephemeral"}
def test_tool_cache_control_is_preserved_on_tool_result_block(self):
messages = apply_anthropic_cache_control([
{"role": "system", "content": "System prompt"},