@@ -208,15 +208,41 @@ impl Adapter for GeminiAdapter {
208208 usage,
209209 } = gemini_response;
210210
211- // FIXME: Needs to take the content list
212- let mut content: MessageContent = MessageContent :: default ( ) ;
211+ let mut thoughts: Vec < String > = Vec :: new ( ) ;
212+ let mut texts: Vec < String > = Vec :: new ( ) ;
213+ let mut tool_calls: Vec < ToolCall > = Vec :: new ( ) ;
214+
213215 for g_item in gemini_content {
214216 match g_item {
215- GeminiChatContent :: Text ( text) => content. push ( text) ,
216- GeminiChatContent :: ToolCall ( tool_call) => content. push ( tool_call) ,
217+ GeminiChatContent :: Text ( text) => texts. push ( text) ,
218+ GeminiChatContent :: ToolCall ( tool_call) => tool_calls. push ( tool_call) ,
219+ GeminiChatContent :: ThoughtSignature ( thought) => thoughts. push ( thought) ,
220+ }
221+ }
222+
223+ let thought_signatures_for_call = ( !thoughts. is_empty ( ) && !tool_calls. is_empty ( ) ) . then ( || thoughts. clone ( ) ) ;
224+ let mut parts: Vec < ContentPart > = thoughts. into_iter ( ) . map ( ContentPart :: ThoughtSignature ) . collect ( ) ;
225+
226+ if let Some ( signatures) = thought_signatures_for_call {
227+ if let Some ( first_call) = tool_calls. first_mut ( ) {
228+ first_call. thought_signatures = Some ( signatures) ;
217229 }
218230 }
219231
232+ if !texts. is_empty ( ) {
233+ let total_len: usize = texts. iter ( ) . map ( |t| t. len ( ) ) . sum ( ) ;
234+ let mut combined_text = String :: with_capacity ( total_len) ;
235+ for text in texts {
236+ combined_text. push_str ( & text) ;
237+ }
238+ if !combined_text. is_empty ( ) {
239+ parts. push ( ContentPart :: Text ( combined_text) ) ;
240+ }
241+ }
242+
243+ parts. extend ( tool_calls. into_iter ( ) . map ( ContentPart :: ToolCall ) ) ;
244+ let content = MessageContent :: from_parts ( parts) ;
245+
220246 Ok ( ChatResponse {
221247 content,
222248 reasoning_content : None ,
@@ -293,13 +319,36 @@ impl GeminiAdapter {
293319 } ;
294320
295321 for mut part in parts {
322+ // -- Capture eventual thought signature
323+ {
324+ if let Some ( thought) = part
325+ . x_take :: < Value > ( "thoughtSignature" )
326+ . ok ( )
327+ . and_then ( |v| if let Value :: String ( v) = v { Some ( v) } else { None } )
328+ {
329+ content. push ( GeminiChatContent :: ThoughtSignature ( thought) ) ;
330+ }
331+ // Note: sometime the thought is in "thought" (undocumented, but observed in some cases or older models?)
332+ // But for Gemini 3 it is thoughtSignature. Keeping this just in case or for backward compat if it was used.
333+ // Actually, let's stick to thoughtSignature as per docs, but if we see "thought" we might want to capture it too.
334+ // Let's check for "thought" if "thoughtSignature" was not found.
335+ else if let Some ( thought) = part
336+ . x_take :: < Value > ( "thought" )
337+ . ok ( )
338+ . and_then ( |v| if let Value :: String ( v) = v { Some ( v) } else { None } )
339+ {
340+ content. push ( GeminiChatContent :: ThoughtSignature ( thought) ) ;
341+ }
342+ }
343+
296344 // -- Capture eventual function call
297345 if let Ok ( fn_call_value) = part. x_take :: < Value > ( "functionCall" ) {
298346 let tool_call = ToolCall {
299347 // NOTE: Gemini does not have call_id so, use name
300348 call_id : fn_call_value. x_get ( "name" ) . unwrap_or ( "" . to_string ( ) ) , // TODO: Handle this, gemini does not return the call_id
301349 fn_name : fn_call_value. x_get ( "name" ) . unwrap_or ( "" . to_string ( ) ) ,
302350 fn_arguments : fn_call_value. x_get ( "args" ) . unwrap_or ( Value :: Null ) ,
351+ thought_signatures : None ,
303352 } ;
304353 content. push ( GeminiChatContent :: ToolCall ( tool_call) )
305354 }
@@ -458,29 +507,66 @@ impl GeminiAdapter {
458507 }
459508 } ) ) ;
460509 }
510+ ContentPart :: ThoughtSignature ( thought) => {
511+ parts_values. push ( json ! ( {
512+ "thoughtSignature" : thought
513+ } ) ) ;
514+ }
461515 }
462516 }
463517
464518 contents. push ( json ! ( { "role" : "user" , "parts" : parts_values} ) ) ;
465519 }
466520 ChatRole :: Assistant => {
467521 let mut parts_values: Vec < Value > = Vec :: new ( ) ;
522+ let mut pending_thought: Option < String > = None ;
468523 for part in msg. content {
469524 match part {
470- ContentPart :: Text ( text) => parts_values. push ( json ! ( { "text" : text} ) ) ,
525+ ContentPart :: Text ( text) => {
526+ if let Some ( thought) = pending_thought. take ( ) {
527+ parts_values. push ( json ! ( { "thoughtSignature" : thought} ) ) ;
528+ }
529+ parts_values. push ( json ! ( { "text" : text} ) ) ;
530+ }
471531 ContentPart :: ToolCall ( tool_call) => {
472- parts_values. push ( json ! ( {
473- "functionCall" : {
532+ let mut part_obj = serde_json:: Map :: new ( ) ;
533+ part_obj. insert (
534+ "functionCall" . to_string ( ) ,
535+ json ! ( {
474536 "name" : tool_call. fn_name,
475537 "args" : tool_call. fn_arguments,
476- }
477- } ) ) ;
538+ } ) ,
539+ ) ;
540+
541+ if let Some ( thought) = pending_thought. take ( ) {
542+ // Inject thoughtSignature alongside functionCall in the same Part object
543+ part_obj. insert ( "thoughtSignature" . to_string ( ) , json ! ( thought) ) ;
544+ }
545+
546+ parts_values. push ( Value :: Object ( part_obj) ) ;
547+ }
548+ ContentPart :: ThoughtSignature ( thought) => {
549+ if let Some ( prev_thought) = pending_thought. take ( ) {
550+ parts_values. push ( json ! ( { "thoughtSignature" : prev_thought} ) ) ;
551+ }
552+ pending_thought = Some ( thought) ;
478553 }
479554 // Ignore unsupported parts for Assistant role
480- ContentPart :: Binary ( _) => { }
481- ContentPart :: ToolResponse ( _) => { }
555+ ContentPart :: Binary ( _) => {
556+ if let Some ( thought) = pending_thought. take ( ) {
557+ parts_values. push ( json ! ( { "thoughtSignature" : thought} ) ) ;
558+ }
559+ }
560+ ContentPart :: ToolResponse ( _) => {
561+ if let Some ( thought) = pending_thought. take ( ) {
562+ parts_values. push ( json ! ( { "thoughtSignature" : thought} ) ) ;
563+ }
564+ }
482565 }
483566 }
567+ if let Some ( thought) = pending_thought {
568+ parts_values. push ( json ! ( { "thoughtSignature" : thought} ) ) ;
569+ }
484570 if !parts_values. is_empty ( ) {
485571 contents. push ( json ! ( { "role" : "model" , "parts" : parts_values} ) ) ;
486572 }
@@ -508,10 +594,15 @@ impl GeminiAdapter {
508594 }
509595 } ) ) ;
510596 }
597+ ContentPart :: ThoughtSignature ( thought) => {
598+ parts_values. push ( json ! ( {
599+ "thoughtSignature" : thought
600+ } ) ) ;
601+ }
511602 _ => {
512603 return Err ( Error :: MessageContentTypeNotSupported {
513604 model_iden : model_iden. clone ( ) ,
514- cause : "ChatRole::Tool can only contain ToolCall or ToolResponse content parts" ,
605+ cause : "ChatRole::Tool can only contain ToolCall, ToolResponse, or Thought content parts" ,
515606 } ) ;
516607 }
517608 }
@@ -580,6 +671,7 @@ pub(super) struct GeminiChatResponse {
580671pub ( super ) enum GeminiChatContent {
581672 Text ( String ) ,
582673 ToolCall ( ToolCall ) ,
674+ ThoughtSignature ( String ) ,
583675}
584676
585677struct GeminiChatRequestParts {
0 commit comments