From cb5e03a72027e26d18abbfa0be4dce0f77864693 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Mon, 17 Aug 2026 12:36:58 +0800 Subject: [PATCH 1/2] fix(antigravity): preserve mixed Gemini tool config --- .../service/antigravity_gateway_compat.go | 36 +++++++++++++++ .../antigravity_gateway_compat_test.go | 45 +++++++++++++++++++ .../antigravity_gateway_service_test.go | 36 +++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/backend/internal/service/antigravity_gateway_compat.go b/backend/internal/service/antigravity_gateway_compat.go index ab8f0d17a2..49ca076884 100644 --- a/backend/internal/service/antigravity_gateway_compat.go +++ b/backend/internal/service/antigravity_gateway_compat.go @@ -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 "" diff --git a/backend/internal/service/antigravity_gateway_compat_test.go b/backend/internal/service/antigravity_gateway_compat_test.go index ff3d195cbf..3d2176a001 100644 --- a/backend/internal/service/antigravity_gateway_compat_test.go +++ b/backend/internal/service/antigravity_gateway_compat_test.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "encoding/json" "io" "net/http" "net/http/httptest" @@ -222,6 +223,50 @@ 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 := wrapped["request"].(map[string]any) + 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 { diff --git a/backend/internal/service/antigravity_gateway_service_test.go b/backend/internal/service/antigravity_gateway_service_test.go index 00fee73a11..1a1bfc998b 100644 --- a/backend/internal/service/antigravity_gateway_service_test.go +++ b/backend/internal/service/antigravity_gateway_service_test.go @@ -340,6 +340,42 @@ 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 := wrapped["request"].(map[string]any) + toolConfig := request["toolConfig"].(map[string]any) + 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() From 971544570d6da41c6486d0e92f225bf28dfe2d7a Mon Sep 17 00:00:00 2001 From: wucm667 Date: Mon, 17 Aug 2026 17:11:33 +0800 Subject: [PATCH 2/2] test(antigravity): check tool config assertions --- backend/internal/service/antigravity_gateway_compat_test.go | 3 ++- .../internal/service/antigravity_gateway_service_test.go | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/antigravity_gateway_compat_test.go b/backend/internal/service/antigravity_gateway_compat_test.go index 3d2176a001..3df0b0081a 100644 --- a/backend/internal/service/antigravity_gateway_compat_test.go +++ b/backend/internal/service/antigravity_gateway_compat_test.go @@ -254,7 +254,8 @@ func TestBuildAntigravityCompatGeminiBody_ConfiguresMixedToolInvocations(t *test var wrapped map[string]any require.NoError(t, json.Unmarshal(body, &wrapped)) - request := wrapped["request"].(map[string]any) + 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) diff --git a/backend/internal/service/antigravity_gateway_service_test.go b/backend/internal/service/antigravity_gateway_service_test.go index 1a1bfc998b..d1c389c7a5 100644 --- a/backend/internal/service/antigravity_gateway_service_test.go +++ b/backend/internal/service/antigravity_gateway_service_test.go @@ -370,8 +370,10 @@ func TestAntigravityGatewayService_ForwardGemini_PreservesServerSideToolInvocati var wrapped map[string]any require.NoError(t, json.Unmarshal(upstream.requestBodies[0], &wrapped)) - request := wrapped["request"].(map[string]any) - toolConfig := request["toolConfig"].(map[string]any) + 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") }