@@ -246,3 +246,116 @@ def test_anthropic_thinking_blocks_with_tool_calls():
246246 tool_calls = assistant_msg .get ("tool_calls" , [])
247247 assert len (cast (list [Any ], tool_calls )) == 1 , "Tool calls should be preserved"
248248 assert cast (list [Any ], tool_calls )[0 ]["function" ]["name" ] == "get_weather"
249+
250+
251+ def test_anthropic_thinking_blocks_without_tool_calls ():
252+ """
253+ Test for models with extended thinking WITHOUT tool calls.
254+
255+ This test verifies that thinking blocks are properly attached to assistant
256+ messages even when there are no tool calls (fixes issue #2195).
257+ """
258+ # Create a message with reasoning and thinking blocks but NO tool calls
259+ message = InternalChatCompletionMessage (
260+ role = "assistant" ,
261+ content = "The weather in Paris is sunny with a temperature of 22°C." ,
262+ reasoning_content = "The user wants to know about the weather in Paris." ,
263+ thinking_blocks = [
264+ {
265+ "type" : "thinking" ,
266+ "thinking" : "Let me think about the weather in Paris." ,
267+ "signature" : "TestSignatureNoTools123" ,
268+ }
269+ ],
270+ tool_calls = None , # No tool calls
271+ )
272+
273+ # Step 1: Convert message to output items
274+ output_items = Converter .message_to_output_items (message )
275+
276+ # Verify reasoning item exists and contains thinking blocks
277+ reasoning_items = [
278+ item for item in output_items if hasattr (item , "type" ) and item .type == "reasoning"
279+ ]
280+ assert len (reasoning_items ) == 1 , "Should have exactly one reasoning item"
281+
282+ reasoning_item = reasoning_items [0 ]
283+
284+ # Verify thinking text is stored in content
285+ assert hasattr (reasoning_item , "content" ) and reasoning_item .content , (
286+ "Reasoning item should have content"
287+ )
288+ assert reasoning_item .content [0 ].type == "reasoning_text" , (
289+ "Content should be reasoning_text type"
290+ )
291+ assert reasoning_item .content [0 ].text == "Let me think about the weather in Paris." , (
292+ "Thinking text should be preserved"
293+ )
294+
295+ # Verify signature is stored in encrypted_content
296+ assert hasattr (reasoning_item , "encrypted_content" ), (
297+ "Reasoning item should have encrypted_content"
298+ )
299+ assert reasoning_item .encrypted_content == "TestSignatureNoTools123" , (
300+ "Signature should be preserved"
301+ )
302+
303+ # Verify message item exists
304+ message_items = [
305+ item for item in output_items if hasattr (item , "type" ) and item .type == "message"
306+ ]
307+ assert len (message_items ) == 1 , "Should have exactly one message item"
308+
309+ # Step 2: Convert output items back to messages with preserve_thinking_blocks=True
310+ items_as_dicts : list [dict [str , Any ]] = []
311+ for item in output_items :
312+ if hasattr (item , "model_dump" ):
313+ items_as_dicts .append (item .model_dump ())
314+ else :
315+ items_as_dicts .append (cast (dict [str , Any ], item ))
316+
317+ messages = Converter .items_to_messages (
318+ items_as_dicts , # type: ignore[arg-type]
319+ model = "anthropic/claude-4-opus" ,
320+ preserve_thinking_blocks = True ,
321+ )
322+
323+ # Should have one assistant message
324+ assistant_messages = [msg for msg in messages if msg .get ("role" ) == "assistant" ]
325+ assert len (assistant_messages ) == 1 , "Should have exactly one assistant message"
326+
327+ assistant_msg = assistant_messages [0 ]
328+
329+ # Content must start with thinking blocks even WITHOUT tool calls
330+ content = assistant_msg .get ("content" )
331+ assert content is not None , "Assistant message should have content"
332+ assert isinstance (content , list ), (
333+ f"Assistant message content should be a list when thinking blocks are present, "
334+ f"but got { type (content )} "
335+ )
336+ assert len (content ) >= 2 , (
337+ f"Assistant message should have at least 2 content items "
338+ f"(thinking + text), got { len (content )} "
339+ )
340+
341+ # First content should be thinking block
342+ first_content = content [0 ]
343+ assert first_content .get ("type" ) == "thinking" , (
344+ f"First content must be 'thinking' type for Anthropic compatibility, "
345+ f"but got '{ first_content .get ('type' )} '"
346+ )
347+ assert first_content .get ("thinking" ) == "Let me think about the weather in Paris." , (
348+ "Thinking content should be preserved"
349+ )
350+ assert first_content .get ("signature" ) == "TestSignatureNoTools123" , (
351+ "Signature should be preserved in thinking block"
352+ )
353+
354+ # Second content should be text
355+ second_content = content [1 ]
356+ assert second_content .get ("type" ) == "text" , (
357+ f"Second content must be 'text' type, but got '{ second_content .get ('type' )} '"
358+ )
359+ assert (
360+ second_content .get ("text" ) == "The weather in Paris is sunny with a temperature of 22°C."
361+ ), "Text content should be preserved"
0 commit comments