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
- 为请求 URL 添加 `?beta=true` 查询参数
- 自动注入 Claude Code 的系统提示词(如未提供)
- 自动注入 Bash 工具定义(如未提供)
这允许在 Higress 中直接使用 Claude Code 的 OAuth Token 进行身份验证。
@@ -1240,7 +1239,7 @@ provider:
启用此模式后,插件将自动:
- 使用 Bearer Token 认证(而非 x-api-key
- 设置 Claude Code 特定的请求头和查询参数
- 注入 Claude Code 的系统提示词和 Bash 工具(如未提供)
- 注入 Claude Code 的系统提示词(如未提供)
**请求示例**
@@ -1259,7 +1258,6 @@ provider:
插件将自动转换为适合 Claude Code 的请求格式,包括:
- 添加系统提示词:`"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)
- Add `?beta=true` query parameter to request URLs
- 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.
@@ -1177,7 +1176,7 @@ provider:
Once this mode is enabled, the plugin will automatically:
- Use Bearer Token authentication (instead of x-api-key)
- 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**
@@ -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:
- 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
### Using Intelligent Protocol Conversion

View File

@@ -21,11 +21,9 @@ const (
claudeDefaultMaxTokens = 4096
// Claude Code mode constants
claudeCodeUserAgent = "claude-cli/2.1.2 (external, cli)"
claudeCodeBetaFeatures = "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219"
claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
claudeCodeBashToolName = "Bash"
claudeCodeBashToolDesc = "Run bash commands"
claudeCodeUserAgent = "claude-cli/2.1.2 (external, cli)"
claudeCodeBetaFeatures = "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219"
claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
)
type claudeProviderInitializer struct{}
@@ -552,32 +550,6 @@ func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRe
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 {
claudeRequest.ToolChoice = &claudeToolChoice{
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"])
})
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) {
request := &chatCompletionRequest{
Model: "claude-sonnet-4-5-20250929",
@@ -363,9 +265,8 @@ func TestClaudeProvider_BuildClaudeTextGenRequest_ClaudeCodeMode(t *testing.T) {
require.Len(t, claudeReq.Messages, 1)
assert.Equal(t, roleUser, claudeReq.Messages[0].Role)
// Verify Bash tool
require.Len(t, claudeReq.Tools, 1)
assert.Equal(t, "Bash", claudeReq.Tools[0].Name)
// Verify no tools are injected by default
assert.Empty(t, claudeReq.Tools)
// Verify the request can be serialized to JSON
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, "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, "Bash", claudeCodeBashToolName)
assert.Equal(t, "Run bash commands", claudeCodeBashToolDesc)
}
func TestClaudeProvider_GetApiName(t *testing.T) {

View File

@@ -270,47 +270,6 @@ func RunClaudeOnHttpRequestBodyTests(t *testing.T) {
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) {
host, status := test.NewTestHost(claudeCodeModeConfig)
defer host.Reset()
@@ -351,111 +310,6 @@ func RunClaudeOnHttpRequestBodyTests(t *testing.T) {
require.True(t, ok)
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")
})
})
}