Compare commits

..

22 Commits

Author SHA1 Message Date
johnlanni
81c32e60f3 fix envoy wasm reroute 2026-02-10 20:39:07 +08:00
johnlanni
ca6ff3a92e fix reroute issue 2026-02-10 19:39:07 +08:00
johnlanni
2c6980e4db fix proxy-wasm-cpp-host bug 2026-02-10 19:04:43 +08:00
johnlanni
9ec54cb6f9 update helm doc 2026-02-10 16:37:10 +08:00
johnlanni
8ded20967a update ENVOY_LATEST_IMAGE_TAG 2026-02-10 16:31:07 +08:00
jingze
42c8b794b6 Update submodule to latest commit eb06d49 in envoy
Change-Id: Iafb0df72f84bc7e03ac552942bb131720faf85ff
Co-developed-by: Cursor <noreply@cursor.com>
2026-02-10 11:24:22 +08:00
jingze
1e5d840d17 Update enableDeltaXDS configuration in values.yaml to true and add ISTIO_DELTA_XDS environment variable in pod template
Change-Id: I3df3446264894c710da6026f78d3440d6c6392e1
Co-developed-by: Cursor <noreply@cursor.com>
2026-02-09 17:02:27 +08:00
johnlanni
6744f6a97a fix envoy image 2026-02-06 18:32:53 +08:00
jingze
9bb315060c docs(helm): add enableDeltaXDS configuration to README.md for Istio delta xDS support
Change-Id: Id5908f1e93be5ff40fd23e730fcb16dff6175bf0
Co-developed-by: Cursor <noreply@cursor.com>
2026-02-06 16:27:13 +08:00
johnlanni
48a790be39 Merge remote-tracking branch 'upstream/release-2.2.0' into release-2.2.0 2026-02-06 16:17:19 +08:00
johnlanni
0e822e5140 fix proxy 2026-02-06 16:15:54 +08:00
jingze
6e98814e5b fix(e2e): add enableDeltaXDS configuration to values.yaml and update controller-deployment.yaml to include ISTIO_DELTA_XDS environment variable
Change-Id: I2e938e85b7908a3a82c57abfee47589ea69c334a
Co-developed-by: Cursor <noreply@cursor.com>
2026-02-06 11:56:23 +08:00
johnlanni
60a3a1c462 fix typo 2026-02-06 00:44:53 +08:00
johnlanni
2e3597493c update ENVOY_LATEST_IMAGE_TAG 2026-02-06 00:43:34 +08:00
johnlanni
f771229a10 Merge branch 'main' into release-2.2.0 2026-02-06 00:42:29 +08:00
johnlanni
c17949e073 update istio&proxy 2026-02-05 22:18:00 +08:00
johnlanni
a0f79d52e9 update ENVOY_PACKAGE_URL_PATTERN 2026-02-05 19:26:17 +08:00
EndlessSeeker
1156696c8c feat: update submodules for git (#3455) 2026-02-05 19:23:15 +08:00
johnlanni
6abd337207 update go-control-plane commit 2026-02-05 16:32:07 +08:00
johnlanni
c8bc2d764d update commit 2026-02-05 16:20:45 +08:00
johnlanni
f621350d84 update build-tools-proxy iamge 2026-02-05 16:11:46 +08:00
johnlanni
2df91aa6b1 update upstream commit, istio from 1.19 to 1.27, envoy from 1.27 to 1.36 2026-02-05 15:22:17 +08:00
20 changed files with 30 additions and 1103 deletions

6
.gitmodules vendored
View File

@@ -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

View File

@@ -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'

View File

@@ -1 +1 @@
v2.1.9
v2.2.0

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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` | |

View File

@@ -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 协议时,插件会自动进行协议转换:

View File

@@ -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:

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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"))
})
}

View File

@@ -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() != "" {

View File

@@ -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

View File

@@ -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} \