1717package com .google .adk .models ;
1818
1919import static com .google .common .truth .Truth .assertThat ;
20+ import static java .nio .charset .StandardCharsets .UTF_8 ;
2021import static org .junit .Assert .assertEquals ;
2122import static org .junit .jupiter .api .Assertions .assertTrue ;
23+ import static org .mockito .ArgumentMatchers .any ;
2224import static org .mockito .Mockito .mock ;
25+ import static org .mockito .Mockito .times ;
26+ import static org .mockito .Mockito .verify ;
2327import static org .mockito .Mockito .when ;
2428
2529import com .anthropic .client .AnthropicClient ;
2630import com .anthropic .core .JsonValue ;
31+ import com .anthropic .models .messages .ContentBlock ;
2732import com .anthropic .models .messages .ContentBlockParam ;
33+ import com .anthropic .models .messages .DirectCaller ;
2834import com .anthropic .models .messages .Message ;
35+ import com .anthropic .models .messages .MessageCreateParams ;
36+ import com .anthropic .models .messages .RedactedThinkingBlock ;
37+ import com .anthropic .models .messages .TextBlock ;
38+ import com .anthropic .models .messages .ThinkingBlock ;
2939import com .anthropic .models .messages .Tool ;
3040import com .anthropic .models .messages .ToolResultBlockParam ;
41+ import com .anthropic .models .messages .ToolUseBlock ;
3142import com .anthropic .models .messages .Usage ;
43+ import com .anthropic .services .blocking .MessageService ;
3244import com .fasterxml .jackson .core .type .TypeReference ;
3345import com .google .common .collect .ImmutableList ;
3446import com .google .common .collect .ImmutableMap ;
47+ import com .google .genai .types .Content ;
3548import com .google .genai .types .FunctionDeclaration ;
3649import com .google .genai .types .FunctionResponse ;
3750import com .google .genai .types .Part ;
3851import com .google .genai .types .Schema ;
3952import java .lang .reflect .Method ;
4053import java .util .Collections ;
54+ import java .util .List ;
4155import java .util .Map ;
4256import org .junit .Before ;
4357import org .junit .Test ;
4458import org .junit .runner .RunWith ;
4559import org .junit .runners .JUnit4 ;
60+ import org .mockito .ArgumentCaptor ;
4661import org .mockito .Mockito ;
4762
4863@ RunWith (JUnit4 .class )
4964public final class ClaudeTest {
5065
5166 private Claude claude ;
67+ private MessageService messageService ;
5268 private Method partToAnthropicMessageBlockMethod ;
5369 private Method functionDeclarationToAnthropicToolMethod ;
5470
5571 @ Before
5672 public void setUp () throws Exception {
5773 AnthropicClient mockClient = Mockito .mock (AnthropicClient .class );
74+ messageService = Mockito .mock (MessageService .class );
75+ when (mockClient .messages ()).thenReturn (messageService );
5876 claude = new Claude ("claude-3-opus" , mockClient );
5977
6078 // Access private method for testing the extraction logic
@@ -74,6 +92,38 @@ private static Map<String, Object> inputSchemaProperties(Tool tool) {
7492 return properties .convert (new TypeReference <Map <String , Object >>() {});
7593 }
7694
95+ private static Message message (ContentBlock ... blocks ) {
96+ Message message = mock (Message .class );
97+ when (message .content ()).thenReturn (ImmutableList .copyOf (blocks ));
98+ return message ;
99+ }
100+
101+ private static ContentBlock thinkingBlock (String thinking , String signature ) {
102+ return ContentBlock .ofThinking (
103+ ThinkingBlock .builder ().thinking (thinking ).signature (signature ).build ());
104+ }
105+
106+ private static ContentBlock redactedThinkingBlock (String data ) {
107+ return ContentBlock .ofRedactedThinking (RedactedThinkingBlock .builder ().data (data ).build ());
108+ }
109+
110+ private static ContentBlock textBlock (String text ) {
111+ return ContentBlock .ofText (
112+ TextBlock .builder ().text (text ).citations (ImmutableList .of ()).build ());
113+ }
114+
115+ private static Content userText (String text ) {
116+ return Content .builder ().role ("user" ).parts (Part .fromText (text )).build ();
117+ }
118+
119+ private static LlmRequest request (Content ... contents ) {
120+ return LlmRequest .builder ().contents (ImmutableList .copyOf (contents )).build ();
121+ }
122+
123+ private static String signatureOf (Part part ) {
124+ return new String (part .thoughtSignature ().get (), UTF_8 );
125+ }
126+
77127 @ Test
78128 public void testPartToAnthropicMessageBlock_mcpTool_legacyTextOutputKey () throws Exception {
79129 Map <String , Object > responseData =
@@ -260,4 +310,136 @@ public void functionDeclarationToAnthropicTool_preservesRefsAndDefs() throws Exc
260310 Map <String , Object > defs = defsValue .convert (new TypeReference <Map <String , Object >>() {});
261311 assertThat (defs ).containsKey ("Pet" );
262312 }
313+
314+ @ Test
315+ public void generateContent_thinkingBlocks_becomeThoughtParts () {
316+ // With display "omitted" (the Claude 5 default) a thinking block has empty text.
317+ Message reply =
318+ message (
319+ thinkingBlock ("" , "signature" ), redactedThinkingBlock ("redacted-data" ), textBlock ("4" ));
320+ when (messageService .create (any (MessageCreateParams .class ))).thenReturn (reply );
321+
322+ LlmResponse response = claude .generateContent (request (userText ("2+2?" )), false ).blockingFirst ();
323+
324+ List <Part > parts = response .content ().get ().parts ().get ();
325+ assertThat (parts ).hasSize (3 );
326+ assertThat (parts .get (0 ).thought ()).hasValue (true );
327+ assertThat (parts .get (0 ).text ()).hasValue ("" );
328+ assertThat (signatureOf (parts .get (0 ))).isEqualTo ("signature" );
329+ assertThat (parts .get (1 ).thought ()).hasValue (true );
330+ assertThat (parts .get (1 ).text ()).isEmpty ();
331+ assertThat (signatureOf (parts .get (1 ))).isEqualTo ("redacted-data" );
332+ assertThat (parts .get (2 ).thought ()).isEmpty ();
333+ assertThat (parts .get (2 ).text ()).hasValue ("4" );
334+ }
335+
336+ @ Test
337+ public void generateContent_toolTurn_sendsThinkingBlocksBackUnchanged () {
338+ Message toolUseReply =
339+ message (
340+ thinkingBlock ("" , "signature-1" ),
341+ redactedThinkingBlock ("redacted-data" ),
342+ textBlock ("Let me check." ),
343+ thinkingBlock ("Checking the weather." , "signature-2" ),
344+ ContentBlock .ofToolUse (
345+ ToolUseBlock .builder ()
346+ .id ("toolu_1" )
347+ .name ("getWeather" )
348+ .input (JsonValue .from (ImmutableMap .of ("city" , "Seoul" )))
349+ .caller (DirectCaller .builder ().build ())
350+ .build ()));
351+ Message finalReply = message (textBlock ("It is sunny." ));
352+ when (messageService .create (any (MessageCreateParams .class )))
353+ .thenReturn (toolUseReply , finalReply );
354+ Content modelTurn =
355+ claude
356+ .generateContent (request (userText ("Weather in Seoul?" )), false )
357+ .blockingFirst ()
358+ .content ()
359+ .get ();
360+ Content toolResult =
361+ Content .builder ()
362+ .role ("user" )
363+ .parts (
364+ Part .builder ()
365+ .functionResponse (
366+ FunctionResponse .builder ()
367+ .id ("toolu_1" )
368+ .name ("getWeather" )
369+ .response (ImmutableMap .of ("result" , "sunny" ))
370+ .build ())
371+ .build ())
372+ .build ();
373+
374+ claude
375+ .generateContent (request (userText ("Weather in Seoul?" ), modelTurn , toolResult ), false )
376+ .blockingFirst ();
377+
378+ ArgumentCaptor <MessageCreateParams > params = ArgumentCaptor .forClass (MessageCreateParams .class );
379+ verify (messageService , times (2 )).create (params .capture ());
380+ List <ContentBlockParam > blocks =
381+ params .getAllValues ().get (1 ).messages ().get (1 ).content ().asBlockParams ();
382+ assertThat (blocks ).hasSize (5 );
383+ assertThat (blocks .get (0 ).asThinking ().thinking ()).isEmpty ();
384+ assertThat (blocks .get (0 ).asThinking ().signature ()).isEqualTo ("signature-1" );
385+ assertThat (blocks .get (1 ).asRedactedThinking ().data ()).isEqualTo ("redacted-data" );
386+ assertThat (blocks .get (2 ).asText ().text ()).isEqualTo ("Let me check." );
387+ assertThat (blocks .get (3 ).asThinking ().thinking ()).isEqualTo ("Checking the weather." );
388+ assertThat (blocks .get (3 ).asThinking ().signature ()).isEqualTo ("signature-2" );
389+ assertThat (blocks .get (4 ).asToolUse ().id ()).isEqualTo ("toolu_1" );
390+ }
391+
392+ @ Test
393+ public void generateContent_emptyThoughtWithoutSignature_isNotSent () {
394+ Message reply = message (textBlock ("ok" ));
395+ when (messageService .create (any (MessageCreateParams .class ))).thenReturn (reply );
396+ Content modelTurn =
397+ Content .builder ()
398+ .role ("model" )
399+ .parts (
400+ Part .builder ().text ("" ).thought (true ).build (),
401+ Part .builder ().text ("" ).thought (true ).thoughtSignature (new byte [0 ]).build (),
402+ Part .fromText ("Hello." ))
403+ .build ();
404+
405+ claude
406+ .generateContent (request (userText ("Hi" ), modelTurn , userText ("Bye" )), false )
407+ .blockingFirst ();
408+
409+ ArgumentCaptor <MessageCreateParams > params = ArgumentCaptor .forClass (MessageCreateParams .class );
410+ verify (messageService ).create (params .capture ());
411+ // Neither thought has a signature to send back, and Anthropic rejects empty text blocks.
412+ List <ContentBlockParam > blocks = params .getValue ().messages ().get (1 ).content ().asBlockParams ();
413+ assertThat (blocks ).hasSize (1 );
414+ assertThat (blocks .get (0 ).asText ().text ()).isEqualTo ("Hello." );
415+ }
416+
417+ @ Test
418+ public void generateContent_thoughtWithBinarySignature_isSentAsText () {
419+ Message reply = message (textBlock ("ok" ));
420+ when (messageService .create (any (MessageCreateParams .class ))).thenReturn (reply );
421+ // Another model (e.g. Gemini) stores a binary signature, which Anthropic could not decode.
422+ Content modelTurn =
423+ Content .builder ()
424+ .role ("model" )
425+ .parts (
426+ Part .builder ()
427+ .text ("Looking up the weather." )
428+ .thought (true )
429+ .thoughtSignature (new byte [] {(byte ) 0xC3 , 0x28 })
430+ .build (),
431+ Part .fromText ("It is sunny." ))
432+ .build ();
433+
434+ claude
435+ .generateContent (request (userText ("Hi" ), modelTurn , userText ("Thanks" )), false )
436+ .blockingFirst ();
437+
438+ ArgumentCaptor <MessageCreateParams > params = ArgumentCaptor .forClass (MessageCreateParams .class );
439+ verify (messageService ).create (params .capture ());
440+ List <ContentBlockParam > blocks = params .getValue ().messages ().get (1 ).content ().asBlockParams ();
441+ assertThat (blocks ).hasSize (2 );
442+ assertThat (blocks .get (0 ).asText ().text ()).isEqualTo ("Looking up the weather." );
443+ assertThat (blocks .get (1 ).asText ().text ()).isEqualTo ("It is sunny." );
444+ }
263445}
0 commit comments