@@ -253,4 +253,106 @@ describe('AxAIGoogleGemini model key preset merging', () => {
253253 expect ( assistantMsg . parts [ 0 ] . functionCall . name ) . toBe ( 'foo' ) ;
254254 expect ( assistantMsg . parts [ 0 ] . thought_signature ) . toBe ( 'sig123' ) ;
255255 } ) ;
256+
257+ it ( 'groups parallel function responses into a single user turn' , async ( ) => {
258+ const ai = new AxAIGoogleGemini ( {
259+ apiKey : 'key' ,
260+ config : { model : AxAIGoogleGeminiModel . Gemini3ProPreview } ,
261+ models : [ ] ,
262+ } ) ;
263+
264+ const capture : { lastBody ?: any } = { } ;
265+ const fetch = createMockFetch (
266+ {
267+ candidates : [
268+ { content : { parts : [ { text : 'ok' } ] } , finishReason : 'STOP' } ,
269+ ] ,
270+ } ,
271+ capture
272+ ) ;
273+ ai . setOptions ( { fetch } ) ;
274+
275+ const history : any [ ] = [
276+ { role : 'user' , content : 'call parallel' } ,
277+ {
278+ role : 'assistant' ,
279+ functionCalls : [
280+ {
281+ function : { name : 'f1' , params : '{}' } ,
282+ id : 'id1' ,
283+ type : 'function' ,
284+ } ,
285+ {
286+ function : { name : 'f2' , params : '{}' } ,
287+ id : 'id2' ,
288+ type : 'function' ,
289+ } ,
290+ ] ,
291+ } ,
292+ { role : 'function' , functionId : 'f1' , result : 'r1' } ,
293+ { role : 'function' , functionId : 'f2' , result : 'r2' } ,
294+ ] ;
295+
296+ await ai . chat ( { chatPrompt : history } , { stream : false } ) ;
297+
298+ const reqBody = capture . lastBody ;
299+ // Expected: User, Model, User (with 2 parts)
300+ expect ( reqBody . contents ) . toHaveLength ( 3 ) ;
301+ const lastUserMsg = reqBody . contents [ 2 ] ;
302+ expect ( lastUserMsg . role ) . toBe ( 'user' ) ;
303+ expect ( lastUserMsg . parts ) . toHaveLength ( 2 ) ;
304+ expect ( lastUserMsg . parts [ 0 ] . functionResponse . name ) . toBe ( 'f1' ) ;
305+ expect ( lastUserMsg . parts [ 1 ] . functionResponse . name ) . toBe ( 'f2' ) ;
306+ } ) ;
307+
308+ it ( 'does not set thought: true on text part when function calls are present' , async ( ) => {
309+ const ai = new AxAIGoogleGemini ( {
310+ apiKey : 'key' ,
311+ config : { model : AxAIGoogleGeminiModel . Gemini3ProPreview } ,
312+ models : [ ] ,
313+ } ) ;
314+
315+ const capture : { lastBody ?: any } = { } ;
316+ const fetch = createMockFetch (
317+ {
318+ candidates : [
319+ { content : { parts : [ { text : 'ok' } ] } , finishReason : 'STOP' } ,
320+ ] ,
321+ } ,
322+ capture
323+ ) ;
324+ ai . setOptions ( { fetch } ) ;
325+
326+ const history : any [ ] = [
327+ { role : 'user' , content : 'call with thought' } ,
328+ {
329+ role : 'assistant' ,
330+ thoughtBlock : { data : 'Thinking...' , signature : 'sig1' } ,
331+ functionCalls : [
332+ {
333+ function : { name : 'f1' , params : '{}' } ,
334+ id : 'id1' ,
335+ type : 'function' ,
336+ } ,
337+ ] ,
338+ } ,
339+ { role : 'function' , functionId : 'f1' , result : 'r1' } ,
340+ ] ;
341+
342+ await ai . chat ( { chatPrompt : history } , { stream : false } ) ;
343+
344+ const reqBody = capture . lastBody ;
345+ const assistantMsg = reqBody . contents [ 1 ] ;
346+ expect ( assistantMsg . role ) . toBe ( 'model' ) ;
347+ expect ( assistantMsg . parts ) . toHaveLength ( 2 ) ;
348+
349+ // Part 0: Text (Thought) - Should NOT have thought: true
350+ expect ( assistantMsg . parts [ 0 ] . text ) . toBe ( 'Thinking...' ) ;
351+ expect ( assistantMsg . parts [ 0 ] . thought ) . toBeUndefined ( ) ;
352+ expect ( assistantMsg . parts [ 0 ] . thought_signature ) . toBeUndefined ( ) ;
353+
354+ // Part 1: Function Call - Should have signature
355+ expect ( assistantMsg . parts [ 1 ] . functionCall . name ) . toBe ( 'f1' ) ;
356+ expect ( assistantMsg . parts [ 1 ] . thought_signature ) . toBe ( 'sig1' ) ;
357+ } ) ;
256358} ) ;
0 commit comments