@@ -153,7 +153,7 @@ func buildGeminiRequestBody(input GenerateInput) (map[string]interface{}, error)
153153 webSearchTools = append (webSearchTools , map [string ]interface {}{"google_search" : map [string ]interface {}{}})
154154 }
155155 appendToolDeclarations (payload , providerTools , webSearchTools , buildGeminiTools (toolDefinitions ))
156- applyGeminiToolConfigDefaults (payload , len (providerTools )+ len (webSearchTools ) > 0 , len ( toolDefinitions ) > 0 )
156+ applyGeminiToolConfigDefaults (payload , len (providerTools )+ len (webSearchTools ) > 0 )
157157
158158 if len (systemTextParts ) > 0 {
159159 payload ["systemInstruction" ] = map [string ]interface {}{
@@ -167,8 +167,8 @@ func buildGeminiRequestBody(input GenerateInput) (map[string]interface{}, error)
167167 return payload , nil
168168}
169169
170- func applyGeminiToolConfigDefaults (payload map [string ]interface {}, hasServerSideTools bool , hasFunctionDeclarations bool ) {
171- if ! hasServerSideTools || ! hasFunctionDeclarations {
170+ func applyGeminiToolConfigDefaults (payload map [string ]interface {}, hasServerSideTools bool ) {
171+ if ! hasServerSideTools {
172172 return
173173 }
174174 toolConfig := asMap (payload ["toolConfig" ])
@@ -925,26 +925,52 @@ func parseGeminiServerToolCalls(parsed map[string]interface{}) []ToolCall {
925925 }
926926 result := make ([]ToolCall , 0 )
927927 for _ , call := range parseGeminiExplicitServerToolCalls (parsed , - 1 ) {
928- appendUniqueToolCall (& result , call )
928+ appendGeminiServerToolCallForParse (& result , call )
929929 }
930930 for candidateIndex , rawCandidate := range asSlice (parsed ["candidates" ]) {
931931 candidate := asMap (rawCandidate )
932932 for _ , call := range parseGeminiExplicitServerToolCalls (candidate , candidateIndex ) {
933- appendUniqueToolCall (& result , call )
933+ appendGeminiServerToolCallForParse (& result , call )
934934 }
935935 if call , ok := parseGeminiGoogleSearchServerToolCall (candidate , candidateIndex ); ok {
936- appendUniqueToolCall (& result , call )
936+ appendGeminiServerToolCallForParse (& result , call )
937937 }
938938 if call , ok := parseGeminiURLContextServerToolCall (candidate , candidateIndex ); ok {
939- appendUniqueToolCall (& result , call )
939+ appendGeminiServerToolCallForParse (& result , call )
940940 }
941941 for _ , call := range parseGeminiCodeExecutionServerToolCalls (candidate , candidateIndex ) {
942- appendUniqueToolCall (& result , call )
942+ appendGeminiServerToolCallForParse (& result , call )
943943 }
944944 }
945945 return result
946946}
947947
948+ func appendGeminiServerToolCallForParse (calls * []ToolCall , call ToolCall ) {
949+ if calls == nil {
950+ return
951+ }
952+ if isGeminiSingletonServerToolCall (call ) {
953+ for idx , existing := range * calls {
954+ if sameToolCallKind (existing , call ) {
955+ incoming := call
956+ incoming .ToolCallID = ""
957+ (* calls )[idx ] = mergeToolCall (existing , incoming )
958+ return
959+ }
960+ }
961+ }
962+ appendUniqueToolCall (calls , call )
963+ }
964+
965+ func isGeminiSingletonServerToolCall (call ToolCall ) bool {
966+ switch normalizeGeminiServerToolName (firstNonEmptyString (call .ToolName , call .ToolType )) {
967+ case "google_search" , "url_context" :
968+ return true
969+ default :
970+ return false
971+ }
972+ }
973+
948974func parseGeminiExplicitServerToolCalls (payload map [string ]interface {}, candidateIndex int ) []ToolCall {
949975 if len (payload ) == 0 {
950976 return nil
@@ -963,9 +989,8 @@ func parseGeminiExplicitServerToolCalls(payload map[string]interface{}, candidat
963989 }
964990 }
965991 }
966- content := asMap (payload ["content" ])
967- for partIndex , raw := range asSlice (content ["parts" ]) {
968- part := asMap (raw )
992+ for partIndex , raw := range geminiContentParts (payload ) {
993+ part := geminiContentPart (raw )
969994 for _ , key := range []string {"serverSideToolInvocation" , "server_side_tool_invocation" , "toolInvocation" , "tool_invocation" } {
970995 if call , ok := parseGeminiExplicitServerToolCall (asMap (part [key ]), candidateIndex , partIndex ); ok {
971996 result = append (result , call )
@@ -1036,7 +1061,7 @@ func normalizeGeminiExplicitServerToolStatus(status string) string {
10361061}
10371062
10381063func parseGeminiGoogleSearchServerToolCall (candidate map [string ]interface {}, candidateIndex int ) (ToolCall , bool ) {
1039- grounding := asMap (candidate [ "groundingMetadata" ] )
1064+ grounding := geminiGroundingMetadata (candidate )
10401065 if ! hasGeminiSearchGroundingMetadata (grounding ) {
10411066 return ToolCall {}, false
10421067 }
@@ -1062,15 +1087,15 @@ func parseGeminiGoogleSearchServerToolCall(candidate map[string]interface{}, can
10621087}
10631088
10641089func parseGeminiURLContextServerToolCall (candidate map [string ]interface {}, candidateIndex int ) (ToolCall , bool ) {
1065- metadata := asMap (candidate [ "urlContextMetadata" ] )
1090+ metadata := geminiURLContextMetadata (candidate )
10661091 urlMetadata := asSlice (metadata ["urlMetadata" ])
10671092 if len (urlMetadata ) == 0 {
10681093 return ToolCall {}, false
10691094 }
10701095 urls := make ([]string , 0 , len (urlMetadata ))
10711096 for _ , raw := range urlMetadata {
10721097 item := asMap (raw )
1073- if uri := firstNonEmptyString ( getString ( item [ "retrievedUrl" ]), getString ( item [ "url" ]), getString ( item [ "uri" ]) ); uri != "" {
1098+ if uri := geminiURLContextItemURL ( item ); uri != "" {
10741099 urls = append (urls , uri )
10751100 }
10761101 }
@@ -1092,12 +1117,11 @@ func parseGeminiURLContextServerToolCall(candidate map[string]interface{}, candi
10921117}
10931118
10941119func parseGeminiCodeExecutionServerToolCalls (candidate map [string ]interface {}, candidateIndex int ) []ToolCall {
1095- content := asMap (candidate ["content" ])
1096- parts := asSlice (content ["parts" ])
1120+ parts := geminiContentParts (candidate )
10971121 result := make ([]ToolCall , 0 )
10981122 currentIndex := - 1
10991123 for _ , raw := range parts {
1100- part := asMap (raw )
1124+ part := geminiContentPart (raw )
11011125 executableCode := asMap (part ["executableCode" ])
11021126 if len (executableCode ) > 0 {
11031127 currentIndex ++
@@ -1132,6 +1156,65 @@ func parseGeminiCodeExecutionServerToolCalls(candidate map[string]interface{}, c
11321156 return result
11331157}
11341158
1159+ func geminiGroundingMetadata (candidate map [string ]interface {}) map [string ]interface {} {
1160+ grounding := asMap (candidate ["groundingMetadata" ])
1161+ if len (grounding ) == 0 {
1162+ grounding = asMap (candidate ["grounding_metadata" ])
1163+ }
1164+ return normalizeGeminiMapAliases (grounding , map [string ]string {
1165+ "grounding_chunks" : "groundingChunks" ,
1166+ "grounding_supports" : "groundingSupports" ,
1167+ "retrieval_metadata" : "retrievalMetadata" ,
1168+ "search_entry_point" : "searchEntryPoint" ,
1169+ "web_search_queries" : "webSearchQueries" ,
1170+ })
1171+ }
1172+
1173+ func geminiURLContextMetadata (candidate map [string ]interface {}) map [string ]interface {} {
1174+ metadata := asMap (candidate ["urlContextMetadata" ])
1175+ if len (metadata ) == 0 {
1176+ metadata = asMap (candidate ["url_context_metadata" ])
1177+ }
1178+ return normalizeGeminiMapAliases (metadata , map [string ]string {
1179+ "url_metadata" : "urlMetadata" ,
1180+ })
1181+ }
1182+
1183+ func geminiContentParts (candidate map [string ]interface {}) []interface {} {
1184+ content := asMap (candidate ["content" ])
1185+ return asSlice (content ["parts" ])
1186+ }
1187+
1188+ func geminiContentPart (raw interface {}) map [string ]interface {} {
1189+ return normalizeGeminiMapAliases (asMap (raw ), map [string ]string {
1190+ "code_execution_result" : "codeExecutionResult" ,
1191+ "executable_code" : "executableCode" ,
1192+ })
1193+ }
1194+
1195+ func normalizeGeminiMapAliases (item map [string ]interface {}, aliases map [string ]string ) map [string ]interface {} {
1196+ if len (item ) == 0 || len (aliases ) == 0 {
1197+ return item
1198+ }
1199+ normalized := item
1200+ cloned := false
1201+ for alias , canonical := range aliases {
1202+ if _ , ok := normalized [canonical ]; ok {
1203+ continue
1204+ }
1205+ value , ok := item [alias ]
1206+ if ! ok {
1207+ continue
1208+ }
1209+ if ! cloned {
1210+ normalized = cloneMap (item )
1211+ cloned = true
1212+ }
1213+ normalized [canonical ] = value
1214+ }
1215+ return normalized
1216+ }
1217+
11351218func geminiGroundingSources (grounding map [string ]interface {}) []map [string ]interface {} {
11361219 chunks := asSlice (grounding ["groundingChunks" ])
11371220 if len (chunks ) == 0 {
@@ -1140,7 +1223,7 @@ func geminiGroundingSources(grounding map[string]interface{}) []map[string]inter
11401223 result := make ([]map [string ]interface {}, 0 , len (chunks ))
11411224 for _ , raw := range chunks {
11421225 chunk := asMap (raw )
1143- for _ , key := range []string {"web" , "retrievedContext" } {
1226+ for _ , key := range []string {"web" , "retrievedContext" , "retrieved_context" } {
11441227 source := asMap (chunk [key ])
11451228 uri := firstNonEmptyString (getString (source ["uri" ]), getString (source ["url" ]))
11461229 if uri == "" {
@@ -1163,19 +1246,28 @@ func geminiURLContextItems(items []interface{}) []map[string]interface{} {
11631246 result := make ([]map [string ]interface {}, 0 , len (items ))
11641247 for _ , raw := range items {
11651248 item := asMap (raw )
1166- url := firstNonEmptyString ( getString ( item [ "retrievedUrl" ]), getString ( item [ "url" ]), getString ( item [ "uri" ]) )
1249+ url := geminiURLContextItemURL ( item )
11671250 if url == "" {
11681251 continue
11691252 }
11701253 next := map [string ]interface {}{"url" : url }
1171- if status := strings .TrimSpace (getString (item ["urlRetrievalStatus" ])); status != "" {
1254+ if status := strings .TrimSpace (firstNonEmptyString ( getString (item ["urlRetrievalStatus" ]), getString ( item [ "url_retrieval_status" ]) )); status != "" {
11721255 next ["status" ] = status
11731256 }
11741257 result = append (result , next )
11751258 }
11761259 return result
11771260}
11781261
1262+ func geminiURLContextItemURL (item map [string ]interface {}) string {
1263+ return firstNonEmptyString (
1264+ getString (item ["retrievedUrl" ]),
1265+ getString (item ["retrieved_url" ]),
1266+ getString (item ["url" ]),
1267+ getString (item ["uri" ]),
1268+ )
1269+ }
1270+
11791271func compactGeminiServerToolPayload (payload map [string ]interface {}) map [string ]interface {} {
11801272 if len (payload ) == 0 {
11811273 return nil
@@ -1230,20 +1322,20 @@ func parseGeminiCitations(parsed map[string]interface{}) []string {
12301322 citations := make ([]string , 0 )
12311323 for _ , rawCandidate := range asSlice (parsed ["candidates" ]) {
12321324 candidate := asMap (rawCandidate )
1233- grounding := asMap (candidate [ "groundingMetadata" ] )
1325+ grounding := geminiGroundingMetadata (candidate )
12341326 for _ , raw := range asSlice (grounding ["groundingChunks" ]) {
12351327 chunk := asMap (raw )
1236- for _ , key := range []string {"web" , "retrievedContext" } {
1328+ for _ , key := range []string {"web" , "retrievedContext" , "retrieved_context" } {
12371329 source := asMap (chunk [key ])
12381330 if uri := firstNonEmptyString (getString (source ["uri" ]), getString (source ["url" ])); uri != "" {
12391331 citations = append (citations , uri )
12401332 }
12411333 }
12421334 }
1243- urlContext := asMap (candidate [ "urlContextMetadata" ] )
1335+ urlContext := geminiURLContextMetadata (candidate )
12441336 for _ , raw := range asSlice (urlContext ["urlMetadata" ]) {
12451337 item := asMap (raw )
1246- if uri := firstNonEmptyString ( getString ( item [ "retrievedUrl" ]), getString ( item [ "url" ]), getString ( item [ "uri" ]) ); uri != "" {
1338+ if uri := geminiURLContextItemURL ( item ); uri != "" {
12471339 citations = append (citations , uri )
12481340 }
12491341 }
@@ -1256,17 +1348,14 @@ func parseGeminiServerSideToolUsage(parsed map[string]interface{}) map[string]in
12561348 return nil
12571349 }
12581350 result := make (map [string ]int64 )
1259- for _ , rawCandidate := range asSlice (parsed [ "candidates" ] ) {
1260- candidate := asMap ( rawCandidate )
1261- if hasGeminiSearchGroundingMetadata ( asMap ( candidate [ "groundingMetadata" ])) {
1351+ for _ , call := range parseGeminiServerToolCalls (parsed ) {
1352+ switch normalizeGeminiServerToolName ( firstNonEmptyString ( call . ToolName , call . ToolType )) {
1353+ case "google_search" :
12621354 result ["google_search" ] = 1
1263- }
1264- if len (asSlice (asMap (candidate ["urlContextMetadata" ])["urlMetadata" ])) > 0 {
1355+ case "url_context" :
12651356 result ["url_context" ] = 1
1266- }
1267- codeExecutionCount := int64 (len (parseGeminiCodeExecutionServerToolCalls (candidate , 0 )))
1268- if codeExecutionCount > 0 {
1269- result ["code_execution" ] += codeExecutionCount
1357+ case "code_execution" :
1358+ result ["code_execution" ]++
12701359 }
12711360 }
12721361 if len (result ) == 0 {
0 commit comments