77 type GoogleGenAI ,
88 type Interactions ,
99 type Part ,
10- type Schema ,
1110 type ThinkingConfig ,
1211 type ToolConfig ,
1312} from '@google/genai'
@@ -21,11 +20,11 @@ import {
2120 captureProviderConversationStep ,
2221 recordProviderConversationToolError ,
2322} from '@/providers/conversation-history'
23+ import { applyGeminiResponseFormat } from '@/providers/gemini/config'
2424import { createGeminiStreamingToolLoopStream } from '@/providers/gemini/streaming-tool-loop'
2525import { priceGeminiTokens , splitGeminiTokens , splitGeminiUsage } from '@/providers/gemini/usage'
2626import {
2727 checkForForcedToolUsage ,
28- cleanSchemaForGemini ,
2928 convertToGeminiFormat ,
3029 convertUsageMetadata ,
3130 createReadableStreamFromGeminiStream ,
@@ -53,7 +52,7 @@ import type {
5352} from '@/providers/types'
5453import {
5554 isDeepResearchModel ,
56- isGemini3Model ,
55+ isGemini3OrLaterModel ,
5756 prepareToolExecution ,
5857 prepareToolsWithUsageControl ,
5958 sumToolCosts ,
@@ -362,19 +361,14 @@ function buildNextConfig(
362361 if ( allForcedToolsUsed && request . responseFormat ) {
363362 nextConfig . tools = undefined
364363 nextConfig . toolConfig = undefined
365- if ( isGemini3Model ( model ) ) {
366- logger . info ( 'Gemini 3: Stripping tools after forced tool execution, schema already set' )
367- } else {
368- nextConfig . responseMimeType = 'application/json'
369- nextConfig . responseSchema = cleanSchemaForGemini ( request . responseFormat . schema ) as Schema
370- logger . info ( 'Using structured output for final response after tool execution' )
371- }
364+ logger . info ( 'Using structured output for final response after tool execution' )
372365 } else if ( state . currentToolConfig ) {
373366 nextConfig . toolConfig = state . currentToolConfig
374367 } else {
375368 nextConfig . toolConfig = { functionCallingConfig : { mode : FunctionCallingConfigMode . AUTO } }
376369 }
377370
371+ applyGeminiResponseFormat ( nextConfig , request . responseFormat , model )
378372 return nextConfig
379373}
380374
@@ -1006,35 +1000,20 @@ export async function executeGeminiRequest(
10061000 geminiConfig . systemInstruction = systemInstruction
10071001 }
10081002
1009- // Handle response format
1010- if ( request . responseFormat && ! tools ?. length ) {
1011- geminiConfig . responseMimeType = 'application/json'
1012- geminiConfig . responseSchema = cleanSchemaForGemini ( request . responseFormat . schema ) as Schema
1013- logger . info ( 'Using Gemini native structured output format' )
1014- } else if ( request . responseFormat && tools ?. length && isGemini3Model ( model ) ) {
1015- geminiConfig . responseMimeType = 'application/json'
1016- geminiConfig . responseJsonSchema = request . responseFormat . schema
1017- logger . info ( 'Using Gemini 3 structured output with tools (responseJsonSchema)' )
1018- } else if ( request . responseFormat && tools ?. length ) {
1019- logger . warn (
1020- 'Gemini 2 does not support responseFormat with tools. Structured output will be applied after tool execution.'
1021- )
1022- }
1023-
1024- // Gemini 3.x takes thinkingLevel directly; Gemini 2.5-series rejects it and needs thinkingBudget.
1003+ // Gemini 3 and later take thinkingLevel directly; Gemini 2.5-series rejects it and needs thinkingBudget.
10251004 // includeThoughts is required for thought parts to appear; it is requested
10261005 // only on agent-events runs so legacy runs keep the pre-agent-events payload.
10271006 if ( request . thinkingLevel && request . thinkingLevel !== 'none' ) {
10281007 const thinkingConfig : ThinkingConfig = { includeThoughts : request . agentEvents === true }
1029- if ( isGemini3Model ( model ) ) {
1008+ if ( isGemini3OrLaterModel ( model ) ) {
10301009 thinkingConfig . thinkingLevel = mapToThinkingLevel ( request . thinkingLevel )
10311010 } else {
10321011 thinkingConfig . thinkingBudget = mapToThinkingBudget ( model , request . thinkingLevel )
10331012 }
10341013 geminiConfig . thinkingConfig = thinkingConfig
10351014 } else if (
10361015 request . thinkingLevel === 'none' &&
1037- ! isGemini3Model ( model ) &&
1016+ ! isGemini3OrLaterModel ( model ) &&
10381017 supportsDisablingGemini25Thinking ( model )
10391018 ) {
10401019 // Omitting thinkingConfig falls back to the API's dynamic default (ON for gemini-2.5-flash),
@@ -1087,17 +1066,19 @@ export async function executeGeminiRequest(
10871066 }
10881067 }
10891068
1069+ applyGeminiResponseFormat ( geminiConfig , request . responseFormat , model )
1070+
10901071 const initialCallTime = Date . now ( )
10911072 /**
10921073 * Gemini 2 cannot combine responseSchema with tools, so structured output
10931074 * is applied on a final schema-configured request after tools settle — the
10941075 * silent path does this; the live loop would break as soon as a turn has
1095- * no calls and skip the schema. Gemini 3 carries responseJsonSchema
1096- * alongside tools, so its live loop keeps structured output .
1076+ * no calls and skip the schema. Gemini 3 and later carry responseJsonSchema
1077+ * alongside automatic tools, or apply it after forced calls settle .
10971078 */
10981079 const hasActiveTools = Boolean ( geminiConfig . tools ?. length )
10991080 const responseFormatNeedsFinalPass =
1100- Boolean ( request . responseFormat ) && hasActiveTools && ! isGemini3Model ( model )
1081+ Boolean ( request . responseFormat ) && hasActiveTools && ! isGemini3OrLaterModel ( model )
11011082 const liveToolLoopSupported = ! responseFormatNeedsFinalPass
11021083 const shouldStream = request . stream && ! hasActiveTools
11031084
@@ -1262,10 +1243,7 @@ export async function executeGeminiRequest(
12621243 tools : undefined ,
12631244 toolConfig : undefined ,
12641245 }
1265- if ( request . responseFormat && ! isGemini3Model ( model ) ) {
1266- finalConfig . responseMimeType = 'application/json'
1267- finalConfig . responseSchema = cleanSchemaForGemini ( request . responseFormat . schema ) as Schema
1268- }
1246+ applyGeminiResponseFormat ( finalConfig , request . responseFormat , model )
12691247
12701248 const finalStartTime = Date . now ( )
12711249 const finalResponsePayload = await prepareConversationGeneration ( request , 'gemini' , {
0 commit comments