@@ -182,3 +182,208 @@ func TestConvertOpenAIRequestToGeminiCleansToolSchemaRequiredFields(t *testing.T
182182 t .Fatalf ("required[1] = %q, want industry. Schema: %s" , got , schema .Raw )
183183 }
184184}
185+
186+ func TestConvertOpenAIRequestToGemini_ResponseFormatJSONSchema (t * testing.T ) {
187+ inputJSON := `{
188+ "model": "gemini-3.1-flash-lite",
189+ "temperature": 0.2,
190+ "messages": [
191+ {
192+ "role": "user",
193+ "content": [
194+ {
195+ "type": "text",
196+ "text": "Return structured JSON."
197+ }
198+ ]
199+ }
200+ ],
201+ "response_format": {
202+ "type": "json_schema",
203+ "json_schema": {
204+ "name": "response",
205+ "strict": true,
206+ "schema": {
207+ "type": "object",
208+ "properties": {
209+ "cleanedContent": {
210+ "type": "string"
211+ }
212+ },
213+ "required": [
214+ "cleanedContent"
215+ ],
216+ "additionalProperties": false
217+ }
218+ }
219+ }
220+ }`
221+
222+ output := ConvertOpenAIRequestToGemini ("gemini-3.1-flash-lite" , []byte (inputJSON ), false )
223+ result := gjson .ParseBytes (output )
224+ genConfig := result .Get ("generationConfig" )
225+
226+ if got := genConfig .Get ("responseMimeType" ).String (); got != "application/json" {
227+ t .Fatalf ("responseMimeType = %q, want application/json. Output: %s" , got , output )
228+ }
229+ schema := genConfig .Get ("responseJsonSchema" )
230+ if ! schema .Exists () {
231+ t .Fatalf ("responseJsonSchema missing. Output: %s" , output )
232+ }
233+ if genConfig .Get ("responseSchema" ).Exists () {
234+ t .Fatalf ("responseSchema should not be set with responseJsonSchema. Output: %s" , output )
235+ }
236+ if got := schema .Get ("type" ).String (); got != "object" {
237+ t .Fatalf ("schema type = %q, want object. Output: %s" , got , output )
238+ }
239+ if got := schema .Get ("properties.cleanedContent.type" ).String (); got != "string" {
240+ t .Fatalf ("cleanedContent type = %q, want string. Output: %s" , got , output )
241+ }
242+ if additionalProperties := schema .Get ("additionalProperties" ); ! additionalProperties .Exists () || additionalProperties .Bool () {
243+ t .Fatalf ("additionalProperties = %s, want false. Output: %s" , additionalProperties .Raw , output )
244+ }
245+ if got := genConfig .Get ("temperature" ).Float (); got != 0.2 {
246+ t .Fatalf ("temperature = %v, want 0.2. Output: %s" , got , output )
247+ }
248+ }
249+
250+ func TestConvertOpenAIRequestToGemini_ResponseFormatJSONObject (t * testing.T ) {
251+ inputJSON := `{
252+ "model": "gemini-flash-lite",
253+ "messages": [{"role": "user", "content": "Return a JSON object."}],
254+ "response_format": {
255+ "type": "json_object"
256+ }
257+ }`
258+
259+ output := ConvertOpenAIRequestToGemini ("gemini-3.1-flash-lite" , []byte (inputJSON ), false )
260+ result := gjson .ParseBytes (output )
261+ genConfig := result .Get ("generationConfig" )
262+
263+ if got := genConfig .Get ("responseMimeType" ).String (); got != "application/json" {
264+ t .Fatalf ("responseMimeType = %q, want application/json. Output: %s" , got , output )
265+ }
266+ if genConfig .Get ("responseJsonSchema" ).Exists () {
267+ t .Fatalf ("responseJsonSchema should not be set for json_object. Output: %s" , output )
268+ }
269+ }
270+
271+ func TestConvertOpenAIRequestToGemini_ResponseFormatAbsent (t * testing.T ) {
272+ inputJSON := `{
273+ "model": "gemini-flash-lite",
274+ "messages": [{"role": "user", "content": "plain text, no response_format"}],
275+ "temperature": 0.5
276+ }`
277+
278+ output := ConvertOpenAIRequestToGemini ("gemini-3.1-flash-lite" , []byte (inputJSON ), false )
279+ result := gjson .ParseBytes (output )
280+
281+ if result .Get ("generationConfig.responseMimeType" ).Exists () {
282+ t .Fatalf ("responseMimeType should not be set when response_format absent. Output: %s" , output )
283+ }
284+ if result .Get ("generationConfig.responseJsonSchema" ).Exists () {
285+ t .Fatalf ("responseJsonSchema should not be set when response_format absent. Output: %s" , output )
286+ }
287+ if got := result .Get ("generationConfig.temperature" ).Float (); got != 0.5 {
288+ t .Fatalf ("temperature = %v, want 0.5. Output: %s" , got , output )
289+ }
290+ }
291+
292+ func TestConvertOpenAIRequestToGemini_ResponseFormatJSONSchemaNoSchema (t * testing.T ) {
293+ // json_schema without an inner schema field: responseMimeType is still set,
294+ // but responseJsonSchema is not (there is nothing to embed).
295+ inputJSON := `{
296+ "model": "gemini-flash-lite",
297+ "messages": [{"role": "user", "content": "shape only"}],
298+ "response_format": {
299+ "type": "json_schema",
300+ "json_schema": {
301+ "name": "response"
302+ }
303+ }
304+ }`
305+
306+ output := ConvertOpenAIRequestToGemini ("gemini-3.1-flash-lite" , []byte (inputJSON ), false )
307+ result := gjson .ParseBytes (output )
308+ genConfig := result .Get ("generationConfig" )
309+
310+ if got := genConfig .Get ("responseMimeType" ).String (); got != "application/json" {
311+ t .Fatalf ("responseMimeType = %q, want application/json. Output: %s" , got , output )
312+ }
313+ if genConfig .Get ("responseJsonSchema" ).Exists () {
314+ t .Fatalf ("responseJsonSchema should not be set when no schema provided. Output: %s" , output )
315+ }
316+ }
317+
318+ func TestConvertOpenAIRequestToGemini_ResponseFormatUnknownType (t * testing.T ) {
319+ // An unrecognized response_format.type is a no-op: no responseMimeType,
320+ // no responseJsonSchema, no spurious generationConfig created. Sibling
321+ // fields like temperature still pass through.
322+ inputJSON := `{
323+ "model": "gemini-flash-lite",
324+ "messages": [{"role": "user", "content": "plain text"}],
325+ "temperature": 0.7,
326+ "response_format": {
327+ "type": "text"
328+ }
329+ }`
330+
331+ output := ConvertOpenAIRequestToGemini ("gemini-3.1-flash-lite" , []byte (inputJSON ), false )
332+ result := gjson .ParseBytes (output )
333+
334+ if result .Get ("generationConfig.responseMimeType" ).Exists () {
335+ t .Fatalf ("responseMimeType should not be set for unknown type. Output: %s" , output )
336+ }
337+ if result .Get ("generationConfig.responseJsonSchema" ).Exists () {
338+ t .Fatalf ("responseJsonSchema should not be set for unknown type. Output: %s" , output )
339+ }
340+ if got := result .Get ("generationConfig.temperature" ).Float (); got != 0.7 {
341+ t .Fatalf ("temperature = %v, want 0.7. Output: %s" , got , output )
342+ }
343+ }
344+
345+ func TestConvertOpenAIRequestToGemini_ResponseFormatPreservesUserGenerationConfig (t * testing.T ) {
346+ // A user-supplied generationConfig must be preserved; response_format only
347+ // adds responseMimeType/responseJsonSchema on top rather than replacing it.
348+ inputJSON := `{
349+ "model": "gemini-flash-lite",
350+ "messages": [{"role": "user", "content": "merge check"}],
351+ "generationConfig": {
352+ "temperature": 0.9,
353+ "topP": 0.8
354+ },
355+ "response_format": {
356+ "type": "json_schema",
357+ "json_schema": {
358+ "schema": {
359+ "type": "object",
360+ "properties": {"answer": {"type": "string"}},
361+ "required": ["answer"],
362+ "additionalProperties": false
363+ }
364+ }
365+ }
366+ }`
367+
368+ output := ConvertOpenAIRequestToGemini ("gemini-3.1-flash-lite" , []byte (inputJSON ), false )
369+ result := gjson .ParseBytes (output )
370+ genConfig := result .Get ("generationConfig" )
371+
372+ if got := genConfig .Get ("responseMimeType" ).String (); got != "application/json" {
373+ t .Fatalf ("responseMimeType = %q, want application/json. Output: %s" , got , output )
374+ }
375+ schema := genConfig .Get ("responseJsonSchema" )
376+ if ! schema .Exists () {
377+ t .Fatalf ("responseJsonSchema missing. Output: %s" , output )
378+ }
379+ if got := schema .Get ("type" ).String (); got != "object" {
380+ t .Fatalf ("schema type = %q, want object. Output: %s" , got , output )
381+ }
382+ // User-supplied generationConfig fields survive the merge.
383+ if got := genConfig .Get ("temperature" ).Float (); got != 0.9 {
384+ t .Fatalf ("user temperature = %v, want 0.9. Output: %s" , got , output )
385+ }
386+ if got := genConfig .Get ("topP" ).Float (); got != 0.8 {
387+ t .Fatalf ("user topP = %v, want 0.8. Output: %s" , got , output )
388+ }
389+ }
0 commit comments