mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 20:57:32 +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)
|
- 设置 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 工具定义(用于执行命令)
|
|
||||||
- 设置适当的认证和请求头
|
- 设置适当的认证和请求头
|
||||||
|
|
||||||
### 使用智能协议转换
|
### 使用智能协议转换
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user