@@ -8,12 +8,68 @@ import { waitUntilCompleted } from "../utils"
88const GEMINI_API_KEY = process . env . GEMINI_API_KEY ?? process . env . GOOGLE_API_KEY
99const GEMINI_MODEL_ID = "gemini-3.1-pro-preview"
1010
11+ type FunctionDeclaration = {
12+ name : string
13+ parametersJsonSchema ?: Record < string , unknown >
14+ }
15+
16+ type GeminiToolConfig = {
17+ functionCallingConfig ?: {
18+ mode ?: string
19+ allowedFunctionNames ?: string [ ]
20+ }
21+ }
22+
1123type CapturedGeminiRequest = {
1224 model ?: string
1325 lastUserMessage : string
1426 thinkingConfig ?: Record < string , unknown >
27+ toolConfig ?: GeminiToolConfig
1528 hasTools : boolean
1629 toolDeclarationCount : number
30+ functionDeclarations : FunctionDeclaration [ ]
31+ }
32+
33+ function findInvalidSchemaPatterns ( schema : unknown , path = "" ) : string [ ] {
34+ if ( ! schema || typeof schema !== "object" || Array . isArray ( schema ) ) {
35+ return [ ]
36+ }
37+
38+ const obj = schema as Record < string , unknown >
39+ const violations : string [ ] = [ ]
40+
41+ if ( "additionalProperties" in obj ) {
42+ violations . push ( `${ path } .additionalProperties (stripped for Gemini compatibility)` )
43+ }
44+
45+ if ( "default" in obj ) {
46+ violations . push ( `${ path } .default (stripped for Gemini compatibility)` )
47+ }
48+
49+ if ( "$schema" in obj ) {
50+ violations . push ( `${ path } .$schema (JSON Schema metadata stripped for Gemini compatibility)` )
51+ }
52+
53+ if ( "type" in obj && Array . isArray ( obj . type ) ) {
54+ violations . push ( `${ path } .type is an array ${ JSON . stringify ( obj . type ) } (Gemini requires a single string type)` )
55+ }
56+
57+ for ( const [ key , value ] of Object . entries ( obj ) ) {
58+ if ( key === "properties" && value && typeof value === "object" ) {
59+ for ( const [ propName , propSchema ] of Object . entries ( value as Record < string , unknown > ) ) {
60+ violations . push ( ...findInvalidSchemaPatterns ( propSchema , `${ path } .properties.${ propName } ` ) )
61+ }
62+ } else if ( key === "items" ) {
63+ violations . push ( ...findInvalidSchemaPatterns ( value , `${ path } .items` ) )
64+ } else if ( key === "anyOf" || key === "oneOf" || key === "allOf" ) {
65+ violations . push ( `${ path } .${ key } (collapsed for Gemini compatibility)` )
66+ if ( Array . isArray ( value ) ) {
67+ value . forEach ( ( item , i ) => violations . push ( ...findInvalidSchemaPatterns ( item , `${ path } .${ key } [${ i } ]` ) ) )
68+ }
69+ }
70+ }
71+
72+ return violations
1773}
1874
1975function getRequestUrl ( input : RequestInfo | URL ) : string {
@@ -74,9 +130,10 @@ function installGeminiRequestCapture(capture: CapturedGeminiRequest[], baseUrl:
74130 if ( isUrlWithOrigin ( url , targetOrigin ) && isGeminiGenerateContentUrl ( url ) ) {
75131 const body = init ?. body && typeof init . body === "string" ? JSON . parse ( init . body ) : { }
76132 const tools = Array . isArray ( body . tools ) ? body . tools : [ ]
77- const toolDeclarationCount = tools . reduce ( ( count : number , tool : { functionDeclarations ?: unknown [ ] } ) => {
78- return count + ( Array . isArray ( tool . functionDeclarations ) ? tool . functionDeclarations . length : 0 )
79- } , 0 )
133+ const functionDeclarations : FunctionDeclaration [ ] = tools . flatMap (
134+ ( tool : { functionDeclarations ?: FunctionDeclaration [ ] } ) =>
135+ Array . isArray ( tool . functionDeclarations ) ? tool . functionDeclarations : [ ] ,
136+ )
80137
81138 capture . push ( {
82139 model : extractGeminiModel ( url ) ,
@@ -85,8 +142,13 @@ function installGeminiRequestCapture(capture: CapturedGeminiRequest[], baseUrl:
85142 body . generationConfig && typeof body . generationConfig === "object"
86143 ? ( body . generationConfig . thinkingConfig as Record < string , unknown > | undefined )
87144 : undefined ,
145+ toolConfig :
146+ body . toolConfig && typeof body . toolConfig === "object"
147+ ? ( body . toolConfig as GeminiToolConfig )
148+ : undefined ,
88149 hasTools : tools . length > 0 ,
89- toolDeclarationCount,
150+ toolDeclarationCount : functionDeclarations . length ,
151+ functionDeclarations,
90152 } )
91153 }
92154
@@ -105,7 +167,8 @@ suite("Gemini provider", function () {
105167 const requests : CapturedGeminiRequest [ ] = [ ]
106168
107169 setup ( function ( ) {
108- if ( ! process . env . AIMOCK_URL && ! GEMINI_API_KEY ) {
170+ const isReplay = process . env . AIMOCK_URL && process . env . AIMOCK_RECORD !== "true"
171+ if ( ! isReplay && ! GEMINI_API_KEY ) {
109172 this . skip ( )
110173 }
111174 } )
@@ -131,23 +194,21 @@ suite("Gemini provider", function () {
131194 } )
132195 } )
133196
134- for ( const reasoningEnabled of [ true , false ] as const ) {
135- test ( `Should complete a task end-to-end using ${ GEMINI_MODEL_ID } via Gemini provider with reasoning ${
136- reasoningEnabled ? "enabled" : "disabled"
137- } `, async ( ) => {
197+ for ( const reasoningEffort of [ "high" , "low" , "disable" ] as const ) {
198+ test ( `Should complete a task end-to-end using ${ GEMINI_MODEL_ID } via Gemini provider with reasoning effort "${ reasoningEffort } "` , async ( ) => {
138199 requests . length = 0
139200
140201 const api = globalThis . api
141202 const aimockUrl = process . env . AIMOCK_URL
142203 const isRecord = process . env . AIMOCK_RECORD === "true"
143- const promptTag = reasoningEnabled ? " gemini-e2e:reasoning-on" : "gemini-e2e:reasoning-off"
204+ const promptTag = ` gemini-e2e:reasoning-${ reasoningEffort } `
144205
145206 await api . setConfiguration ( {
146207 apiProvider : "gemini" as const ,
147208 geminiApiKey : aimockUrl && ! isRecord ? "mock-key" : GEMINI_API_KEY ! ,
148209 apiModelId : GEMINI_MODEL_ID ,
149- enableReasoningEffort : reasoningEnabled ,
150- reasoningEffort : reasoningEnabled ? ( "high" as const ) : ( "disable" as const ) ,
210+ enableReasoningEffort : reasoningEffort !== "disable" ,
211+ reasoningEffort : reasoningEffort ,
151212 ...( aimockUrl && { googleGeminiBaseUrl : aimockUrl } ) ,
152213 } )
153214
@@ -179,18 +240,39 @@ suite("Gemini provider", function () {
179240 firstRequest . toolDeclarationCount > 0 ,
180241 "Gemini provider should declare at least one callable tool" ,
181242 )
243+ assert . strictEqual (
244+ firstRequest . toolConfig ?. functionCallingConfig ?. allowedFunctionNames ,
245+ undefined ,
246+ "Gemini requests should not send allowedFunctionNames; the Gemini backend returns generic INVALID_ARGUMENT for larger or history-incompatible restriction lists" ,
247+ )
182248
183- if ( reasoningEnabled ) {
184- assert . ok (
185- firstRequest . thinkingConfig ,
186- "Reasoning-enabled Gemini requests should include thinkingConfig" ,
249+ // Verify tool schemas are sanitized for Gemini compatibility. Gemini documents
250+ // function declaration schemas as a selected OpenAPI-style subset with
251+ // single-value `type` plus `nullable`; live testing also showed opaque
252+ // INVALID_ARGUMENT failures from broader third-party MCP schema metadata.
253+ for ( const decl of firstRequest . functionDeclarations ) {
254+ const violations = findInvalidSchemaPatterns (
255+ decl . parametersJsonSchema ,
256+ `${ decl . name } .parametersJsonSchema` ,
187257 )
188- } else {
258+ assert . strictEqual (
259+ violations . length ,
260+ 0 ,
261+ `Tool "${ decl . name } " has Gemini-incompatible schema: ${ violations . join ( "; " ) } ` ,
262+ )
263+ }
264+
265+ if ( reasoningEffort === "disable" ) {
189266 assert . strictEqual (
190267 firstRequest . thinkingConfig ,
191268 undefined ,
192269 "Reasoning-disabled Gemini requests should omit thinkingConfig" ,
193270 )
271+ } else {
272+ assert . ok (
273+ firstRequest . thinkingConfig ,
274+ `Gemini requests with reasoningEffort="${ reasoningEffort } " should include thinkingConfig` ,
275+ )
194276 }
195277
196278 const completionMessage = messages . find (
0 commit comments