mirror of
https://github.com/alibaba/higress.git
synced 2026-03-07 10:00:48 +08:00
refactor(ai-proxy): remove automatic Bash tool injection in Claude Code mode (#3462)
This commit is contained in:
@@ -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 工具定义(用于执行命令)
|
||||
- 设置适当的认证和请求头
|
||||
|
||||
### 使用智能协议转换
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user