Merge pull request #5711 from wucm667/fix/issue-5709-antigravity-tool-config

fix(antigravity): preserve mixed Gemini tool config
This commit is contained in:
Wesley Liddick
2026-08-18 13:55:24 +08:00
committed by GitHub
3 changed files with 120 additions and 0 deletions
@@ -264,6 +264,10 @@ func (s *AntigravityGatewayService) buildAntigravityCompatGeminiBody(
if err != nil {
return nil, err
}
body, err = enableMixedGeminiToolInvocations(body)
if err != nil {
return nil, err
}
body = ensureGeminiFunctionCallThoughtSignatures(body)
body, err = injectIdentityPatchToGeminiRequest(body)
if err != nil {
@@ -280,6 +284,38 @@ func (s *AntigravityGatewayService) buildAntigravityCompatGeminiBody(
return antigravity.TransformClaudeToGeminiWithOptions(claudeRequest, projectID, mappedModel, options)
}
func enableMixedGeminiToolInvocations(body []byte) ([]byte, error) {
var request map[string]any
if err := json.Unmarshal(body, &request); err != nil {
return nil, err
}
var hasGoogleSearch, hasFunctionDeclarations bool
if tools, ok := request["tools"].([]any); ok {
for _, rawTool := range tools {
tool, ok := rawTool.(map[string]any)
if !ok {
continue
}
_, hasSearch := tool["googleSearch"]
declarations, hasFunctions := tool["functionDeclarations"].([]any)
hasGoogleSearch = hasGoogleSearch || hasSearch
hasFunctionDeclarations = hasFunctionDeclarations || hasFunctions && len(declarations) > 0
}
}
if !hasGoogleSearch || !hasFunctionDeclarations {
return body, nil
}
toolConfig, _ := request["toolConfig"].(map[string]any)
if toolConfig == nil {
toolConfig = make(map[string]any)
request["toolConfig"] = toolConfig
}
toolConfig["includeServerSideToolInvocations"] = true
return json.Marshal(request)
}
func antigravityCompatProxyURL(account *Account) string {
if account.ProxyID == nil || account.Proxy == nil {
return ""
@@ -3,6 +3,7 @@ package service
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
@@ -222,6 +223,51 @@ func TestAntigravityCompatRejectsUnsupportedAccountType(t *testing.T) {
}
}
func TestBuildAntigravityCompatGeminiBody_ConfiguresMixedToolInvocations(t *testing.T) {
svc := &AntigravityGatewayService{}
tests := []struct {
name string
tools string
wantField bool
}{
{
name: "mixed server and client tools",
tools: `[{"name":"get_weather","input_schema":{"type":"object"}},{"type":"web_search_20250305","name":"web_search"}]`,
wantField: true,
},
{
name: "client tools only",
tools: `[{"name":"get_weather","input_schema":{"type":"object"}}]`,
},
{
name: "server tools only",
tools: `[{"type":"web_search_20250305","name":"web_search"}]`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
claudeBody := []byte(`{"messages":[{"role":"user","content":"hello"}],"tools":` + tt.tools + `}`)
claudeBody = bytes.ReplaceAll(claudeBody, []byte{92}, nil)
body, err := svc.buildAntigravityCompatGeminiBody(context.Background(), claudeBody, nil, "project-1", "gemini-2.5-flash")
require.NoError(t, err)
var wrapped map[string]any
require.NoError(t, json.Unmarshal(body, &wrapped))
request, ok := wrapped["request"].(map[string]any)
require.True(t, ok)
toolConfig, exists := request["toolConfig"].(map[string]any)
if !tt.wantField {
require.False(t, exists)
return
}
require.True(t, exists)
require.Equal(t, true, toolConfig["includeServerSideToolInvocations"])
require.NotContains(t, toolConfig, "include_server_side_tool_invocations")
})
}
}
func TestAntigravityCompatPreservesChatTokenLimit(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
@@ -340,6 +340,44 @@ func TestAntigravityGatewayService_ForwardGemini_UsesConfiguredProjectFallback(t
require.Equal(t, "configured-project", wrapped["project"])
}
func TestAntigravityGatewayService_ForwardGemini_PreservesServerSideToolInvocationConfig(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}],"tools":[{"functionDeclarations":[{"name":"get_weather","parameters":{"type":"object","additionalProperties":false}}]},{"googleSearch":{}}],"toolConfig":{"includeServerSideToolInvocations":true}}`)
writer := httptest.NewRecorder()
c, _ := gin.CreateTestContext(writer)
body = bytes.ReplaceAll(body, []byte{92}, nil)
c.Request = httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-2.5-flash:generateContent", bytes.NewReader(body))
upstream := &queuedHTTPUpstreamStub{responses: []*http.Response{{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader("data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"ok\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{}}}\n\n")),
}}}
svc := &AntigravityGatewayService{
settingService: NewSettingService(&antigravitySettingRepoStub{}, &config.Config{Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize}}),
tokenProvider: &AntigravityTokenProvider{},
httpUpstream: upstream,
}
account := &Account{
ID: 103, Name: "native-gemini", Platform: PlatformAntigravity, Type: AccountTypeOAuth, Status: StatusActive, Concurrency: 1,
Credentials: map[string]any{"access_token": "token", "project_id": "project-103", "model_mapping": map[string]any{"gemini-2.5-flash": "gemini-2.5-flash"}},
}
result, err := svc.ForwardGemini(context.Background(), c, account, "gemini-2.5-flash", "generateContent", false, body, false)
require.NoError(t, err)
require.NotNil(t, result)
require.Len(t, upstream.requestBodies, 1)
var wrapped map[string]any
require.NoError(t, json.Unmarshal(upstream.requestBodies[0], &wrapped))
request, ok := wrapped["request"].(map[string]any)
require.True(t, ok)
toolConfig, ok := request["toolConfig"].(map[string]any)
require.True(t, ok)
require.Equal(t, true, toolConfig["includeServerSideToolInvocations"])
require.NotContains(t, toolConfig, "include_server_side_tool_invocations")
}
func TestAntigravityGatewayService_ForwardGemini_MissingProjectReturnsLocalError(t *testing.T) {
gin.SetMode(gin.TestMode)
writer := httptest.NewRecorder()