11import { toGeminiSchema } from "@cortexkit/antigravity-auth-core"
22import type {
3+ AssistantMessage ,
34 Context ,
45 ImageContent ,
56 Message ,
@@ -12,7 +13,7 @@ import type {
1213
1314/** Gemini `contents` part shapes. */
1415type GeminiPart =
15- | { text : string }
16+ | { text : string ; thought ?: boolean ; thoughtSignature ?: string }
1617 | { inlineData : { mimeType : string ; data : string } }
1718 | {
1819 functionCall : { name : string ; args : Record < string , unknown > ; id : string }
@@ -55,26 +56,39 @@ function convertUserParts(content: Array<TextContent | ImageContent>): GeminiPar
5556 return parts
5657}
5758
58- function convertAssistantParts ( content : Array < TextContent | ThinkingContent | ToolCall > ) : GeminiPart [ ] {
59+ function convertAssistantParts (
60+ message : AssistantMessage ,
61+ preserveSignedHistory : boolean ,
62+ ) : GeminiPart [ ] {
5963 const parts : GeminiPart [ ] = [ ]
60- for ( const block of content ) {
61- if ( block . type === "text" && block . text . trim ( ) ) {
62- parts . push ( { text : sanitize ( block . text ) } )
64+ for ( const block of message . content ) {
65+ if ( block . type === "thinking" ) {
66+ if ( preserveSignedHistory && block . thinking ) {
67+ parts . push ( {
68+ text : sanitize ( block . thinking ) ,
69+ thought : true ,
70+ ...( block . thinkingSignature ? { thoughtSignature : block . thinkingSignature } : { } ) ,
71+ } )
72+ }
73+ } else if ( block . type === "text" && block . text . trim ( ) ) {
74+ parts . push ( {
75+ text : sanitize ( block . text ) ,
76+ ...( preserveSignedHistory && block . textSignature
77+ ? { thoughtSignature : block . textSignature }
78+ : { } ) ,
79+ } )
6380 } else if ( block . type === "toolCall" ) {
64- // Antigravity requires the prior functionCall to echo its
65- // thoughtSignature on replay (400 INVALID_ARGUMENT otherwise).
6681 parts . push ( {
6782 functionCall : {
6883 name : block . name ,
6984 args : ( block . arguments ?? { } ) as Record < string , unknown > ,
7085 id : block . id ,
7186 } ,
72- ...( block . thoughtSignature ? { thoughtSignature : block . thoughtSignature } : { } ) ,
87+ ...( preserveSignedHistory && block . thoughtSignature
88+ ? { thoughtSignature : block . thoughtSignature }
89+ : { } ) ,
7390 } )
7491 }
75- // Thinking blocks are intentionally not replayed: OpenCode/pi history does
76- // not carry replayable signed Antigravity thinking, and unsigned thinking
77- // is rejected. The model regenerates thinking each turn.
7892 }
7993 return parts
8094}
@@ -90,8 +104,35 @@ function toolResultResponse(message: ToolResultMessage): Record<string, unknown>
90104 return { output : text }
91105}
92106
93- function convertMessages ( messages : Message [ ] ) : GeminiContent [ ] {
107+ export interface BuildGeminiRequestOptions {
108+ provider ?: string
109+ model ?: string
110+ }
111+
112+ function isSameTargetModel (
113+ message : AssistantMessage ,
114+ options : BuildGeminiRequestOptions | undefined ,
115+ ) : boolean {
116+ if ( ! options ?. provider || ! options . model ) return true
117+ return message . provider === options . provider && message . model === options . model
118+ }
119+
120+ function convertMessages (
121+ messages : Message [ ] ,
122+ options ?: BuildGeminiRequestOptions ,
123+ ) : GeminiContent [ ] {
94124 const contents : GeminiContent [ ] = [ ]
125+ const callMatchesTarget = new Map < string , boolean > ( )
126+
127+ for ( const message of messages ) {
128+ if ( message ?. role !== "assistant" ) continue
129+ const matchesTarget = isSameTargetModel ( message , options )
130+ for ( const block of message . content ) {
131+ if ( block . type === "toolCall" ) {
132+ callMatchesTarget . set ( block . id , matchesTarget )
133+ }
134+ }
135+ }
95136
96137 for ( const message of messages ) {
97138 if ( ! message ) continue
@@ -108,12 +149,13 @@ function convertMessages(messages: Message[]): GeminiContent[] {
108149 }
109150
110151 if ( message . role === "assistant" ) {
111- const parts = convertAssistantParts ( message . content )
152+ const parts = convertAssistantParts ( message , isSameTargetModel ( message , options ) )
112153 if ( parts . length ) contents . push ( { role : "model" , parts } )
113154 continue
114155 }
115156
116157 if ( message . role === "toolResult" ) {
158+ const role = callMatchesTarget . get ( message . toolCallId ) === false ? "model" : "user"
117159 const part : GeminiPart = {
118160 functionResponse : {
119161 name : message . toolName ,
@@ -123,10 +165,10 @@ function convertMessages(messages: Message[]): GeminiContent[] {
123165 }
124166 // Gemini groups consecutive function responses into one user turn.
125167 const last = contents [ contents . length - 1 ]
126- if ( last && last . role === "user" && last . parts . every ( ( p ) => "functionResponse" in p ) ) {
168+ if ( last && last . role === role && last . parts . every ( ( p ) => "functionResponse" in p ) ) {
127169 last . parts . push ( part )
128170 } else {
129- contents . push ( { role : "user" , parts : [ part ] } )
171+ contents . push ( { role, parts : [ part ] } )
130172 }
131173 }
132174 }
@@ -154,9 +196,12 @@ function convertTools(tools: Tool[] | undefined): GeminiTool[] | undefined {
154196 * Convert a pi `Context` into a Gemini `generateContent` request body
155197 * (the inner `request` object of the Antigravity envelope).
156198 */
157- export function buildGeminiRequest ( context : Context ) : GeminiRequest {
199+ export function buildGeminiRequest (
200+ context : Context ,
201+ options ?: BuildGeminiRequestOptions ,
202+ ) : GeminiRequest {
158203 const request : GeminiRequest = {
159- contents : convertMessages ( context . messages ) ,
204+ contents : convertMessages ( context . messages , options ) ,
160205 }
161206
162207 const tools = convertTools ( context . tools )
0 commit comments