2626import com .google .adk .JsonBaseModel ;
2727import com .google .adk .models .LlmRequest ;
2828import com .google .common .collect .ImmutableList ;
29+ import com .google .common .collect .ImmutableMap ;
2930import com .google .genai .types .Content ;
3031import com .google .genai .types .FunctionDeclaration ;
3132import com .google .genai .types .FunctionResponse ;
@@ -351,41 +352,43 @@ private static List<Message> processContent(Content content) {
351352 List <ChatCompletionsCommon .ToolCall > toolCalls = new ArrayList <>();
352353 List <Message > toolResponses = new ArrayList <>();
353354 List <String > refusals = new ArrayList <>();
354-
355- content
356- .parts ()
357- .ifPresent (
358- parts -> {
359- for (Part part : parts ) {
360- if (part .text ().isPresent ()) {
361- // Text Parts may carry refusal content prefixed with REFUSAL_PREFIX.
362- ChatCompletionsCommon .RefusalSplit split =
363- ChatCompletionsCommon .parseRefusalPrefix (part .text ().get ());
364- if (split .content () != null ) {
365- ContentPart textPart = new ContentPart ();
366- textPart .type = "text" ;
367- textPart .text = split .content ();
368- contentParts .add (textPart );
369- }
370- if (split .refusal () != null ) {
371- refusals .add (split .refusal ());
372- }
373- } else if (part .inlineData ().isPresent ()) {
374- contentParts .add (processInlineDataPart (part ));
375- } else if (part .fileData ().isPresent ()) {
376- contentParts .add (processFileDataPart (part ));
377- } else if (part .functionCall ().isPresent ()) {
378- toolCalls .add (processFunctionCallPart (part ));
379- } else if (part .functionResponse ().isPresent ()) {
380- toolResponses .add (processFunctionResponsePart (part ));
381- } else if (part .executableCode ().isPresent ()) {
382- logger .warn ("Executable code is not supported in Chat Completion conversion" );
383- } else if (part .codeExecutionResult ().isPresent ()) {
384- logger .warn (
385- "Code execution result is not supported in Chat Completion conversion" );
386- }
387- }
388- });
355+ // Capture a message-level thought_signature from the first text Part that carries one.
356+ // This signature must be echoed back on subsequent turns to ensure proper round-tripping.
357+ byte [] textThoughtSignature = null ;
358+
359+ if (content .parts ().isPresent ()) {
360+ for (Part part : content .parts ().get ()) {
361+ if (part .text ().isPresent ()) {
362+ // Text Parts may carry refusal content prefixed with REFUSAL_PREFIX.
363+ ChatCompletionsCommon .RefusalSplit split =
364+ ChatCompletionsCommon .parseRefusalPrefix (part .text ().get ());
365+ if (split .content () != null ) {
366+ ContentPart textPart = new ContentPart ();
367+ textPart .type = "text" ;
368+ textPart .text = split .content ();
369+ contentParts .add (textPart );
370+ }
371+ if (split .refusal () != null ) {
372+ refusals .add (split .refusal ());
373+ }
374+ if (textThoughtSignature == null && part .thoughtSignature ().isPresent ()) {
375+ textThoughtSignature = part .thoughtSignature ().get ();
376+ }
377+ } else if (part .inlineData ().isPresent ()) {
378+ contentParts .add (processInlineDataPart (part ));
379+ } else if (part .fileData ().isPresent ()) {
380+ contentParts .add (processFileDataPart (part ));
381+ } else if (part .functionCall ().isPresent ()) {
382+ toolCalls .add (processFunctionCallPart (part ));
383+ } else if (part .functionResponse ().isPresent ()) {
384+ toolResponses .add (processFunctionResponsePart (part ));
385+ } else if (part .executableCode ().isPresent ()) {
386+ logger .warn ("Executable code is not supported in Chat Completion conversion" );
387+ } else if (part .codeExecutionResult ().isPresent ()) {
388+ logger .warn ("Code execution result is not supported in Chat Completion conversion" );
389+ }
390+ }
391+ }
389392
390393 if (!toolResponses .isEmpty ()) {
391394 return toolResponses ;
@@ -403,6 +406,14 @@ private static List<Message> processContent(Content content) {
403406 msg .content = new MessageContent (ImmutableList .copyOf (contentParts ));
404407 }
405408 }
409+ // Round-trip the message-level thought_signature for assistant text responses.
410+ if (textThoughtSignature != null ) {
411+ msg .extraContent =
412+ ImmutableMap .of (
413+ "google" ,
414+ ImmutableMap .of (
415+ "thought_signature" , Base64 .getEncoder ().encodeToString (textThoughtSignature )));
416+ }
406417 List <Message > messages = new ArrayList <>();
407418 messages .add (msg );
408419 return messages ;
@@ -446,6 +457,10 @@ private static ContentPart processFileDataPart(Part part) {
446457 /**
447458 * Processes a function call part and returns a mapped ToolCall.
448459 *
460+ * <p>If the source {@link Part} carries a {@code thoughtSignature}, it is round-tripped back out
461+ * as a base64-encoded string in {@code extra_content.google.thought_signature} to satisfy
462+ * endpoint requirements.
463+ *
449464 * @param part The input part containing a requested function call or invocation.
450465 * @return The mapped function call tool call.
451466 */
@@ -464,6 +479,13 @@ private static ChatCompletionsCommon.ToolCall processFunctionCallPart(Part part)
464479 }
465480 }
466481 toolCall .function = function ;
482+ part .thoughtSignature ()
483+ .ifPresent (
484+ sigBytes -> {
485+ String sig = Base64 .getEncoder ().encodeToString (sigBytes );
486+ toolCall .extraContent =
487+ ImmutableMap .of ("google" , ImmutableMap .of ("thought_signature" , sig ));
488+ });
467489 return toolCall ;
468490 }
469491
@@ -616,6 +638,13 @@ static class Message {
616638
617639 /** See class definition for more details. */
618640 public String refusal ;
641+
642+ /**
643+ * Message-level additional parameters used by some providers. Used for round-tripping data like
644+ * {@code extra_content.google.thought_signature}.
645+ */
646+ @ JsonProperty ("extra_content" )
647+ public Map <String , Object > extraContent ;
619648 }
620649
621650 /**
0 commit comments