refactor(ai-proxy): remove automatic Bash tool injection in Claude Code mode (#3462)

This commit is contained in:
澄潭
2026-02-07 20:24:43 +08:00
committed by GitHub
parent 92ece2c86d
commit cd670e957f
5 changed files with 7 additions and 286 deletions

View File

@@ -233,7 +233,6 @@ Anthropic Claude 所对应的 `type` 为 `claude`。它特有的配置字段如
- 设置 Claude Code 特定的请求头user-agent、x-app、anthropic-beta - 设置 Claude Code 特定的请求头user-agent、x-app、anthropic-beta
- 为请求 URL 添加 `?beta=true` 查询参数 - 为请求 URL 添加 `?beta=true` 查询参数
- 自动注入 Claude Code 的系统提示词(如未提供) - 自动注入 Claude Code 的系统提示词(如未提供)
- 自动注入 Bash 工具定义(如未提供)
这允许在 Higress 中直接使用 Claude Code 的 OAuth Token 进行身份验证。 这允许在 Higress 中直接使用 Claude Code 的 OAuth Token 进行身份验证。
@@ -1240,7 +1239,7 @@ provider:
启用此模式后,插件将自动: 启用此模式后,插件将自动:
- 使用 Bearer Token 认证(而非 x-api-key - 使用 Bearer Token 认证(而非 x-api-key
- 设置 Claude Code 特定的请求头和查询参数 - 设置 Claude Code 特定的请求头和查询参数
- 注入 Claude Code 的系统提示词和 Bash 工具(如未提供) - 注入 Claude Code 的系统提示词(如未提供)
**请求示例** **请求示例**
@@ -1259,7 +1258,6 @@ provider:
插件将自动转换为适合 Claude Code 的请求格式,包括: 插件将自动转换为适合 Claude Code 的请求格式,包括:
- 添加系统提示词:`"You are Claude Code, Anthropic's official CLI for Claude."` - 添加系统提示词:`"You are Claude Code, Anthropic's official CLI for Claude."`
- 添加 Bash 工具定义(用于执行命令)
- 设置适当的认证和请求头 - 设置适当的认证和请求头
### 使用智能协议转换 ### 使用智能协议转换

View File

@@ -199,7 +199,6 @@ When `claudeCodeMode: true` is enabled, the plugin will:
- Set Claude Code-specific request headers (user-agent, x-app, anthropic-beta) - Set Claude Code-specific request headers (user-agent, x-app, anthropic-beta)
- Add `?beta=true` query parameter to request URLs - Add `?beta=true` query parameter to request URLs
- Automatically inject Claude Code system prompt if not provided - Automatically inject Claude Code system prompt if not provided
- Automatically inject Bash tool definition if not provided
This enables direct use of Claude Code OAuth tokens for authentication in Higress. This enables direct use of Claude Code OAuth tokens for authentication in Higress.
@@ -1177,7 +1176,7 @@ provider:
Once this mode is enabled, the plugin will automatically: Once this mode is enabled, the plugin will automatically:
- Use Bearer Token authentication (instead of x-api-key) - Use Bearer Token authentication (instead of x-api-key)
- Set Claude Code-specific request headers and query parameters - Set Claude Code-specific request headers and query parameters
- Inject Claude Code system prompt and Bash tool definitions if not provided - Inject Claude Code system prompt if not provided
**Request Example** **Request Example**
@@ -1196,7 +1195,6 @@ Once this mode is enabled, the plugin will automatically:
The plugin will automatically transform the request into Claude Code format, including: The plugin will automatically transform the request into Claude Code format, including:
- Adding system prompt: `"You are Claude Code, Anthropic's official CLI for Claude."` - Adding system prompt: `"You are Claude Code, Anthropic's official CLI for Claude."`
- Adding Bash tool definition (for command execution)
- Setting appropriate authentication and request headers - Setting appropriate authentication and request headers
### Using Intelligent Protocol Conversion ### Using Intelligent Protocol Conversion

View File

@@ -24,8 +24,6 @@ const (
claudeCodeUserAgent = "claude-cli/2.1.2 (external, cli)" claudeCodeUserAgent = "claude-cli/2.1.2 (external, cli)"
claudeCodeBetaFeatures = "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219" claudeCodeBetaFeatures = "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219"
claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude." claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
claudeCodeBashToolName = "Bash"
claudeCodeBashToolDesc = "Run bash commands"
) )
type claudeProviderInitializer struct{} type claudeProviderInitializer struct{}
@@ -552,32 +550,6 @@ func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRe
claudeRequest.Tools = append(claudeRequest.Tools, claudeTool) claudeRequest.Tools = append(claudeRequest.Tools, claudeTool)
} }
// In Claude Code mode, add Bash tool if not present
if c.config.claudeCodeMode {
hasBashTool := false
for _, tool := range claudeRequest.Tools {
if tool.Name == claudeCodeBashToolName {
hasBashTool = true
break
}
}
if !hasBashTool {
claudeRequest.Tools = append(claudeRequest.Tools, claudeTool{
Name: claudeCodeBashToolName,
Description: claudeCodeBashToolDesc,
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"command": map[string]interface{}{
"type": "string",
},
},
"required": []string{"command"},
},
})
}
}
if tc := origRequest.getToolChoiceObject(); tc != nil { if tc := origRequest.getToolChoiceObject(); tc != nil {
claudeRequest.ToolChoice = &claudeToolChoice{ claudeRequest.ToolChoice = &claudeToolChoice{
Name: tc.Function.Name, Name: tc.Function.Name,

View File

@@ -237,104 +237,6 @@ func TestClaudeProvider_BuildClaudeTextGenRequest_ClaudeCodeMode(t *testing.T) {
assert.Equal(t, "ephemeral", claudeReq.System.ArrayValue[0].CacheControl["type"]) assert.Equal(t, "ephemeral", claudeReq.System.ArrayValue[0].CacheControl["type"])
}) })
t.Run("injects_bash_tool_when_missing", func(t *testing.T) {
request := &chatCompletionRequest{
Model: "claude-sonnet-4-5-20250929",
MaxTokens: 8192,
Messages: []chatMessage{
{Role: roleUser, Content: "List files"},
},
}
claudeReq := provider.buildClaudeTextGenRequest(request)
// Should have Bash tool injected
require.Len(t, claudeReq.Tools, 1)
assert.Equal(t, claudeCodeBashToolName, claudeReq.Tools[0].Name)
assert.Equal(t, claudeCodeBashToolDesc, claudeReq.Tools[0].Description)
// Verify input schema
assert.NotNil(t, claudeReq.Tools[0].InputSchema)
assert.Equal(t, "object", claudeReq.Tools[0].InputSchema["type"])
})
t.Run("does_not_duplicate_bash_tool", func(t *testing.T) {
request := &chatCompletionRequest{
Model: "claude-sonnet-4-5-20250929",
MaxTokens: 8192,
Messages: []chatMessage{
{Role: roleUser, Content: "List files"},
},
Tools: []tool{
{
Type: "function",
Function: function{
Name: "Bash",
Description: "Custom bash tool",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"command": map[string]interface{}{"type": "string"},
},
},
},
},
},
}
claudeReq := provider.buildClaudeTextGenRequest(request)
// Should not duplicate Bash tool
assert.Len(t, claudeReq.Tools, 1)
assert.Equal(t, "Bash", claudeReq.Tools[0].Name)
// Should preserve the original description
assert.Equal(t, "Custom bash tool", claudeReq.Tools[0].Description)
})
t.Run("adds_bash_tool_alongside_existing_tools", func(t *testing.T) {
request := &chatCompletionRequest{
Model: "claude-sonnet-4-5-20250929",
MaxTokens: 8192,
Messages: []chatMessage{
{Role: roleUser, Content: "Hello"},
},
Tools: []tool{
{
Type: "function",
Function: function{
Name: "Read",
Description: "Read files",
Parameters: map[string]interface{}{
"type": "object",
},
},
},
{
Type: "function",
Function: function{
Name: "Write",
Description: "Write files",
Parameters: map[string]interface{}{
"type": "object",
},
},
},
},
}
claudeReq := provider.buildClaudeTextGenRequest(request)
// Should have original tools plus Bash tool
assert.Len(t, claudeReq.Tools, 3)
toolNames := make([]string, len(claudeReq.Tools))
for i, tool := range claudeReq.Tools {
toolNames[i] = tool.Name
}
assert.Contains(t, toolNames, "Read")
assert.Contains(t, toolNames, "Write")
assert.Contains(t, toolNames, "Bash")
})
t.Run("full_request_transformation", func(t *testing.T) { t.Run("full_request_transformation", func(t *testing.T) {
request := &chatCompletionRequest{ request := &chatCompletionRequest{
Model: "claude-sonnet-4-5-20250929", Model: "claude-sonnet-4-5-20250929",
@@ -363,9 +265,8 @@ func TestClaudeProvider_BuildClaudeTextGenRequest_ClaudeCodeMode(t *testing.T) {
require.Len(t, claudeReq.Messages, 1) require.Len(t, claudeReq.Messages, 1)
assert.Equal(t, roleUser, claudeReq.Messages[0].Role) assert.Equal(t, roleUser, claudeReq.Messages[0].Role)
// Verify Bash tool // Verify no tools are injected by default
require.Len(t, claudeReq.Tools, 1) assert.Empty(t, claudeReq.Tools)
assert.Equal(t, "Bash", claudeReq.Tools[0].Name)
// Verify the request can be serialized to JSON // Verify the request can be serialized to JSON
jsonBytes, err := json.Marshal(claudeReq) jsonBytes, err := json.Marshal(claudeReq)
@@ -388,8 +289,6 @@ func TestClaudeConstants(t *testing.T) {
assert.Equal(t, "claude-cli/2.1.2 (external, cli)", claudeCodeUserAgent) assert.Equal(t, "claude-cli/2.1.2 (external, cli)", claudeCodeUserAgent)
assert.Equal(t, "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219", claudeCodeBetaFeatures) assert.Equal(t, "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219", claudeCodeBetaFeatures)
assert.Equal(t, "You are Claude Code, Anthropic's official CLI for Claude.", claudeCodeSystemPrompt) assert.Equal(t, "You are Claude Code, Anthropic's official CLI for Claude.", claudeCodeSystemPrompt)
assert.Equal(t, "Bash", claudeCodeBashToolName)
assert.Equal(t, "Run bash commands", claudeCodeBashToolDesc)
} }
func TestClaudeProvider_GetApiName(t *testing.T) { func TestClaudeProvider_GetApiName(t *testing.T) {

View File

@@ -270,47 +270,6 @@ func RunClaudeOnHttpRequestBodyTests(t *testing.T) {
require.Equal(t, "ephemeral", cacheControlMap["type"]) require.Equal(t, "ephemeral", cacheControlMap["type"])
}) })
t.Run("claude code mode injects bash tool", func(t *testing.T) {
host, status := test.NewTestHost(claudeCodeModeConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "api.anthropic.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
body := `{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 8192,
"messages": [
{"role": "user", "content": "List files"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
require.Equal(t, types.ActionContinue, action)
processedBody := host.GetRequestBody()
var request map[string]interface{}
err := json.Unmarshal(processedBody, &request)
require.NoError(t, err)
// Claude Code mode should inject Bash tool
tools, hasTools := request["tools"]
require.True(t, hasTools, "claude code mode should inject tools")
toolsArr, ok := tools.([]interface{})
require.True(t, ok)
require.Len(t, toolsArr, 1)
bashTool, ok := toolsArr[0].(map[string]interface{})
require.True(t, ok)
require.Equal(t, "Bash", bashTool["name"])
require.Equal(t, "Run bash commands", bashTool["description"])
})
t.Run("claude code mode preserves existing system prompt", func(t *testing.T) { t.Run("claude code mode preserves existing system prompt", func(t *testing.T) {
host, status := test.NewTestHost(claudeCodeModeConfig) host, status := test.NewTestHost(claudeCodeModeConfig)
defer host.Reset() defer host.Reset()
@@ -351,111 +310,6 @@ func RunClaudeOnHttpRequestBodyTests(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.Equal(t, "You are a custom assistant.", systemBlock["text"]) require.Equal(t, "You are a custom assistant.", systemBlock["text"])
}) })
t.Run("claude code mode does not duplicate bash tool", func(t *testing.T) {
host, status := test.NewTestHost(claudeCodeModeConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "api.anthropic.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
body := `{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 8192,
"messages": [
{"role": "user", "content": "Hello"}
],
"tools": [
{
"type": "function",
"function": {
"name": "Bash",
"description": "Custom bash tool",
"parameters": {"type": "object"}
}
}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
require.Equal(t, types.ActionContinue, action)
processedBody := host.GetRequestBody()
var request map[string]interface{}
err := json.Unmarshal(processedBody, &request)
require.NoError(t, err)
// Should not duplicate Bash tool
tools, hasTools := request["tools"]
require.True(t, hasTools)
toolsArr, ok := tools.([]interface{})
require.True(t, ok)
require.Len(t, toolsArr, 1, "should not duplicate Bash tool")
})
t.Run("claude code mode adds bash tool alongside existing tools", func(t *testing.T) {
host, status := test.NewTestHost(claudeCodeModeConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "api.anthropic.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
body := `{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 8192,
"messages": [
{"role": "user", "content": "Hello"}
],
"tools": [
{
"type": "function",
"function": {
"name": "Read",
"description": "Read files",
"parameters": {"type": "object"}
}
}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
require.Equal(t, types.ActionContinue, action)
processedBody := host.GetRequestBody()
var request map[string]interface{}
err := json.Unmarshal(processedBody, &request)
require.NoError(t, err)
// Should have both Read and Bash tools
tools, hasTools := request["tools"]
require.True(t, hasTools)
toolsArr, ok := tools.([]interface{})
require.True(t, ok)
require.Len(t, toolsArr, 2, "should have Read tool plus injected Bash tool")
// Verify both tools exist
toolNames := make([]string, 0)
for _, tool := range toolsArr {
toolMap, ok := tool.(map[string]interface{})
if ok {
if name, hasName := toolMap["name"]; hasName {
toolNames = append(toolNames, name.(string))
}
}
}
require.Contains(t, toolNames, "Read")
require.Contains(t, toolNames, "Bash")
})
}) })
} }