fix(antigravity): preserve mixed built-in chat tools
This commit is contained in:
Wesley Liddick
2026-08-29 09:07:27 +08:00
committed by GitHub
6 changed files with 83 additions and 6 deletions
@@ -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"`
@@ -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
}
@@ -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
}
@@ -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{
+1 -1
View File
@@ -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
@@ -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 {