@@ -27,7 +27,7 @@ func TestGemini(t *testing.T) {
2727
2828 testConfig := testutil.ComprehensiveTestConfig {
2929 Provider : schemas .Gemini ,
30- ChatModel : "gemini-2.0 -flash" ,
30+ ChatModel : "gemini-2.5 -flash" ,
3131 VisionModel : "gemini-2.0-flash" ,
3232 EmbeddingModel : "text-embedding-004" ,
3333 TranscriptionModel : "gemini-2.5-flash" ,
@@ -288,6 +288,364 @@ func TestBifrostToGeminiToolConversion(t *testing.T) {
288288 }
289289}
290290
291+ // TestStructuredOutputConversion tests that response_format with json_schema is properly converted to Gemini's responseJsonSchema
292+ func TestStructuredOutputConversion (t * testing.T ) {
293+ tests := []struct {
294+ name string
295+ input * schemas.BifrostChatRequest
296+ validate func (t * testing.T , result * gemini.GeminiGenerationRequest )
297+ }{
298+ {
299+ name : "JSONSchemaWithUnionTypes_ConvertedToAnyOf" ,
300+ input : & schemas.BifrostChatRequest {
301+ Model : "gemini-2.5-pro" ,
302+ Input : []schemas.ChatMessage {
303+ {
304+ Role : schemas .ChatMessageRoleUser ,
305+ Content : & schemas.ChatMessageContent {
306+ ContentStr : schemas .Ptr ("Extract information: User ID is 12345, Status is \" active\" " ),
307+ },
308+ },
309+ },
310+ Params : & schemas.ChatParameters {
311+ ResponseFormat : schemas.Ptr [interface {}](map [string ]interface {}{
312+ "type" : "json_schema" ,
313+ "json_schema" : map [string ]interface {}{
314+ "name" : "UserInfo" ,
315+ "schema" : map [string ]interface {}{
316+ "type" : "object" ,
317+ "properties" : map [string ]interface {}{
318+ "user_id" : map [string ]interface {}{
319+ "type" : []interface {}{"string" , "integer" },
320+ "description" : "User ID as string or integer" ,
321+ },
322+ "status" : map [string ]interface {}{
323+ "type" : "string" ,
324+ "enum" : []interface {}{"active" , "inactive" },
325+ },
326+ },
327+ "required" : []interface {}{"user_id" , "status" },
328+ "additionalProperties" : false ,
329+ },
330+ },
331+ }),
332+ },
333+ },
334+ validate : func (t * testing.T , result * gemini.GeminiGenerationRequest ) {
335+ // Verify ResponseMIMEType is set
336+ assert .Equal (t , "application/json" , result .GenerationConfig .ResponseMIMEType , "responseMimeType should be application/json" )
337+
338+ // Verify ResponseJSONSchema is set
339+ assert .NotNil (t , result .GenerationConfig .ResponseJSONSchema , "responseJsonSchema should be set" )
340+
341+ // Validate the schema structure
342+ schemaMap , ok := result .GenerationConfig .ResponseJSONSchema .(map [string ]interface {})
343+ require .True (t , ok , "ResponseJSONSchema should be a map" )
344+
345+ // Check properties
346+ properties , ok := schemaMap ["properties" ].(map [string ]interface {})
347+ require .True (t , ok , "properties should be a map" )
348+
349+ // Validate user_id property - should be converted to anyOf
350+ userID , ok := properties ["user_id" ].(map [string ]interface {})
351+ require .True (t , ok , "user_id should exist in properties" )
352+
353+ // user_id should have anyOf instead of type array
354+ anyOf , hasAnyOf := userID ["anyOf" ]
355+ assert .True (t , hasAnyOf , "user_id should have anyOf for union types" )
356+
357+ anyOfSlice , ok := anyOf .([]interface {})
358+ require .True (t , ok , "anyOf should be a slice" )
359+ require .Len (t , anyOfSlice , 2 , "anyOf should have 2 branches for string and integer" )
360+
361+ // Verify the anyOf branches
362+ stringBranch := anyOfSlice [0 ].(map [string ]interface {})
363+ assert .Equal (t , "string" , stringBranch ["type" ])
364+
365+ integerBranch := anyOfSlice [1 ].(map [string ]interface {})
366+ assert .Equal (t , "integer" , integerBranch ["type" ])
367+
368+ // Validate status property - should remain unchanged
369+ status , ok := properties ["status" ].(map [string ]interface {})
370+ require .True (t , ok , "status should exist in properties" )
371+ assert .Equal (t , "string" , status ["type" ])
372+ enum := status ["enum" ].([]interface {})
373+ assert .Len (t , enum , 2 )
374+ },
375+ },
376+ {
377+ name : "JSONSchemaWithNullableType_KeptAsArray" ,
378+ input : & schemas.BifrostChatRequest {
379+ Model : "gemini-2.5-pro" ,
380+ Input : []schemas.ChatMessage {
381+ {
382+ Role : schemas .ChatMessageRoleUser ,
383+ Content : & schemas.ChatMessageContent {
384+ ContentStr : schemas .Ptr ("Extract nullable field" ),
385+ },
386+ },
387+ },
388+ Params : & schemas.ChatParameters {
389+ ResponseFormat : schemas.Ptr [interface {}](map [string ]interface {}{
390+ "type" : "json_schema" ,
391+ "json_schema" : map [string ]interface {}{
392+ "name" : "NullableData" ,
393+ "schema" : map [string ]interface {}{
394+ "type" : "object" ,
395+ "properties" : map [string ]interface {}{
396+ "name" : map [string ]interface {}{
397+ "type" : []interface {}{"string" , "null" },
398+ },
399+ },
400+ },
401+ },
402+ }),
403+ },
404+ },
405+ validate : func (t * testing.T , result * gemini.GeminiGenerationRequest ) {
406+ schemaMap := result .GenerationConfig .ResponseJSONSchema .(map [string ]interface {})
407+ properties := schemaMap ["properties" ].(map [string ]interface {})
408+ name := properties ["name" ].(map [string ]interface {})
409+
410+ // Nullable types should be kept as array (Gemini supports this)
411+ typeVal := name ["type" ]
412+ typeSlice , ok := typeVal .([]interface {})
413+ require .True (t , ok , "type should remain as array for nullable types" )
414+ require .Len (t , typeSlice , 2 )
415+ assert .Contains (t , typeSlice , "string" )
416+ assert .Contains (t , typeSlice , "null" )
417+ },
418+ },
419+ {
420+ name : "JSONSchemaComplex" ,
421+ input : & schemas.BifrostChatRequest {
422+ Model : "gemini-2.5-pro" ,
423+ Input : []schemas.ChatMessage {
424+ {
425+ Role : schemas .ChatMessageRoleUser ,
426+ Content : & schemas.ChatMessageContent {
427+ ContentStr : schemas .Ptr ("Extract nested data" ),
428+ },
429+ },
430+ },
431+ Params : & schemas.ChatParameters {
432+ ResponseFormat : schemas.Ptr [interface {}](map [string ]interface {}{
433+ "type" : "json_schema" ,
434+ "json_schema" : map [string ]interface {}{
435+ "name" : "ComplexData" ,
436+ "schema" : map [string ]interface {}{
437+ "type" : "object" ,
438+ "properties" : map [string ]interface {}{
439+ "items" : map [string ]interface {}{
440+ "type" : "array" ,
441+ "items" : map [string ]interface {}{
442+ "type" : "object" ,
443+ "properties" : map [string ]interface {}{
444+ "id" : map [string ]interface {}{
445+ "type" : "integer" ,
446+ },
447+ "name" : map [string ]interface {}{
448+ "type" : "string" ,
449+ },
450+ },
451+ "required" : []interface {}{"id" , "name" },
452+ },
453+ },
454+ },
455+ "required" : []interface {}{"items" },
456+ },
457+ },
458+ }),
459+ },
460+ },
461+ validate : func (t * testing.T , result * gemini.GeminiGenerationRequest ) {
462+ assert .Equal (t , "application/json" , result .GenerationConfig .ResponseMIMEType )
463+ assert .NotNil (t , result .GenerationConfig .ResponseJSONSchema )
464+
465+ schemaMap := result .GenerationConfig .ResponseJSONSchema .(map [string ]interface {})
466+ properties := schemaMap ["properties" ].(map [string ]interface {})
467+ items := properties ["items" ].(map [string ]interface {})
468+
469+ // Validate array items
470+ assert .Equal (t , "array" , items ["type" ])
471+ itemsSchema := items ["items" ].(map [string ]interface {})
472+ assert .Equal (t , "object" , itemsSchema ["type" ])
473+
474+ // Validate nested properties
475+ nestedProps := itemsSchema ["properties" ].(map [string ]interface {})
476+ assert .Contains (t , nestedProps , "id" )
477+ assert .Contains (t , nestedProps , "name" )
478+ },
479+ },
480+ {
481+ name : "JSONObjectFormat" ,
482+ input : & schemas.BifrostChatRequest {
483+ Model : "gemini-2.5-pro" ,
484+ Input : []schemas.ChatMessage {
485+ {
486+ Role : schemas .ChatMessageRoleUser ,
487+ Content : & schemas.ChatMessageContent {
488+ ContentStr : schemas .Ptr ("Return JSON" ),
489+ },
490+ },
491+ },
492+ Params : & schemas.ChatParameters {
493+ ResponseFormat : schemas.Ptr [interface {}](map [string ]interface {}{
494+ "type" : "json_object" ,
495+ }),
496+ },
497+ },
498+ validate : func (t * testing.T , result * gemini.GeminiGenerationRequest ) {
499+ // json_object should only set ResponseMIMEType without schema
500+ assert .Equal (t , "application/json" , result .GenerationConfig .ResponseMIMEType )
501+ assert .Nil (t , result .GenerationConfig .ResponseJSONSchema )
502+ assert .Nil (t , result .GenerationConfig .ResponseSchema )
503+ },
504+ },
505+ }
506+
507+ for _ , tt := range tests {
508+ t .Run (tt .name , func (t * testing.T ) {
509+ result := gemini .ToGeminiChatCompletionRequest (tt .input )
510+ require .NotNil (t , result , "Conversion should not return nil" )
511+ tt .validate (t , result )
512+ })
513+ }
514+ }
515+
516+ // TestResponsesStructuredOutputConversion tests that Responses API text config with union types is properly handled
517+ func TestResponsesStructuredOutputConversion (t * testing.T ) {
518+ tests := []struct {
519+ name string
520+ input * schemas.BifrostResponsesRequest
521+ validate func (t * testing.T , result * gemini.GeminiGenerationRequest )
522+ }{
523+ {
524+ name : "ResponsesAPI_UnionTypes_ConvertedToAnyOf" ,
525+ input : & schemas.BifrostResponsesRequest {
526+ Provider : schemas .Gemini ,
527+ Model : "gemini-2.5-pro" ,
528+ Input : []schemas.ResponsesMessage {
529+ {
530+ Role : schemas .Ptr (schemas .ResponsesInputMessageRoleUser ),
531+ Type : schemas .Ptr (schemas .ResponsesMessageTypeMessage ),
532+ Content : & schemas.ResponsesMessageContent {
533+ ContentStr : schemas .Ptr ("Extract info with union types" ),
534+ },
535+ },
536+ },
537+ Params : & schemas.ResponsesParameters {
538+ Text : & schemas.ResponsesTextConfig {
539+ Format : & schemas.ResponsesTextConfigFormat {
540+ Type : "json_schema" ,
541+ Name : schemas .Ptr ("UserInfo" ),
542+ JSONSchema : & schemas.ResponsesTextConfigFormatJSONSchema {
543+ Type : schemas .Ptr ("object" ),
544+ Properties : & map [string ]interface {}{
545+ "user_id" : map [string ]interface {}{
546+ "type" : []interface {}{"string" , "integer" },
547+ "description" : "User ID as string or integer" ,
548+ },
549+ "status" : map [string ]interface {}{
550+ "type" : "string" ,
551+ "enum" : []interface {}{"active" , "inactive" },
552+ },
553+ },
554+ Required : []string {"user_id" , "status" },
555+ AdditionalProperties : schemas .Ptr (false ),
556+ },
557+ },
558+ },
559+ },
560+ },
561+ validate : func (t * testing.T , result * gemini.GeminiGenerationRequest ) {
562+ // Verify ResponseMIMEType is set
563+ assert .Equal (t , "application/json" , result .GenerationConfig .ResponseMIMEType )
564+ assert .NotNil (t , result .GenerationConfig .ResponseJSONSchema )
565+
566+ // Validate the schema structure
567+ schemaMap , ok := result .GenerationConfig .ResponseJSONSchema .(map [string ]interface {})
568+ require .True (t , ok , "ResponseJSONSchema should be a map" )
569+
570+ properties , ok := schemaMap ["properties" ].(map [string ]interface {})
571+ require .True (t , ok , "properties should be a map" )
572+
573+ // Validate user_id property - should be converted to anyOf
574+ userID , ok := properties ["user_id" ].(map [string ]interface {})
575+ require .True (t , ok , "user_id should exist in properties" )
576+
577+ // user_id should have anyOf instead of type array
578+ anyOf , hasAnyOf := userID ["anyOf" ]
579+ assert .True (t , hasAnyOf , "user_id should have anyOf for union types in Responses API" )
580+
581+ anyOfSlice , ok := anyOf .([]interface {})
582+ require .True (t , ok , "anyOf should be a slice" )
583+ require .Len (t , anyOfSlice , 2 , "anyOf should have 2 branches for string and integer" )
584+
585+ // Verify the anyOf branches
586+ stringBranch := anyOfSlice [0 ].(map [string ]interface {})
587+ assert .Equal (t , "string" , stringBranch ["type" ])
588+
589+ integerBranch := anyOfSlice [1 ].(map [string ]interface {})
590+ assert .Equal (t , "integer" , integerBranch ["type" ])
591+ },
592+ },
593+ {
594+ name : "ResponsesAPI_NullableType_KeptAsArray" ,
595+ input : & schemas.BifrostResponsesRequest {
596+ Provider : schemas .Gemini ,
597+ Model : "gemini-2.5-pro" ,
598+ Input : []schemas.ResponsesMessage {
599+ {
600+ Role : schemas .Ptr (schemas .ResponsesInputMessageRoleUser ),
601+ Type : schemas .Ptr (schemas .ResponsesMessageTypeMessage ),
602+ Content : & schemas.ResponsesMessageContent {
603+ ContentStr : schemas .Ptr ("Extract nullable field" ),
604+ },
605+ },
606+ },
607+ Params : & schemas.ResponsesParameters {
608+ Text : & schemas.ResponsesTextConfig {
609+ Format : & schemas.ResponsesTextConfigFormat {
610+ Type : "json_schema" ,
611+ Name : schemas .Ptr ("NullableData" ),
612+ JSONSchema : & schemas.ResponsesTextConfigFormatJSONSchema {
613+ Type : schemas .Ptr ("object" ),
614+ Properties : & map [string ]interface {}{
615+ "name" : map [string ]interface {}{
616+ "type" : []interface {}{"string" , "null" },
617+ },
618+ },
619+ },
620+ },
621+ },
622+ },
623+ },
624+ validate : func (t * testing.T , result * gemini.GeminiGenerationRequest ) {
625+ schemaMap := result .GenerationConfig .ResponseJSONSchema .(map [string ]interface {})
626+ properties := schemaMap ["properties" ].(map [string ]interface {})
627+ name := properties ["name" ].(map [string ]interface {})
628+
629+ // Nullable types should be kept as array (Gemini supports this)
630+ typeVal := name ["type" ]
631+ typeSlice , ok := typeVal .([]interface {})
632+ require .True (t , ok , "type should remain as array for nullable types in Responses API" )
633+ require .Len (t , typeSlice , 2 )
634+ assert .Contains (t , typeSlice , "string" )
635+ assert .Contains (t , typeSlice , "null" )
636+ },
637+ },
638+ }
639+
640+ for _ , tt := range tests {
641+ t .Run (tt .name , func (t * testing.T ) {
642+ result := gemini .ToGeminiResponsesRequest (tt .input )
643+ require .NotNil (t , result , "Responses API conversion should not return nil" )
644+ tt .validate (t , result )
645+ })
646+ }
647+ }
648+
291649// TestBifrostResponsesToGeminiToolConversion tests the conversion of tools from Bifrost Responses API to Gemini format
292650func TestBifrostResponsesToGeminiToolConversion (t * testing.T ) {
293651 tests := []struct {
0 commit comments