mirror of
https://github.com/alibaba/higress.git
synced 2026-02-27 06:00:51 +08:00
Compare commits
22 Commits
feat/claud
...
release-2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81c32e60f3 | ||
|
|
ca6ff3a92e | ||
|
|
2c6980e4db | ||
|
|
9ec54cb6f9 | ||
|
|
8ded20967a | ||
|
|
42c8b794b6 | ||
|
|
1e5d840d17 | ||
|
|
6744f6a97a | ||
|
|
9bb315060c | ||
|
|
48a790be39 | ||
|
|
0e822e5140 | ||
|
|
6e98814e5b | ||
|
|
60a3a1c462 | ||
|
|
2e3597493c | ||
|
|
f771229a10 | ||
|
|
c17949e073 | ||
|
|
a0f79d52e9 | ||
|
|
1156696c8c | ||
|
|
6abd337207 | ||
|
|
c8bc2d764d | ||
|
|
f621350d84 | ||
|
|
2df91aa6b1 |
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -21,15 +21,15 @@
|
||||
[submodule "istio/proxy"]
|
||||
path = istio/proxy
|
||||
url = https://github.com/higress-group/proxy
|
||||
branch = istio-1.19
|
||||
branch = envoy-1.36
|
||||
shallow = true
|
||||
[submodule "envoy/go-control-plane"]
|
||||
path = envoy/go-control-plane
|
||||
url = https://github.com/higress-group/go-control-plane
|
||||
branch = istio-1.27
|
||||
branch = envoy-1.36
|
||||
shallow = true
|
||||
[submodule "envoy/envoy"]
|
||||
path = envoy/envoy
|
||||
url = https://github.com/higress-group/envoy
|
||||
branch = envoy-1.27
|
||||
branch = envoy-1.36
|
||||
shallow = true
|
||||
|
||||
@@ -146,7 +146,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.2.0/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.2.1/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -200,8 +200,8 @@ install: pre-install
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= cdf0f16bf622102f89a0d0257834f43f502e4b99
|
||||
ISTIO_LATEST_IMAGE_TAG ?= a7525f292c38d7d3380f3ce7ee971ad6e3c46adf
|
||||
ENVOY_LATEST_IMAGE_TAG ?= ca6ff3a92e3fa592bff706894b22e0509a69757b
|
||||
ISTIO_LATEST_IMAGE_TAG ?= c482b42b9a14885bd6692c6abd01345d50a372f7
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
Submodule envoy/envoy updated: 3fe314c698...b46236685e
Submodule envoy/go-control-plane updated: 90eca02281...af656ebdd1
@@ -123,6 +123,8 @@ template:
|
||||
- name: LITE_METRICS
|
||||
value: "on"
|
||||
{{- end }}
|
||||
- name: ISTIO_DELTA_XDS
|
||||
value: "{{ .Values.global.enableDeltaXDS }}"
|
||||
{{- if include "skywalking.enabled" . }}
|
||||
- name: ISTIO_BOOTSTRAP_OVERRIDE
|
||||
value: /etc/istio/custom-bootstrap/custom_bootstrap.json
|
||||
|
||||
@@ -144,3 +144,7 @@ rules:
|
||||
- apiGroups: [""]
|
||||
verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ]
|
||||
resources: [ "serviceaccounts"]
|
||||
# istio leader election need
|
||||
- apiGroups: ["coordination.k8s.io"]
|
||||
resources: ["leases"]
|
||||
verbs: ["get", "update", "patch", "create"]
|
||||
|
||||
@@ -173,6 +173,8 @@ spec:
|
||||
value: "{{ .Values.global.xdsMaxRecvMsgSize }}"
|
||||
- name: ENBALE_SCOPED_RDS
|
||||
value: "{{ .Values.global.enableSRDS }}"
|
||||
- name: ISTIO_DELTA_XDS
|
||||
value: "{{ .Values.global.enableDeltaXDS }}"
|
||||
- name: ON_DEMAND_RDS
|
||||
value: "{{ .Values.global.onDemandRDS }}"
|
||||
- name: HOST_RDS_MERGE_SUBSET
|
||||
|
||||
@@ -9,6 +9,8 @@ global:
|
||||
xdsMaxRecvMsgSize: "104857600"
|
||||
defaultUpstreamConcurrencyThreshold: 10000
|
||||
enableSRDS: true
|
||||
# -- Whether to enable Istio delta xDS, default is false.
|
||||
enableDeltaXDS: true
|
||||
# -- Whether to enable Redis(redis-stack-server) for Higress, default is false.
|
||||
enableRedis: false
|
||||
enablePluginServer: false
|
||||
|
||||
@@ -163,6 +163,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.defaultResources | object | `{"requests":{"cpu":"10m"}}` | A minimal set of requested resources to applied to all deployments so that Horizontal Pod Autoscaler will be able to function (if set). Each component can overwrite these default values by adding its own resources block in the relevant section below and setting the desired resources values. |
|
||||
| global.defaultUpstreamConcurrencyThreshold | int | `10000` | |
|
||||
| global.disableAlpnH2 | bool | `false` | Whether to disable HTTP/2 in ALPN |
|
||||
| global.enableDeltaXDS | bool | `true` | Whether to enable Istio delta xDS, default is false. |
|
||||
| global.enableGatewayAPI | bool | `true` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableH3 | bool | `false` | |
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
|
||||
Submodule istio/istio updated: c4703274ca...77149ea560
Submodule istio/proxy updated: ced6d8167a...4735dd6b87
@@ -224,18 +224,6 @@ Anthropic Claude 所对应的 `type` 为 `claude`。它特有的配置字段如
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| --------------- | -------- | -------- | ------ | ----------------------------------------- |
|
||||
| `claudeVersion` | string | 可选 | - | Claude 服务的 API 版本,默认为 2023-06-01 |
|
||||
| `claudeCodeMode` | boolean | 可选 | false | 启用 Claude Code 模式,用于支持 Claude Code OAuth 令牌认证。启用后将伪装成 Claude Code 客户端发起请求 |
|
||||
|
||||
**Claude Code 模式说明**
|
||||
|
||||
启用 `claudeCodeMode: true` 时,插件将:
|
||||
- 使用 Bearer Token 认证替代 x-api-key(适配 Claude Code OAuth 令牌)
|
||||
- 设置 Claude Code 特定的请求头(user-agent、x-app、anthropic-beta)
|
||||
- 为请求 URL 添加 `?beta=true` 查询参数
|
||||
- 自动注入 Claude Code 的系统提示词(如未提供)
|
||||
- 自动注入 Bash 工具定义(如未提供)
|
||||
|
||||
这允许在 Higress 中直接使用 Claude Code 的 OAuth Token 进行身份验证。
|
||||
|
||||
#### Ollama
|
||||
|
||||
@@ -1223,45 +1211,6 @@ URL: `http://your-domain/v1/messages`
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 Claude Code 模式
|
||||
|
||||
Claude Code 是 Anthropic 提供的官方 CLI 工具。通过启用 `claudeCodeMode`,可以使用 Claude Code 的 OAuth Token 进行身份验证:
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: claude
|
||||
apiTokens:
|
||||
- 'sk-ant-oat01-xxxxx' # Claude Code OAuth Token
|
||||
claudeCodeMode: true # 启用 Claude Code 模式
|
||||
```
|
||||
|
||||
启用此模式后,插件将自动:
|
||||
- 使用 Bearer Token 认证(而非 x-api-key)
|
||||
- 设置 Claude Code 特定的请求头和查询参数
|
||||
- 注入 Claude Code 的系统提示词和 Bash 工具(如未提供)
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "List files in current directory"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
插件将自动转换为适合 Claude Code 的请求格式,包括:
|
||||
- 添加系统提示词:`"You are Claude Code, Anthropic's official CLI for Claude."`
|
||||
- 添加 Bash 工具定义(用于执行命令)
|
||||
- 设置适当的认证和请求头
|
||||
|
||||
### 使用智能协议转换
|
||||
|
||||
当目标供应商不原生支持 Claude 协议时,插件会自动进行协议转换:
|
||||
|
||||
@@ -185,23 +185,11 @@ For MiniMax, the corresponding `type` is `minimax`. Its unique configuration fie
|
||||
|
||||
#### Anthropic Claude
|
||||
|
||||
For Anthropic Claude, the corresponding `type` is `claude`. Its unique configuration fields are:
|
||||
For Anthropic Claude, the corresponding `type` is `claude`. Its unique configuration field is:
|
||||
|
||||
| Name | Data Type | Filling Requirements | Default Value | Description |
|
||||
|------------|-------------|----------------------|---------------|---------------------------------------------------------------------------------------------------------------|
|
||||
| `claudeVersion` | string | Optional | - | The version of the Claude service's API, default is 2023-06-01. |
|
||||
| `claudeCodeMode` | boolean | Optional | false | Enable Claude Code mode for OAuth token authentication. When enabled, requests will be formatted as Claude Code client requests. |
|
||||
|
||||
**Claude Code Mode**
|
||||
|
||||
When `claudeCodeMode: true` is enabled, the plugin will:
|
||||
- Use Bearer Token authentication instead of x-api-key (compatible with Claude Code OAuth tokens)
|
||||
- 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.
|
||||
|
||||
#### Ollama
|
||||
|
||||
@@ -1160,45 +1148,6 @@ Both protocol formats will return responses in their respective formats:
|
||||
}
|
||||
```
|
||||
|
||||
### Using Claude Code Mode
|
||||
|
||||
Claude Code is Anthropic's official CLI tool. By enabling `claudeCodeMode`, you can authenticate using Claude Code OAuth tokens:
|
||||
|
||||
**Configuration Information**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: claude
|
||||
apiTokens:
|
||||
- "sk-ant-oat01-xxxxx" # Claude Code OAuth Token
|
||||
claudeCodeMode: true # Enable Claude Code mode
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
**Request Example**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "List files in current directory"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
When the target provider doesn't natively support Claude protocol, the plugin automatically performs protocol conversion:
|
||||
|
||||
@@ -150,9 +150,3 @@ func TestBedrock(t *testing.T) {
|
||||
test.RunBedrockOnHttpResponseHeadersTests(t)
|
||||
test.RunBedrockOnHttpResponseBodyTests(t)
|
||||
}
|
||||
|
||||
func TestClaude(t *testing.T) {
|
||||
test.RunClaudeParseConfigTests(t)
|
||||
test.RunClaudeOnHttpRequestHeadersTests(t)
|
||||
test.RunClaudeOnHttpRequestBodyTests(t)
|
||||
}
|
||||
|
||||
@@ -19,13 +19,6 @@ const (
|
||||
claudeDomain = "api.anthropic.com"
|
||||
claudeDefaultVersion = "2023-06-01"
|
||||
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"
|
||||
)
|
||||
|
||||
type claudeProviderInitializer struct{}
|
||||
@@ -326,36 +319,13 @@ func (c *claudeProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiNam
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), c.config.capabilities)
|
||||
util.OverwriteRequestHostHeader(headers, claudeDomain)
|
||||
|
||||
headers.Set("x-api-key", c.config.GetApiTokenInUse(ctx))
|
||||
|
||||
if c.config.apiVersion == "" {
|
||||
c.config.apiVersion = claudeDefaultVersion
|
||||
}
|
||||
|
||||
headers.Set("anthropic-version", c.config.apiVersion)
|
||||
|
||||
// Check if Claude Code mode is enabled
|
||||
if c.config.claudeCodeMode {
|
||||
// Claude Code mode: use OAuth token with Bearer authorization
|
||||
token := c.config.GetApiTokenInUse(ctx)
|
||||
headers.Set("authorization", "Bearer "+token)
|
||||
headers.Del("x-api-key")
|
||||
|
||||
// Set Claude Code specific headers
|
||||
headers.Set("user-agent", claudeCodeUserAgent)
|
||||
headers.Set("x-app", "cli")
|
||||
headers.Set("anthropic-beta", claudeCodeBetaFeatures)
|
||||
|
||||
// Add ?beta=true query parameter to the path
|
||||
currentPath := headers.Get(":path")
|
||||
if currentPath != "" && !strings.Contains(currentPath, "beta=true") {
|
||||
if strings.Contains(currentPath, "?") {
|
||||
headers.Set(":path", currentPath+"&beta=true")
|
||||
} else {
|
||||
headers.Set(":path", currentPath+"?beta=true")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard mode: use x-api-key
|
||||
headers.Set("x-api-key", c.config.GetApiTokenInUse(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
@@ -443,30 +413,11 @@ func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRe
|
||||
claudeRequest.MaxTokens = claudeDefaultMaxTokens
|
||||
}
|
||||
|
||||
// Track if system message exists in original request
|
||||
hasSystemMessage := false
|
||||
for _, message := range origRequest.Messages {
|
||||
if message.Role == roleSystem {
|
||||
hasSystemMessage = true
|
||||
// In Claude Code mode, use array format with cache_control
|
||||
if c.config.claudeCodeMode {
|
||||
claudeRequest.System = &claudeSystemPrompt{
|
||||
ArrayValue: []claudeChatMessageContent{
|
||||
{
|
||||
Type: contentTypeText,
|
||||
Text: message.StringContent(),
|
||||
CacheControl: map[string]interface{}{
|
||||
"type": "ephemeral",
|
||||
},
|
||||
},
|
||||
},
|
||||
IsArray: true,
|
||||
}
|
||||
} else {
|
||||
claudeRequest.System = &claudeSystemPrompt{
|
||||
StringValue: message.StringContent(),
|
||||
IsArray: false,
|
||||
}
|
||||
claudeRequest.System = &claudeSystemPrompt{
|
||||
StringValue: message.StringContent(),
|
||||
IsArray: false,
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -527,22 +478,6 @@ func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRe
|
||||
claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage)
|
||||
}
|
||||
|
||||
// In Claude Code mode, add default system prompt if not present
|
||||
if c.config.claudeCodeMode && !hasSystemMessage {
|
||||
claudeRequest.System = &claudeSystemPrompt{
|
||||
ArrayValue: []claudeChatMessageContent{
|
||||
{
|
||||
Type: contentTypeText,
|
||||
Text: claudeCodeSystemPrompt,
|
||||
CacheControl: map[string]interface{}{
|
||||
"type": "ephemeral",
|
||||
},
|
||||
},
|
||||
},
|
||||
IsArray: true,
|
||||
}
|
||||
}
|
||||
|
||||
for _, tool := range origRequest.Tools {
|
||||
claudeTool := claudeTool{
|
||||
Name: tool.Function.Name,
|
||||
@@ -552,32 +487,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,
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClaudeProviderInitializer_ValidateConfig(t *testing.T) {
|
||||
initializer := &claudeProviderInitializer{}
|
||||
|
||||
t.Run("valid_config_with_api_tokens", func(t *testing.T) {
|
||||
config := &ProviderConfig{
|
||||
apiTokens: []string{"test-token"},
|
||||
}
|
||||
err := initializer.ValidateConfig(config)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid_config_without_api_tokens", func(t *testing.T) {
|
||||
config := &ProviderConfig{
|
||||
apiTokens: nil,
|
||||
}
|
||||
err := initializer.ValidateConfig(config)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no apiToken found in provider config")
|
||||
})
|
||||
|
||||
t.Run("invalid_config_with_empty_api_tokens", func(t *testing.T) {
|
||||
config := &ProviderConfig{
|
||||
apiTokens: []string{},
|
||||
}
|
||||
err := initializer.ValidateConfig(config)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no apiToken found in provider config")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaudeProviderInitializer_DefaultCapabilities(t *testing.T) {
|
||||
initializer := &claudeProviderInitializer{}
|
||||
|
||||
capabilities := initializer.DefaultCapabilities()
|
||||
expected := map[string]string{
|
||||
string(ApiNameChatCompletion): PathAnthropicMessages,
|
||||
string(ApiNameCompletion): PathAnthropicComplete,
|
||||
string(ApiNameAnthropicMessages): PathAnthropicMessages,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
string(ApiNameModels): PathOpenAIModels,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, capabilities)
|
||||
}
|
||||
|
||||
func TestClaudeProviderInitializer_CreateProvider(t *testing.T) {
|
||||
initializer := &claudeProviderInitializer{}
|
||||
|
||||
config := ProviderConfig{
|
||||
apiTokens: []string{"test-token"},
|
||||
}
|
||||
|
||||
provider, err := initializer.CreateProvider(config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, provider)
|
||||
|
||||
assert.Equal(t, providerTypeClaude, provider.GetProviderType())
|
||||
|
||||
claudeProvider, ok := provider.(*claudeProvider)
|
||||
require.True(t, ok)
|
||||
assert.NotNil(t, claudeProvider.config.apiTokens)
|
||||
assert.Equal(t, []string{"test-token"}, claudeProvider.config.apiTokens)
|
||||
}
|
||||
|
||||
func TestClaudeProvider_GetProviderType(t *testing.T) {
|
||||
provider := &claudeProvider{
|
||||
config: ProviderConfig{
|
||||
apiTokens: []string{"test-token"},
|
||||
},
|
||||
contextCache: createContextCache(&ProviderConfig{}),
|
||||
}
|
||||
|
||||
assert.Equal(t, providerTypeClaude, provider.GetProviderType())
|
||||
}
|
||||
|
||||
// Note: TransformRequestHeaders tests are skipped because they require WASM runtime
|
||||
// The header transformation logic is tested via integration tests instead.
|
||||
// Here we test the helper functions and logic that can be unit tested.
|
||||
|
||||
func TestClaudeCodeMode_HeaderLogic(t *testing.T) {
|
||||
// Test the logic for adding beta=true query parameter
|
||||
t.Run("adds_beta_query_param_to_path_without_query", func(t *testing.T) {
|
||||
currentPath := "/v1/messages"
|
||||
var newPath string
|
||||
if currentPath != "" && !strings.Contains(currentPath, "beta=true") {
|
||||
if strings.Contains(currentPath, "?") {
|
||||
newPath = currentPath + "&beta=true"
|
||||
} else {
|
||||
newPath = currentPath + "?beta=true"
|
||||
}
|
||||
} else {
|
||||
newPath = currentPath
|
||||
}
|
||||
assert.Equal(t, "/v1/messages?beta=true", newPath)
|
||||
})
|
||||
|
||||
t.Run("adds_beta_query_param_to_path_with_existing_query", func(t *testing.T) {
|
||||
currentPath := "/v1/messages?foo=bar"
|
||||
var newPath string
|
||||
if currentPath != "" && !strings.Contains(currentPath, "beta=true") {
|
||||
if strings.Contains(currentPath, "?") {
|
||||
newPath = currentPath + "&beta=true"
|
||||
} else {
|
||||
newPath = currentPath + "?beta=true"
|
||||
}
|
||||
} else {
|
||||
newPath = currentPath
|
||||
}
|
||||
assert.Equal(t, "/v1/messages?foo=bar&beta=true", newPath)
|
||||
})
|
||||
|
||||
t.Run("does_not_duplicate_beta_param", func(t *testing.T) {
|
||||
currentPath := "/v1/messages?beta=true"
|
||||
var newPath string
|
||||
if currentPath != "" && !strings.Contains(currentPath, "beta=true") {
|
||||
if strings.Contains(currentPath, "?") {
|
||||
newPath = currentPath + "&beta=true"
|
||||
} else {
|
||||
newPath = currentPath + "?beta=true"
|
||||
}
|
||||
} else {
|
||||
newPath = currentPath
|
||||
}
|
||||
assert.Equal(t, "/v1/messages?beta=true", newPath)
|
||||
})
|
||||
|
||||
t.Run("bearer_token_format", func(t *testing.T) {
|
||||
token := "sk-ant-oat01-oauth-token"
|
||||
bearerAuth := "Bearer " + token
|
||||
assert.Equal(t, "Bearer sk-ant-oat01-oauth-token", bearerAuth)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaudeProvider_BuildClaudeTextGenRequest_StandardMode(t *testing.T) {
|
||||
provider := &claudeProvider{
|
||||
config: ProviderConfig{
|
||||
claudeCodeMode: false,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("builds_request_without_injecting_defaults", func(t *testing.T) {
|
||||
request := &chatCompletionRequest{
|
||||
Model: "claude-sonnet-4-5-20250929",
|
||||
MaxTokens: 8192,
|
||||
Stream: true,
|
||||
Messages: []chatMessage{
|
||||
{Role: roleUser, Content: "Hello"},
|
||||
},
|
||||
}
|
||||
|
||||
claudeReq := provider.buildClaudeTextGenRequest(request)
|
||||
|
||||
// Should not have system prompt injected
|
||||
assert.Nil(t, claudeReq.System)
|
||||
// Should not have tools injected
|
||||
assert.Empty(t, claudeReq.Tools)
|
||||
})
|
||||
|
||||
t.Run("preserves_existing_system_message", func(t *testing.T) {
|
||||
request := &chatCompletionRequest{
|
||||
Model: "claude-sonnet-4-5-20250929",
|
||||
MaxTokens: 8192,
|
||||
Messages: []chatMessage{
|
||||
{Role: roleSystem, Content: "You are a helpful assistant."},
|
||||
{Role: roleUser, Content: "Hello"},
|
||||
},
|
||||
}
|
||||
|
||||
claudeReq := provider.buildClaudeTextGenRequest(request)
|
||||
|
||||
assert.NotNil(t, claudeReq.System)
|
||||
assert.False(t, claudeReq.System.IsArray)
|
||||
assert.Equal(t, "You are a helpful assistant.", claudeReq.System.StringValue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaudeProvider_BuildClaudeTextGenRequest_ClaudeCodeMode(t *testing.T) {
|
||||
provider := &claudeProvider{
|
||||
config: ProviderConfig{
|
||||
claudeCodeMode: true,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("injects_default_system_prompt_when_missing", func(t *testing.T) {
|
||||
request := &chatCompletionRequest{
|
||||
Model: "claude-sonnet-4-5-20250929",
|
||||
MaxTokens: 8192,
|
||||
Stream: true,
|
||||
Messages: []chatMessage{
|
||||
{Role: roleUser, Content: "List files"},
|
||||
},
|
||||
}
|
||||
|
||||
claudeReq := provider.buildClaudeTextGenRequest(request)
|
||||
|
||||
// Should have default Claude Code system prompt
|
||||
require.NotNil(t, claudeReq.System)
|
||||
assert.True(t, claudeReq.System.IsArray)
|
||||
require.Len(t, claudeReq.System.ArrayValue, 1)
|
||||
assert.Equal(t, claudeCodeSystemPrompt, claudeReq.System.ArrayValue[0].Text)
|
||||
assert.Equal(t, contentTypeText, claudeReq.System.ArrayValue[0].Type)
|
||||
// Should have cache_control
|
||||
assert.NotNil(t, claudeReq.System.ArrayValue[0].CacheControl)
|
||||
assert.Equal(t, "ephemeral", claudeReq.System.ArrayValue[0].CacheControl["type"])
|
||||
})
|
||||
|
||||
t.Run("preserves_existing_system_message_with_cache_control", func(t *testing.T) {
|
||||
request := &chatCompletionRequest{
|
||||
Model: "claude-sonnet-4-5-20250929",
|
||||
MaxTokens: 8192,
|
||||
Messages: []chatMessage{
|
||||
{Role: roleSystem, Content: "Custom system prompt"},
|
||||
{Role: roleUser, Content: "Hello"},
|
||||
},
|
||||
}
|
||||
|
||||
claudeReq := provider.buildClaudeTextGenRequest(request)
|
||||
|
||||
// Should preserve custom system prompt but with array format and cache_control
|
||||
require.NotNil(t, claudeReq.System)
|
||||
assert.True(t, claudeReq.System.IsArray)
|
||||
require.Len(t, claudeReq.System.ArrayValue, 1)
|
||||
assert.Equal(t, "Custom system prompt", claudeReq.System.ArrayValue[0].Text)
|
||||
// Should have cache_control
|
||||
assert.NotNil(t, claudeReq.System.ArrayValue[0].CacheControl)
|
||||
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",
|
||||
MaxTokens: 8192,
|
||||
Stream: true,
|
||||
Temperature: 1.0,
|
||||
Messages: []chatMessage{
|
||||
{Role: roleUser, Content: "List files in current directory"},
|
||||
},
|
||||
}
|
||||
|
||||
claudeReq := provider.buildClaudeTextGenRequest(request)
|
||||
|
||||
// Verify complete request structure
|
||||
assert.Equal(t, "claude-sonnet-4-5-20250929", claudeReq.Model)
|
||||
assert.Equal(t, 8192, claudeReq.MaxTokens)
|
||||
assert.True(t, claudeReq.Stream)
|
||||
assert.Equal(t, 1.0, claudeReq.Temperature)
|
||||
|
||||
// Verify system prompt
|
||||
require.NotNil(t, claudeReq.System)
|
||||
assert.True(t, claudeReq.System.IsArray)
|
||||
assert.Equal(t, claudeCodeSystemPrompt, claudeReq.System.ArrayValue[0].Text)
|
||||
|
||||
// Verify messages
|
||||
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 the request can be serialized to JSON
|
||||
jsonBytes, err := json.Marshal(claudeReq)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonBytes)
|
||||
})
|
||||
}
|
||||
|
||||
// Note: TransformRequestBody tests are skipped because they require WASM runtime
|
||||
// The request body transformation is tested indirectly through buildClaudeTextGenRequest tests
|
||||
|
||||
// Test constants
|
||||
func TestClaudeConstants(t *testing.T) {
|
||||
assert.Equal(t, "api.anthropic.com", claudeDomain)
|
||||
assert.Equal(t, "2023-06-01", claudeDefaultVersion)
|
||||
assert.Equal(t, 4096, claudeDefaultMaxTokens)
|
||||
assert.Equal(t, "claude", providerTypeClaude)
|
||||
|
||||
// Claude Code mode constants
|
||||
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) {
|
||||
provider := &claudeProvider{}
|
||||
|
||||
t.Run("messages_path", func(t *testing.T) {
|
||||
assert.Equal(t, ApiNameChatCompletion, provider.GetApiName("/v1/messages"))
|
||||
assert.Equal(t, ApiNameChatCompletion, provider.GetApiName("/api/v1/messages"))
|
||||
})
|
||||
|
||||
t.Run("complete_path", func(t *testing.T) {
|
||||
assert.Equal(t, ApiNameCompletion, provider.GetApiName("/v1/complete"))
|
||||
})
|
||||
|
||||
t.Run("models_path", func(t *testing.T) {
|
||||
assert.Equal(t, ApiNameModels, provider.GetApiName("/v1/models"))
|
||||
})
|
||||
|
||||
t.Run("embeddings_path", func(t *testing.T) {
|
||||
assert.Equal(t, ApiNameEmbeddings, provider.GetApiName("/v1/embeddings"))
|
||||
})
|
||||
|
||||
t.Run("unknown_path", func(t *testing.T) {
|
||||
assert.Equal(t, ApiName(""), provider.GetApiName("/unknown"))
|
||||
})
|
||||
}
|
||||
@@ -442,9 +442,6 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN 豆包服务域名
|
||||
// @Description zh-CN 仅适用于豆包服务,默认转发域名为 ark.cn-beijing.volces.com
|
||||
doubaoDomain string `required:"false" yaml:"doubaoDomain" json:"doubaoDomain"`
|
||||
// @Title zh-CN Claude Code 模式
|
||||
// @Description zh-CN 仅适用于Claude服务。启用后将伪装成Claude Code客户端发起请求,支持使用Claude Code的OAuth Token进行认证。
|
||||
claudeCodeMode bool `required:"false" yaml:"claudeCodeMode" json:"claudeCodeMode"`
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetId() string {
|
||||
@@ -649,7 +646,6 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.vllmServerHost = json.Get("vllmServerHost").String()
|
||||
c.vllmCustomUrl = json.Get("vllmCustomUrl").String()
|
||||
c.doubaoDomain = json.Get("doubaoDomain").String()
|
||||
c.claudeCodeMode = json.Get("claudeCodeMode").Bool()
|
||||
c.contextCleanupCommands = make([]string, 0)
|
||||
for _, cmd := range json.Get("contextCleanupCommands").Array() {
|
||||
if cmd.String() != "" {
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Claude standard mode config
|
||||
var claudeStandardConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
"apiTokens": []string{"sk-ant-api-key-123"},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// Claude Code mode config
|
||||
var claudeCodeModeConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
"apiTokens": []string{"sk-ant-oat01-oauth-token-456"},
|
||||
"claudeCodeMode": true,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// Claude Code mode config with custom apiVersion
|
||||
var claudeCodeModeWithVersionConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
"apiTokens": []string{"sk-ant-oat01-oauth-token-789"},
|
||||
"claudeCodeMode": true,
|
||||
"claudeVersion": "2024-01-01",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// Claude config without token (should fail validation)
|
||||
var claudeNoTokenConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func RunClaudeParseConfigTests(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
t.Run("claude standard config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeStandardConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
t.Run("claude code mode config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
t.Run("claude config without token fails", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeNoTokenConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunClaudeOnHttpRequestHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("claude standard mode uses x-api-key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeStandardConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "x-api-key", "sk-ant-api-key-123"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-version", "2023-06-01"))
|
||||
|
||||
// Should NOT have Claude Code specific headers
|
||||
_, hasAuth := test.GetHeaderValue(requestHeaders, "authorization")
|
||||
require.False(t, hasAuth, "standard mode should not have authorization header")
|
||||
|
||||
_, hasXApp := test.GetHeaderValue(requestHeaders, "x-app")
|
||||
require.False(t, hasXApp, "standard mode should not have x-app header")
|
||||
})
|
||||
|
||||
t.Run("claude code mode uses bearer authorization", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
|
||||
// Claude Code mode should use Bearer authorization
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "authorization", "Bearer sk-ant-oat01-oauth-token-456"))
|
||||
|
||||
// Should NOT have x-api-key in Claude Code mode
|
||||
_, hasXApiKey := test.GetHeaderValue(requestHeaders, "x-api-key")
|
||||
require.False(t, hasXApiKey, "claude code mode should not have x-api-key header")
|
||||
|
||||
// Should have Claude Code specific headers
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "x-app", "cli"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "user-agent", "claude-cli/2.1.2 (external, cli)"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-beta", "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-version", "2023-06-01"))
|
||||
})
|
||||
|
||||
t.Run("claude code mode adds beta query param", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
path, found := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, found)
|
||||
require.Contains(t, path, "beta=true", "claude code mode should add beta=true query param")
|
||||
})
|
||||
|
||||
t.Run("claude code mode with custom version", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeWithVersionConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-version", "2024-01-01"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunClaudeOnHttpRequestBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("claude standard mode does not inject defaults", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeStandardConfig)
|
||||
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,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
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)
|
||||
|
||||
// Standard mode should NOT inject system prompt or tools
|
||||
_, hasSystem := request["system"]
|
||||
require.False(t, hasSystem, "standard mode should not inject system prompt")
|
||||
|
||||
tools, hasTools := request["tools"]
|
||||
if hasTools {
|
||||
toolsArr, ok := tools.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Empty(t, toolsArr, "standard mode should not inject tools")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("claude code mode injects default system prompt", 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,
|
||||
"stream": true,
|
||||
"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 system prompt
|
||||
system, hasSystem := request["system"]
|
||||
require.True(t, hasSystem, "claude code mode should inject system prompt")
|
||||
|
||||
systemArr, ok := system.([]interface{})
|
||||
require.True(t, ok, "system should be an array in claude code mode")
|
||||
require.Len(t, systemArr, 1)
|
||||
|
||||
systemBlock, ok := systemArr[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "text", systemBlock["type"])
|
||||
require.Equal(t, "You are Claude Code, Anthropic's official CLI for Claude.", systemBlock["text"])
|
||||
|
||||
// Should have cache_control
|
||||
cacheControl, hasCacheControl := systemBlock["cache_control"]
|
||||
require.True(t, hasCacheControl, "system prompt should have cache_control")
|
||||
cacheControlMap, ok := cacheControl.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
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()
|
||||
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": "system", "content": "You are a custom assistant."},
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
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 preserve custom system prompt (not default)
|
||||
system, hasSystem := request["system"]
|
||||
require.True(t, hasSystem)
|
||||
|
||||
systemArr, ok := system.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, systemArr, 1)
|
||||
|
||||
systemBlock, ok := systemArr[0].(map[string]interface{})
|
||||
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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Note: Response headers tests are skipped as they require complex mocking
|
||||
// The response header transformation is covered by integration tests
|
||||
@@ -30,7 +30,7 @@ fi
|
||||
CONDITIONAL_HOST_MOUNTS+="--mount type=bind,source=${ROOT}/external/package,destination=/home/package "
|
||||
CONDITIONAL_HOST_MOUNTS+="--mount type=bind,source=${ROOT}/external/envoy,destination=/home/envoy "
|
||||
|
||||
BUILD_TOOLS_IMG=${BUILD_TOOLS_IMG:-"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/build-tools-proxy:release-1.19-ef344298e65eeb2d9e2d07b87eb4e715c2def613"}
|
||||
BUILD_TOOLS_IMG=${BUILD_TOOLS_IMG:-"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/build-tools-proxy:master-eebcdda8856e2d4f528991d27d4808880cce4c52"}
|
||||
|
||||
BUILD_WITH_CONTAINER=1 \
|
||||
CONDITIONAL_HOST_MOUNTS=${CONDITIONAL_HOST_MOUNTS} \
|
||||
|
||||
Reference in New Issue
Block a user