Compare commits

...

11 Commits

38 changed files with 4937 additions and 212 deletions

View File

@@ -10,14 +10,14 @@ Deploy and configure Higress AI Gateway for Clawdbot/OpenClaw integration with o
## Prerequisites
- Docker installed and running
- Internet access to download the setup script
- Internet access to download setup script
- LLM provider API keys (at least one)
## Workflow
### Step 1: Download Setup Script
Download the official get-ai-gateway.sh script:
Download official get-ai-gateway.sh script:
```bash
curl -fsSL https://raw.githubusercontent.com/higress-group/higress-standalone/main/all-in-one/get-ai-gateway.sh -o get-ai-gateway.sh
@@ -26,15 +26,17 @@ chmod +x get-ai-gateway.sh
### Step 2: Gather Configuration
Ask the user for:
Ask user for:
1. **LLM Provider API Keys** (at least one required):
**Top Commonly Used Providers:**
- Aliyun Dashscope (Qwen): `--dashscope-key`
- DeepSeek: `--deepseek-key`
- Moonshot (Kimi): `--moonshot-key`
- Zhipu AI: `--zhipuai-key`
- Claude Code (OAuth mode): `--claude-code-key` (run `claude setup-token` to get token)
- Claude: `--claude-key`
- Minimax: `--minimax-key`
- Azure OpenAI: `--azure-key`
- AWS Bedrock: `--bedrock-key`
@@ -42,8 +44,8 @@ Ask the user for:
- OpenAI: `--openai-key`
- OpenRouter: `--openrouter-key`
- Grok: `--grok-key`
See CLI Parameters Reference for complete list with model pattern options.
To configure additional providers beyond the above, run `./get-ai-gateway.sh --help` to view the complete list of supported models and providers.
2. **Port Configuration** (optional):
- HTTP port: `--http-port` (default: 8080)
@@ -56,7 +58,7 @@ Ask the user for:
### Step 3: Run Setup Script
Run the script in non-interactive mode with gathered parameters:
Run script in non-interactive mode with gathered parameters:
```bash
./get-ai-gateway.sh start --non-interactive \
@@ -100,23 +102,23 @@ After script completion:
docker ps --filter "name=higress-ai-gateway"
```
2. Test the gateway endpoint:
2. Test gateway endpoint:
```bash
curl http://localhost:8080/v1/models
```
3. Access the console (optional):
3. Access console (optional):
```
http://localhost:8001
```
### Step 5: Configure Clawdbot/OpenClaw Plugin
If the user wants to use Higress with Clawdbot/OpenClaw, install the appropriate plugin:
If user wants to use Higress with Clawdbot/OpenClaw, install appropriate plugin:
#### Automatic Installation
Detect runtime and install the correct plugin version:
Detect runtime and install correct plugin version:
```bash
# Detect which runtime is installed
@@ -133,7 +135,7 @@ else
exit 1
fi
# Install the plugin
# Install plugin
PLUGIN_DEST="$RUNTIME_DIR/extensions/higress-ai-gateway"
echo "Installing Higress AI Gateway plugin for $RUNTIME..."
mkdir -p "$(dirname "$PLUGIN_DEST")"
@@ -142,7 +144,7 @@ cp -r "$PLUGIN_SRC" "$PLUGIN_DEST"
echo "✓ Plugin installed at: $PLUGIN_DEST"
# Configure provider
echo
echo ""
echo "Configuring provider..."
$RUNTIME models auth login --provider higress
```
@@ -161,10 +163,8 @@ After deployment, manage API keys without redeploying:
```bash
# View configured API keys
./get-ai-gateway.sh config list
# Add or update an API key (hot-reload, no restart needed)
./get-ai-gateway.sh config add --provider <provider> --key <api-key>
# Remove an API key (hot-reload, no restart needed)
./get-ai-gateway.sh config remove --provider <provider>
```
@@ -220,13 +220,15 @@ PLUGIN_REGISTRY="higress-registry.ap-southeast-7.cr.aliyuncs.com" \
| `--deepseek-key` | DeepSeek |
| `--moonshot-key` | Moonshot (Kimi) |
| `--zhipuai-key` | Zhipu AI |
| `--claude-code-key` | Claude Code (OAuth mode - run `claude setup-token` to get token) |
| `--claude-key` | Claude |
| `--openai-key` | OpenAI |
| `--openrouter-key` | OpenRouter |
| `--claude-key` | Claude |
| `--gemini-key` | Google Gemini |
| `--groq-key` | Groq |
**Additional Providers:**
`--doubao-key`, `--baichuan-key`, `--yi-key`, `--stepfun-key`, `--minimax-key`, `--cohere-key`, `--mistral-key`, `--github-key`, `--fireworks-key`, `--togetherai-key`, `--grok-key`, `--azure-key`, `--bedrock-key`, `--vertex-key`
## Managing Configuration
@@ -236,15 +238,14 @@ PLUGIN_REGISTRY="higress-registry.ap-southeast-7.cr.aliyuncs.com" \
```bash
# List all configured API keys
./get-ai-gateway.sh config list
# Add or update an API key (hot-reload)
./get-ai-gateway.sh config add --provider deepseek --key sk-xxx
# Remove an API key (hot-reload)
./get-ai-gateway.sh config remove --provider deepseek
```
**Supported provider aliases:**
`dashscope`/`qwen`, `moonshot`/`kimi`, `zhipuai`/`zhipu`, `togetherai`/`together`
### Routing Rules
@@ -252,10 +253,8 @@ PLUGIN_REGISTRY="higress-registry.ap-southeast-7.cr.aliyuncs.com" \
```bash
# Add a routing rule
./get-ai-gateway.sh route add --model claude-opus-4.5 --trigger "深入思考|deep thinking"
# List all rules
./get-ai-gateway.sh route list
# Remove a rule
./get-ai-gateway.sh route remove --rule-id 0
```
@@ -265,11 +264,12 @@ See [higress-auto-router](../higress-auto-router/SKILL.md) for detailed document
## Access Logs
Gateway access logs are available at:
```
$DATA_FOLDER/logs/access.log
```
These logs can be used with the **agent-session-monitor** skill for token tracking and conversation analysis.
These logs can be used with **agent-session-monitor** skill for token tracking and conversation analysis.
## Related Skills
@@ -295,6 +295,7 @@ These logs can be used with the **agent-session-monitor** skill for token tracki
```
**Response:**
```
Auto-detected timezone: Asia/Shanghai
Selected plugin registry: higress-registry.cn-hangzhou.cr.aliyuncs.com
@@ -325,6 +326,7 @@ curl 'http://localhost:8080/v1/chat/completions' \
4. Set up session monitoring
**Response:**
```
Auto-detected timezone: Asia/Shanghai
Selected plugin registry: higress-registry.cn-hangzhou.cr.aliyuncs.com
@@ -366,6 +368,7 @@ Selected plugin registry: higress-registry.cn-hangzhou.cr.aliyuncs.com
```
**Response:**
```
当前配置的API keys:
@@ -399,6 +402,7 @@ Configuration has been hot-reloaded (no restart needed).
```
**Response:**
```
Auto-detected timezone: America/Los_Angeles
Selected plugin registry: higress-registry.us-west-1.cr.aliyuncs.com

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

@@ -182,6 +182,8 @@ Higress would not be possible without the valuable open-source work of projects
- Higress Console: https://github.com/higress-group/higress-console
- Higress Standalone: https://github.com/higress-group/higress-standalone
- Higress Plugin Serverhttps://github.com/higress-group/plugin-server
- Higress Wasm Plugin Golang SDKhttps://github.com/higress-group/wasm-go
### Contributors

View File

@@ -208,6 +208,8 @@ WeChat公式アカウント
- Higressコンソールhttps://github.com/higress-group/higress-console
- Higressスタンドアロン版https://github.com/higress-group/higress-standalone
- Higress Plugin Serverhttps://github.com/higress-group/plugin-server
- Higress Wasm Plugin Golang SDKhttps://github.com/higress-group/wasm-go
### 貢献者

View File

@@ -221,6 +221,8 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
- Higress 控制台https://github.com/higress-group/higress-console
- Higress独立运行版https://github.com/higress-group/higress-standalone
- Higress 插件服务器https://github.com/higress-group/plugin-server
- Higress Wasm 插件 Golang SDKhttps://github.com/higress-group/wasm-go
### 贡献者

View File

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

45
go.mod
View File

@@ -20,7 +20,7 @@ require (
github.com/caddyserver/certmagic v0.21.3
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
github.com/dubbogo/gost v1.13.1
github.com/envoyproxy/go-control-plane/envoy v1.35.0
github.com/envoyproxy/go-control-plane/envoy v1.36.0
github.com/go-errors/errors v1.5.1
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.4
@@ -38,10 +38,10 @@ require (
github.com/tidwall/gjson v1.17.0
go.uber.org/atomic v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.44.0
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
golang.org/x/net v0.47.0
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
istio.io/api v1.27.1-0.20250820125923-f5a5d3a605a9
istio.io/client-go v1.27.1-0.20250820130622-12f6d11feb40
istio.io/istio v0.0.0
@@ -65,7 +65,7 @@ require (
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.4 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/logging v1.13.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
dario.cat/mergo v1.0.2 // indirect
@@ -103,11 +103,10 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -117,23 +116,23 @@ require (
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/envoyproxy/go-control-plane v0.13.4 // indirect
github.com/envoyproxy/go-control-plane v0.14.0 // indirect
github.com/envoyproxy/go-control-plane/contrib v0.0.0-20251016030003-90eca0228178 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
@@ -220,7 +219,7 @@ require (
github.com/yl2chen/cidranger v1.0.2 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
@@ -231,24 +230,24 @@ require (
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/api v0.250.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/gcfg.v1 v1.2.3 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect

1897
go.sum
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.9
appVersion: 2.2.0
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -15,4 +15,4 @@ dependencies:
repository: "file://../redis"
version: 0.0.1
type: application
version: 2.1.9
version: 2.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

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 2.1.9
version: 2.2.0
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.1.9
digest: sha256:d696af6726b40219cc16e7cf8de7400101479dfbd8deb3101d7ee736415b9875
generated: "2025-11-13T16:33:49.721553+08:00"
version: 2.2.0
digest: sha256:2cb148fa6d52856344e1905d3fea018466c2feb52013e08997c2d5c7d50f2e5d
generated: "2026-02-11T17:45:59.187965929+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.9
appVersion: 2.2.0
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 2.1.9
version: 2.2.0
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.1.9
version: 2.2.0
type: application
version: 2.1.9
version: 2.2.0

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

@@ -64,16 +64,16 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/envoyproxy/go-control-plane/contrib v0.0.0-20251016030003-90eca0228178 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
@@ -111,10 +111,9 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.3 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.7.27 // indirect
github.com/containerd/continuity v0.4.4 // indirect
@@ -132,7 +131,7 @@ require (
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -152,7 +151,7 @@ require (
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.0 // indirect
@@ -162,7 +161,6 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
@@ -231,7 +229,6 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/prometheus v0.307.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rubenv/sql-migrate v1.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -268,26 +265,26 @@ require (
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
istio.io/api v1.27.1-0.20250820125923-f5a5d3a605a9 // indirect

View File

File diff suppressed because it is too large Load Diff

View File

@@ -224,6 +224,17 @@ 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 的系统提示词(如未提供)
这允许在 Higress 中直接使用 Claude Code 的 OAuth Token 进行身份验证。
#### Ollama
@@ -1211,6 +1222,44 @@ 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 的系统提示词(如未提供)
**请求示例**
```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."`
- 设置适当的认证和请求头
### 使用智能协议转换
当目标供应商不原生支持 Claude 协议时,插件会自动进行协议转换:

View File

@@ -185,11 +185,22 @@ For MiniMax, the corresponding `type` is `minimax`. Its unique configuration fie
#### Anthropic Claude
For Anthropic Claude, the corresponding `type` is `claude`. Its unique configuration field is:
For Anthropic Claude, the corresponding `type` is `claude`. Its unique configuration fields are:
| 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
This enables direct use of Claude Code OAuth tokens for authentication in Higress.
#### Ollama
@@ -1148,6 +1159,44 @@ 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 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."`
- 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

@@ -149,4 +149,11 @@ func TestBedrock(t *testing.T) {
test.RunBedrockOnHttpRequestBodyTests(t)
test.RunBedrockOnHttpResponseHeadersTests(t)
test.RunBedrockOnHttpResponseBodyTests(t)
test.RunBedrockToolCallTests(t)
}
func TestClaude(t *testing.T) {
test.RunClaudeParseConfigTests(t)
test.RunClaudeOnHttpRequestHeadersTests(t)
test.RunClaudeOnHttpRequestBodyTests(t)
}

View File

@@ -206,7 +206,16 @@ func (m *azureProvider) transformRequestPath(ctx wrapper.HttpContext, apiName Ap
path = strings.ReplaceAll(path, pathAzureModelPlaceholder, model)
log.Debugf("azureProvider: model replaced path: %s", path)
}
path = path + "?" + m.serviceUrl.RawQuery
if !strings.Contains(path, "?") {
// No query string yet
path = path + "?" + m.serviceUrl.RawQuery
} else if strings.HasSuffix(path, "?") {
// Ends with "?" and has no query parameter
path = path + m.serviceUrl.RawQuery
} else {
// Has other query parameters
path = path + "&" + m.serviceUrl.RawQuery
}
log.Debugf("azureProvider: final path: %s", path)
return path

View File

@@ -769,7 +769,15 @@ func (b *bedrockProvider) buildBedrockTextGenerationRequest(origRequest *chatCom
case roleSystem:
systemMessages = append(systemMessages, systemContentBlock{Text: msg.StringContent()})
case roleTool:
messages = append(messages, chatToolMessage2BedrockMessage(msg))
toolResultContent := chatToolMessage2BedrockToolResultContent(msg)
if len(messages) > 0 && messages[len(messages)-1].Role == roleUser && messages[len(messages)-1].Content[0].ToolResult != nil {
messages[len(messages)-1].Content = append(messages[len(messages)-1].Content, toolResultContent)
} else {
messages = append(messages, bedrockMessage{
Role: roleUser,
Content: []bedrockMessageContent{toolResultContent},
})
}
default:
messages = append(messages, chatMessage2BedrockMessage(msg))
}
@@ -1060,7 +1068,7 @@ type tokenUsage struct {
TotalTokens int `json:"totalTokens"`
}
func chatToolMessage2BedrockMessage(chatMessage chatMessage) bedrockMessage {
func chatToolMessage2BedrockToolResultContent(chatMessage chatMessage) bedrockMessageContent {
toolResultContent := &toolResultBlock{}
toolResultContent.ToolUseId = chatMessage.ToolCallId
if text, ok := chatMessage.Content.(string); ok {
@@ -1083,29 +1091,29 @@ func chatToolMessage2BedrockMessage(chatMessage chatMessage) bedrockMessage {
} else {
log.Warnf("the content type is not supported, current content is %v", chatMessage.Content)
}
return bedrockMessage{
Role: roleUser,
Content: []bedrockMessageContent{
{
ToolResult: toolResultContent,
},
},
return bedrockMessageContent{
ToolResult: toolResultContent,
}
}
func chatMessage2BedrockMessage(chatMessage chatMessage) bedrockMessage {
var result bedrockMessage
if len(chatMessage.ToolCalls) > 0 {
contents := make([]bedrockMessageContent, 0, len(chatMessage.ToolCalls))
for _, toolCall := range chatMessage.ToolCalls {
params := map[string]interface{}{}
json.Unmarshal([]byte(toolCall.Function.Arguments), &params)
contents = append(contents, bedrockMessageContent{
ToolUse: &toolUseBlock{
Input: params,
Name: toolCall.Function.Name,
ToolUseId: toolCall.Id,
},
})
}
result = bedrockMessage{
Role: chatMessage.Role,
Content: []bedrockMessageContent{{}},
}
params := map[string]interface{}{}
json.Unmarshal([]byte(chatMessage.ToolCalls[0].Function.Arguments), &params)
result.Content[0].ToolUse = &toolUseBlock{
Input: params,
Name: chatMessage.ToolCalls[0].Function.Name,
ToolUseId: chatMessage.ToolCalls[0].Id,
Content: contents,
}
} else if chatMessage.IsStringContent() {
result = bedrockMessage{

View File

@@ -19,6 +19,11 @@ 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."
)
type claudeProviderInitializer struct{}
@@ -319,13 +324,36 @@ 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) {
@@ -413,11 +441,30 @@ 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 {
claudeRequest.System = &claudeSystemPrompt{
StringValue: message.StringContent(),
IsArray: false,
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,
}
}
continue
}
@@ -478,6 +525,22 @@ 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,

View File

@@ -0,0 +1,317 @@
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("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 no tools are injected by default
assert.Empty(t, claudeReq.Tools)
// 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)
}
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,6 +442,9 @@ 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 {
@@ -646,6 +649,7 @@ 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

@@ -343,7 +343,7 @@ func RunAzureOnHttpRequestHeadersTests(t *testing.T) {
// 验证Path是否被正确处理
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/openai/deployments/test-deployment/chat/completions", "Path should contain Azure deployment path")
require.Equal(t, "/openai/deployments/test-deployment/chat/completions?api-version=2024-02-15-preview", pathValue, "Path should equal Azure deployment path")
// 验证Content-Length是否被删除
_, hasContentLength := test.GetHeaderValue(requestHeaders, "Content-Length")
@@ -443,8 +443,7 @@ func RunAzureOnHttpRequestBodyTests(t *testing.T) {
requestHeaders := host.GetRequestHeaders()
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/openai/deployments/test-deployment/chat/completions", "Path should contain Azure deployment path")
require.Contains(t, pathValue, "api-version=2024-02-15-preview", "Path should contain API version")
require.Equal(t, pathValue, "/openai/deployments/test-deployment/chat/completions?api-version=2024-02-15-preview", "Path should contain Azure deployment path")
})
// 测试Azure OpenAI请求体处理不同模型
@@ -577,7 +576,7 @@ func RunAzureOnHttpRequestBodyTests(t *testing.T) {
requestHeaders := host.GetRequestHeaders()
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/openai/deployments/deployment-only/chat/completions", "Path should use default deployment")
require.Equal(t, pathValue, "/openai/deployments/deployment-only/chat/completions?api-version=2024-02-15-preview", "Path should use default deployment")
})
// 测试Azure OpenAI请求体处理仅域名配置
@@ -613,7 +612,42 @@ func RunAzureOnHttpRequestBodyTests(t *testing.T) {
requestHeaders := host.GetRequestHeaders()
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/openai/deployments/gpt-3.5-turbo/chat/completions", "Path should use model from request body")
require.Equal(t, pathValue, "/openai/deployments/gpt-3.5-turbo/chat/completions?api-version=2024-02-15-preview", "Path should use model from request body")
})
// 测试Azure OpenAI模型无关请求处理仅域名配置
t.Run("azure domain only model independent", func(t *testing.T) {
host, status := test.NewTestHost(azureDomainOnlyConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/files?limit=10&purpose=assistants"},
{":method", "GET"},
})
require.Equal(t, types.HeaderStopIteration, action)
// 验证请求路径是否使用模型占位符
requestHeaders := host.GetRequestHeaders()
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Equal(t, pathValue, "/openai/files?limit=10&purpose=assistants&api-version=2024-02-15-preview", "Path should have api-version appended")
// 设置请求头
action = host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/files?"},
{":method", "GET"},
})
require.Equal(t, types.HeaderStopIteration, action)
// 验证请求路径是否使用模型占位符
requestHeaders = host.GetRequestHeaders()
pathValue, hasPath = test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Equal(t, pathValue, "/openai/files?api-version=2024-02-15-preview", "Path should have api-version appended")
})
})
}
@@ -827,10 +861,8 @@ func RunAzureBasePathHandlingTests(t *testing.T) {
require.NotContains(t, pathValue, "/azure-gpt4",
"After body stage: basePath should be removed from path")
// 在 openai 协议下,路径会被转换为 Azure 的路径格式
require.Contains(t, pathValue, "/openai/deployments/gpt-4/chat/completions",
require.Equal(t, pathValue, "/openai/deployments/gpt-4/chat/completions?api-version=2024-02-15-preview",
"Path should be transformed to Azure format")
require.Contains(t, pathValue, "api-version=2024-02-15-preview",
"Path should contain API version")
})
// 测试 basePath prepend 在 original 协议下能正常工作

View File

@@ -442,6 +442,186 @@ func RunBedrockOnHttpResponseHeadersTests(t *testing.T) {
})
}
func RunBedrockToolCallTests(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Test single tool call conversion (regression test)
t.Run("bedrock single tool call conversion", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
requestBody := `{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "What is the weather in Beijing?"},
{"role": "assistant", "content": "Let me check the weather for you.", "tool_calls": [{"id": "call_001", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Beijing\"}"}}]},
{"role": "tool", "content": "Sunny, 25°C", "tool_call_id": "call_001"}
],
"tools": [{"type": "function", "function": {"name": "get_weather", "description": "Get weather info", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
processedBody := host.GetRequestBody()
require.NotNil(t, processedBody)
var bodyMap map[string]interface{}
err := json.Unmarshal(processedBody, &bodyMap)
require.NoError(t, err)
messages := bodyMap["messages"].([]interface{})
// messages[0] = user, messages[1] = assistant with toolUse, messages[2] = user with toolResult
require.Len(t, messages, 3, "Should have 3 messages: user, assistant, user(toolResult)")
// Verify assistant message has exactly 1 toolUse
assistantMsg := messages[1].(map[string]interface{})
require.Equal(t, "assistant", assistantMsg["role"])
assistantContent := assistantMsg["content"].([]interface{})
require.Len(t, assistantContent, 1, "Assistant should have 1 content block")
toolUseBlock := assistantContent[0].(map[string]interface{})
require.Contains(t, toolUseBlock, "toolUse", "Content block should contain toolUse")
// Verify tool result message
toolResultMsg := messages[2].(map[string]interface{})
require.Equal(t, "user", toolResultMsg["role"])
toolResultContent := toolResultMsg["content"].([]interface{})
require.Len(t, toolResultContent, 1, "Tool result message should have 1 content block")
require.Contains(t, toolResultContent[0].(map[string]interface{}), "toolResult", "Content block should contain toolResult")
})
// Test multiple parallel tool calls conversion
t.Run("bedrock multiple parallel tool calls conversion", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
requestBody := `{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "What is the weather in Beijing and Shanghai?"},
{"role": "assistant", "content": "Let me check both cities.", "tool_calls": [{"id": "call_001", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Beijing\"}"}}, {"id": "call_002", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Shanghai\"}"}}]},
{"role": "tool", "content": "Sunny, 25°C", "tool_call_id": "call_001"},
{"role": "tool", "content": "Cloudy, 22°C", "tool_call_id": "call_002"}
],
"tools": [{"type": "function", "function": {"name": "get_weather", "description": "Get weather info", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
processedBody := host.GetRequestBody()
require.NotNil(t, processedBody)
var bodyMap map[string]interface{}
err := json.Unmarshal(processedBody, &bodyMap)
require.NoError(t, err)
messages := bodyMap["messages"].([]interface{})
// messages[0] = user, messages[1] = assistant with 2 toolUse, messages[2] = user with 2 toolResult
require.Len(t, messages, 3, "Should have 3 messages: user, assistant, user(toolResults merged)")
// Verify assistant message has 2 toolUse blocks
assistantMsg := messages[1].(map[string]interface{})
require.Equal(t, "assistant", assistantMsg["role"])
assistantContent := assistantMsg["content"].([]interface{})
require.Len(t, assistantContent, 2, "Assistant should have 2 content blocks for parallel tool calls")
firstToolUse := assistantContent[0].(map[string]interface{})["toolUse"].(map[string]interface{})
require.Equal(t, "get_weather", firstToolUse["name"])
require.Equal(t, "call_001", firstToolUse["toolUseId"])
secondToolUse := assistantContent[1].(map[string]interface{})["toolUse"].(map[string]interface{})
require.Equal(t, "get_weather", secondToolUse["name"])
require.Equal(t, "call_002", secondToolUse["toolUseId"])
// Verify tool results are merged into a single user message
toolResultMsg := messages[2].(map[string]interface{})
require.Equal(t, "user", toolResultMsg["role"])
toolResultContent := toolResultMsg["content"].([]interface{})
require.Len(t, toolResultContent, 2, "Tool results should be merged into 2 content blocks in one user message")
firstResult := toolResultContent[0].(map[string]interface{})["toolResult"].(map[string]interface{})
require.Equal(t, "call_001", firstResult["toolUseId"])
secondResult := toolResultContent[1].(map[string]interface{})["toolResult"].(map[string]interface{})
require.Equal(t, "call_002", secondResult["toolUseId"])
})
// Test tool call with text content mixed
t.Run("bedrock tool call with text content mixed", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
requestBody := `{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "What is the weather in Beijing?"},
{"role": "assistant", "content": "Let me check.", "tool_calls": [{"id": "call_001", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Beijing\"}"}}]},
{"role": "tool", "content": "Sunny, 25°C", "tool_call_id": "call_001"},
{"role": "assistant", "content": "The weather in Beijing is sunny with 25°C."},
{"role": "user", "content": "Thanks!"}
],
"tools": [{"type": "function", "function": {"name": "get_weather", "description": "Get weather info", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
processedBody := host.GetRequestBody()
require.NotNil(t, processedBody)
var bodyMap map[string]interface{}
err := json.Unmarshal(processedBody, &bodyMap)
require.NoError(t, err)
messages := bodyMap["messages"].([]interface{})
// messages[0] = user, messages[1] = assistant(toolUse), messages[2] = user(toolResult),
// messages[3] = assistant(text), messages[4] = user(text)
require.Len(t, messages, 5, "Should have 5 messages in mixed tool call and text scenario")
// Verify message roles alternate correctly
require.Equal(t, "user", messages[0].(map[string]interface{})["role"])
require.Equal(t, "assistant", messages[1].(map[string]interface{})["role"])
require.Equal(t, "user", messages[2].(map[string]interface{})["role"])
require.Equal(t, "assistant", messages[3].(map[string]interface{})["role"])
require.Equal(t, "user", messages[4].(map[string]interface{})["role"])
// Verify assistant text message (messages[3]) has text content
assistantTextMsg := messages[3].(map[string]interface{})
assistantTextContent := assistantTextMsg["content"].([]interface{})
require.Len(t, assistantTextContent, 1)
require.Contains(t, assistantTextContent[0].(map[string]interface{}), "text", "Text assistant message should have text content")
require.Contains(t, assistantTextContent[0].(map[string]interface{})["text"], "sunny", "Text content should contain weather info")
})
})
}
func RunBedrockOnHttpResponseBodyTests(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Test Bedrock response body processing

View File

@@ -0,0 +1,317 @@
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 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"])
})
})
}
// Note: Response headers tests are skipped as they require complex mocking
// The response header transformation is covered by integration tests

View File

@@ -67,7 +67,7 @@ func genJWTs(keySets map[string]keySet) (jwts jwts) {
Expiry: jwt.NewNumericDate(time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC)),
NotBefore: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
},
"expried": {
"expired": {
Issuer: "higress-test",
Subject: "higress-test",
Audience: []string{"foo", "bar"},

View File

@@ -8,12 +8,12 @@
{
"alg": "RS256",
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.jqzlhBPk9mmvtTT5aCYf-_5uXXSEU5bQ32fx78XeboCnjR9K1CsI4KYUIkXEX3bk66XJQUeSes7lz3gA4Yzkd-v9oADHTgpKnIxzv_5mD0_afIwEFjcalqVbSvCmro4PessQZDnmU7AIzoo3RPSqbmq8xbPVYUH9I-OO8aUu2ATd1HozgxJH1XnRU8k9KMkVW8XhvJXLKZJmnqe3Tu6pCU_tawFlBfBC4fAhMf0yX2CGE0ABAHubcdiI6JXObQmQQ9Or2a-g2a8g_Bw697PoPOsAn0YpTrHst9GcyTpkbNTAq9X8fc5EM7hiDM1FGeMYcaQTdMnOh4HBhP0p4YEhvA",
"type": "expried"
"type": "expired"
},
{
"alg": "ES256",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.9AnXd2rZ6FirHZQAoabyL4xZNz0jr-3LmcV4-pFV3JrdtUT4386Mw5Qan125fUB-rZf_ZBlv0Bft2tWY149fyg",
"type": "expried"
"type": "expired"
},
{
"alg": "ES256",

View File

@@ -268,7 +268,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "1. Default header with expried ES256",
TestCaseName: "1. Default header with expired ES256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -289,7 +289,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "2. Default header with expried RS256",
TestCaseName: "2. Default header with expired RS256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -310,7 +310,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "3. Default params with expried ES256",
TestCaseName: "3. Default params with expired ES256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -330,7 +330,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "4. Default params with expried RS256",
TestCaseName: "4. Default params with expired RS256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -350,7 +350,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "5. Custom header with expried ES256",
TestCaseName: "5. Custom header with expired ES256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -371,7 +371,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "6. Custom header with expried RS256",
TestCaseName: "6. Custom header with expired RS256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -392,7 +392,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "7. Custom params with expried ES256",
TestCaseName: "7. Custom params with expired ES256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -412,7 +412,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "8. Custom params with expried RS256",
TestCaseName: "8. Custom params with expired RS256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -432,7 +432,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "9. Custom cookies with expried ES256",
TestCaseName: "9. Custom cookies with expired ES256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -453,7 +453,7 @@ var WasmPluginsJWTAuthExpried = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "10. Custom cookies with expried RS256",
TestCaseName: "10. Custom cookies with expired RS256",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -774,7 +774,7 @@ var WasmPluginsJWTAuthSingleConsumer = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "2. Default hedaer with expried ES256 by single consumer_EC",
TestCaseName: "2. Default hedaer with expired ES256 by single consumer_EC",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
@@ -877,7 +877,7 @@ var WasmPluginsJWTAuthSingleConsumer = suite.ConformanceTest{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
TestCaseName: "7. Default params with expried ES256 by single consumer_EC",
TestCaseName: "7. Default params with expired ES256 by single consumer_EC",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{

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