diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index 215911e09d9..60443f3c2d1 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -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 diff --git a/tests/agent/test_anthropic_adapter.py b/tests/agent/test_anthropic_adapter.py index b7e24a65a90..1e5be480751 100644 --- a/tests/agent/test_anthropic_adapter.py +++ b/tests/agent/test_anthropic_adapter.py @@ -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"},