@@ -842,3 +842,57 @@ def test_any_llm_reasoning_objects_prefer_content_attributes_over_iterable_pairs
842842 delta = pytypes .SimpleNamespace (reasoning = Reasoning (content = "用户" ))
843843
844844 assert _extract_any_llm_reasoning_text (delta ) == "用户"
845+
846+
847+ def test_any_llm_split_does_not_duplicate_content_or_thinking (monkeypatch ) -> None :
848+ """Splitting multi-tool assistant messages must not duplicate text/thinking blocks.
849+
850+ Anthropic's extended thinking API rejects requests that include the same signed
851+ thinking block more than once, and duplicated assistant text corrupts conversation
852+ history. Only the first split should retain content, thinking_blocks, and
853+ reasoning_content; subsequent splits should carry the tool_call alone.
854+ """
855+ provider = FakeAnyLLMProvider (supports_responses = False )
856+ module , _ = _import_any_llm_module (monkeypatch , provider )
857+ AnyLLMModel = module .AnyLLMModel
858+
859+ model = AnyLLMModel (model = "anthropic/claude-3-5-sonnet" )
860+ messages : list [Any ] = [
861+ {"role" : "user" , "content" : "Search both" },
862+ {
863+ "role" : "assistant" ,
864+ "content" : "Looking up both queries." ,
865+ "thinking_blocks" : [{"type" : "thinking" , "thinking" : "plan" , "signature" : "sig_abc" }],
866+ "reasoning_content" : "internal plan" ,
867+ "tool_calls" : [
868+ {
869+ "id" : "call_1" ,
870+ "type" : "function" ,
871+ "function" : {"name" : "s" , "arguments" : "{}" },
872+ },
873+ {
874+ "id" : "call_2" ,
875+ "type" : "function" ,
876+ "function" : {"name" : "s" , "arguments" : "{}" },
877+ },
878+ ],
879+ },
880+ {"role" : "tool" , "tool_call_id" : "call_1" , "content" : "ok1" },
881+ {"role" : "tool" , "tool_call_id" : "call_2" , "content" : "ok2" },
882+ ]
883+
884+ result = model ._fix_tool_message_ordering (messages )
885+
886+ assistants = [m for m in result if m .get ("role" ) == "assistant" ]
887+ assert len (assistants ) == 2
888+ # First split keeps the shared fields.
889+ assert assistants [0 ].get ("content" ) == "Looking up both queries."
890+ assert "thinking_blocks" in assistants [0 ]
891+ assert "reasoning_content" in assistants [0 ]
892+ # Second split must NOT duplicate them.
893+ assert "content" not in assistants [1 ]
894+ assert "thinking_blocks" not in assistants [1 ]
895+ assert "reasoning_content" not in assistants [1 ]
896+ # Tool calls are still split one-per-message.
897+ assert assistants [0 ]["tool_calls" ][0 ]["id" ] == "call_1"
898+ assert assistants [1 ]["tool_calls" ][0 ]["id" ] == "call_2"
0 commit comments