diff --git a/backend/internal/pkg/antigravity/gemini_types.go b/backend/internal/pkg/antigravity/gemini_types.go index 4fd7882adc..599119618e 100644 --- a/backend/internal/pkg/antigravity/gemini_types.go +++ b/backend/internal/pkg/antigravity/gemini_types.go @@ -86,8 +86,12 @@ type GeminiThinkingConfig struct { type GeminiToolDeclaration struct { FunctionDeclarations []GeminiFunctionDecl `json:"functionDeclarations,omitempty"` GoogleSearch *GeminiGoogleSearch `json:"googleSearch,omitempty"` + CodeExecution *GeminiCodeExecution `json:"codeExecution,omitempty"` } +// GeminiCodeExecution enables Gemini's built-in code execution tool. +type GeminiCodeExecution struct{} + // GeminiFunctionDecl Gemini 函数声明 type GeminiFunctionDecl struct { Name string `json:"name"` diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index 9da3cc33a2..819db987f0 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -710,6 +710,10 @@ func isWebSearchTool(tool ClaudeTool) bool { } } +func isCodeExecutionTool(tool ClaudeTool) bool { + return strings.TrimSpace(tool.Type) == "code_execution" +} + // hasMixedToolInvocations 判断构建后的工具声明是否同时包含函数声明与内置工具 // (googleSearch)。仅在两者并存时需要开启 includeServerSideToolInvocations。 func hasMixedToolInvocations(declarations []GeminiToolDeclaration) bool { @@ -718,7 +722,7 @@ func hasMixedToolInvocations(declarations []GeminiToolDeclaration) bool { if len(d.FunctionDeclarations) > 0 { hasFunc = true } - if d.GoogleSearch != nil { + if d.GoogleSearch != nil || d.CodeExecution != nil { hasBuiltin = true } } @@ -732,11 +736,18 @@ func buildTools(tools []ClaudeTool) []GeminiToolDeclaration { } hasWebSearch := hasWebSearchTool(tools) + hasCodeExecution := false + for _, tool := range tools { + if isCodeExecutionTool(tool) { + hasCodeExecution = true + break + } + } // 普通工具 var funcDecls []GeminiFunctionDecl for _, tool := range tools { - if isWebSearchTool(tool) { + if isWebSearchTool(tool) || isCodeExecutionTool(tool) { continue } // 跳过无效工具名称 @@ -800,6 +811,11 @@ func buildTools(tools []ClaudeTool) []GeminiToolDeclaration { }, }) } + if hasCodeExecution { + declarations = append(declarations, GeminiToolDeclaration{ + CodeExecution: &GeminiCodeExecution{}, + }) + } if len(declarations) == 0 { return nil } diff --git a/backend/internal/pkg/apicompat/chatcompletions_to_responses.go b/backend/internal/pkg/apicompat/chatcompletions_to_responses.go index 384b58495b..f72b5faf6d 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_to_responses.go +++ b/backend/internal/pkg/apicompat/chatcompletions_to_responses.go @@ -428,7 +428,8 @@ func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []R var out []ResponsesTool for _, t := range tools { - if strings.EqualFold(strings.TrimSpace(t.Type), "x_search") { + toolType := strings.ToLower(strings.TrimSpace(t.Type)) + if toolType == "x_search" { out = append(out, ResponsesTool{ Type: "x_search", AllowedXHandles: t.AllowedXHandles, @@ -440,6 +441,10 @@ func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []R }) continue } + if toolType == "web_search" || toolType == "code_execution" { + out = append(out, ResponsesTool{Type: toolType}) + continue + } if t.Type != "function" || t.Function == nil { continue } diff --git a/backend/internal/pkg/apicompat/chatcompletions_x_search_test.go b/backend/internal/pkg/apicompat/chatcompletions_x_search_test.go index 2ffbea2a3b..79230edecd 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_x_search_test.go +++ b/backend/internal/pkg/apicompat/chatcompletions_x_search_test.go @@ -41,6 +41,28 @@ func TestChatCompletionsToResponsesPreservesXSearchTool(t *testing.T) { require.JSONEq(t, `{"type":"x_search"}`, string(resp.ToolChoice)) } +func TestChatCompletionsToResponsesPreservesSupportedBuiltInTools(t *testing.T) { + req := &ChatCompletionsRequest{ + Model: "claude-opus-4-6-thinking", + Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"hello"`)}}, + Tools: []ChatTool{ + {Type: "function", Function: &ChatFunction{Name: "read_file", Parameters: json.RawMessage(`{"type":"object"}`)}}, + {Type: "web_search"}, + {Type: "code_execution"}, + {Type: "unsupported_builtin"}, + {Type: "function"}, + }, + } + + resp, err := ChatCompletionsToResponses(req) + require.NoError(t, err) + require.Len(t, resp.Tools, 3) + require.Equal(t, "function", resp.Tools[0].Type) + require.Equal(t, "read_file", resp.Tools[0].Name) + require.Equal(t, "web_search", resp.Tools[1].Type) + require.Equal(t, "code_execution", resp.Tools[2].Type) +} + func TestResponsesToChatCompletionsPreservesXSearchTool(t *testing.T) { enabled := true req := &ResponsesRequest{ diff --git a/backend/internal/pkg/apicompat/types.go b/backend/internal/pkg/apicompat/types.go index 3b4b618e1f..3a9d0a8ff2 100644 --- a/backend/internal/pkg/apicompat/types.go +++ b/backend/internal/pkg/apicompat/types.go @@ -716,7 +716,7 @@ type ChatFile struct { // ChatTool describes a tool available to the model. type ChatTool struct { - Type string `json:"type"` // "function" | "x_search" + Type string `json:"type"` // "function" | "web_search" | "code_execution" | "x_search" Function *ChatFunction `json:"function,omitempty"` // type=x_search diff --git a/backend/internal/service/antigravity_gateway_compat_test.go b/backend/internal/service/antigravity_gateway_compat_test.go index a92b1654c2..eb4f67ab6e 100644 --- a/backend/internal/service/antigravity_gateway_compat_test.go +++ b/backend/internal/service/antigravity_gateway_compat_test.go @@ -89,8 +89,9 @@ func newAntigravityCompatAccount(accountType string) *Account { "access_token": "stale-account-token", "project_id": "project-3757", "model_mapping": map[string]any{ - "gemini-3.1-pro-high": "gemini-3.1-pro-high", - "claude-sonnet-4-5": "claude-sonnet-4-5", + "gemini-3.1-pro-high": "gemini-3.1-pro-high", + "claude-sonnet-4-5": "claude-sonnet-4-5", + "claude-opus-4-6-thinking": "claude-opus-4-6-thinking", }, }, } @@ -269,6 +270,35 @@ func TestBuildAntigravityCompatGeminiBody_ConfiguresMixedToolInvocations(t *test } } +func TestAntigravityCompatChatMixedBuiltInToolsEnableServerSideInvocations(t *testing.T) { + gin.SetMode(gin.TestMode) + upstream := &queuedHTTPUpstreamStub{responses: []*http.Response{antigravityCompatSuccessResponse()}} + svc := newAntigravityCompatService(config.GatewayConfig{MaxLineSize: defaultMaxLineSize}, upstream) + body := []byte(`{ + "model":"claude-opus-4-6-thinking", + "messages":[{"role":"user","content":"hello"}], + "stream":true, + "tools":[ + {"type":"function","function":{"name":"read_file","parameters":{"type":"object","properties":{"path":{"type":"string"}}}}}, + {"type":"function","function":{"name":"terminal","parameters":{"type":"object","properties":{"command":{"type":"string"}}}}}, + {"type":"web_search"}, + {"type":"code_execution"} + ] + }`) + c, _ := newAntigravityCompatContext(http.MethodPost, "/v1/chat/completions", body) + + result, err := svc.ForwardAsChatCompletions(context.Background(), c, newAntigravityCompatAccount(AccountTypeOAuth), body, nil) + + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, upstream.requestBodies, 1) + requestBody := upstream.requestBodies[0] + require.True(t, gjson.GetBytes(requestBody, "request.toolConfig.includeServerSideToolInvocations").Bool()) + require.Len(t, gjson.GetBytes(requestBody, "request.tools.0.functionDeclarations").Array(), 2) + require.True(t, gjson.GetBytes(requestBody, "request.tools.1.googleSearch").Exists()) + require.True(t, gjson.GetBytes(requestBody, "request.tools.2.codeExecution").Exists()) +} + func TestAntigravityCompatPreservesChatTokenLimit(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct {