mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-06 15:03:54 +08:00
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user