Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

e2e: Enhance the e2e testing of the ai-proxy plugin based on the LLM mock server #1713

Merged
merged 12 commits into from
Feb 5, 2025
9 changes: 5 additions & 4 deletions plugins/wasm-go/extensions/ai-proxy/README.md
Original file line number Diff line number Diff line change
@@ -130,10 +130,11 @@ Azure OpenAI 所对应的 `type` 为 `azure`。它特有的配置字段如下:

通义千问所对应的 `type``qwen`。它特有的配置字段如下:

| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|--------------------|-----------------|------|-----|------------------------------------------------------------------|
| `qwenEnableSearch` | boolean | 非必填 | - | 是否启用通义千问内置的互联网搜索功能。 |
| `qwenFileIds` | array of string | 非必填 | - | 通过文件接口上传至Dashscope的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ---------------------- | --------------- | -------- | ------ | ------------------------------------------------------------ |
| `qwenEnableSearch` | boolean | 非必填 | - | 是否启用通义千问内置的互联网搜索功能。 |
| `qwenFileIds` | array of string | 非必填 | - | 通过文件接口上传至Dashscope的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
| `qwenEnableCompatible` | boolean | 非必填 | false | 开启通义千问兼容模式。启用通义千问兼容模式后,将调用千问的兼容模式接口,同时对请求/响应不做修改。 |

#### 百川智能 (Baichuan AI)

1 change: 1 addition & 0 deletions plugins/wasm-go/extensions/ai-proxy/README_EN.md
Original file line number Diff line number Diff line change
@@ -106,6 +106,7 @@ For Qwen (Tongyi Qwen), the corresponding `type` is `qwen`. Its unique configura
|--------------------|-----------------|----------------------|---------------|------------------------------------------------------------------------------------------------------------------------|
| `qwenEnableSearch` | boolean | Optional | - | Whether to enable the built-in Internet search function provided by Qwen. |
| `qwenFileIds` | array of string | Optional | - | The file IDs uploaded via the Dashscope file interface, whose content will be used as context for AI conversations. Cannot be configured with the `context` field. |
| `qwenEnableCompatible` | boolean | Optional | false | Enable Qwen compatibility mode. When Qwen compatibility mode is enabled, the compatible mode interface of Qwen will be called, and the request/response will not be modified. |

#### Baichuan AI

46 changes: 46 additions & 0 deletions test/e2e/conformance/base/llm-mock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright (c) 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: v1
kind: Namespace
metadata:
name: higress-conformance-ai-backend
labels:
higress-conformance: infra
---
apiVersion: v1
kind: Pod
metadata:
name: llm-mock
namespace: higress-conformance-ai-backend
labels:
name: llm-mock
spec:
containers:
- name: llm-mock
image: registry.cn-hangzhou.aliyuncs.com/hxt/llm-mock:latest
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: llm-mock-service
namespace: higress-conformance-ai-backend
spec:
selector:
name: llm-mock
clusterIP: None
ports:
- port: 3000
358 changes: 307 additions & 51 deletions test/e2e/conformance/tests/go-wasm-ai-proxy.go

Large diffs are not rendered by default.

185 changes: 152 additions & 33 deletions test/e2e/conformance/tests/go-wasm-ai-proxy.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
# Copyright (c) 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,74 +14,193 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: wasmplugin-ai-proxy-openai
namespace: higress-conformance-infra
name: wasmplugin-ai-proxy-baidu
namespace: higress-conformance-ai-backend
spec:
ingressClassName: higress
rules:
- host: "openai.ai.com"
- host: "qianfan.baidubce.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
name: llm-mock-service
port:
number: 8080
number: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wasmplugin-ai-proxy-doubao
namespace: higress-conformance-ai-backend
spec:
ingressClassName: higress
rules:
- host: "ark.cn-beijing.volces.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: llm-mock-service
port:
number: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wasmplugin-ai-proxy-minimax-v2-api
namespace: higress-conformance-ai-backend
spec:
ingressClassName: higress
rules:
- host: "api.minimax.chat-v2-api"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: llm-mock-service
port:
number: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wasmplugin-ai-proxy-minimax-pro-api
namespace: higress-conformance-ai-backend
spec:
ingressClassName: higress
rules:
- host: "api.minimax.chat-pro-api"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: llm-mock-service
port:
number: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wasmplugin-ai-proxy-qwen-compatible-mode
namespace: higress-conformance-ai-backend
spec:
ingressClassName: higress
rules:
- host: "dashscope.aliyuncs.com-compatible-mode"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: llm-mock-service
port:
number: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: wasmplugin-ai-proxy-qwen
namespace: higress-conformance-infra
namespace: higress-conformance-ai-backend
spec:
ingressClassName: higress
rules:
- host: "qwen.ai.com"
- host: "dashscope.aliyuncs.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
name: llm-mock-service
port:
number: 8080
number: 3000
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: ai-proxy
namespace: higress-system
spec:
priority: 200
defaultConfigDisable: true
phase: UNSPECIFIED_PHASE
priority: 100
matchRules:
- config:
provider:
type: "openai"
customSettings:
- name: "max_tokens"
value: 123
overwrite: false
- name: "temperature"
value: 0.66
overwrite: true
apiTokens:
- fake_token
modelMapping:
'gpt-3': ernie-3.5-8k
'*': ernie-3.5-8k
type: baidu
ingress:
- higress-conformance-ai-backend/wasmplugin-ai-proxy-baidu
- config:
provider:
apiTokens:
- fake_token
modelMapping:
'*': fake_doubao_endpoint
type: doubao
ingress:
- higress-conformance-ai-backend/wasmplugin-ai-proxy-doubao
- config:
provider:
apiTokens:
- fake_token
modelMapping:
'gpt-3': abab6.5s-chat
'gpt-4': abab6.5g-chat
'*': abab6.5t-chat
type: minimax
ingress:
- higress-conformance-ai-backend/wasmplugin-ai-proxy-minimax-v2-api
- config:
provider:
apiTokens:
- fake_token
modelMapping:
'gpt-3': abab6.5s-chat
'gpt-4': abab6.5g-chat
'*': abab6.5t-chat
type: minimax
minimaxApiType: pro
minimaxGroupId: 1
ingress:
- higress-conformance-ai-backend/wasmplugin-ai-proxy-minimax-pro-api
- config:
provider:
apiTokens:
- fake_token
modelMapping:
'gpt-3': qwen-turbo
'gpt-35-turbo': qwen-plus
'gpt-4-*': qwen-max
'*': qwen-turbo
type: qwen
qwenEnableCompatible: true
ingress:
- higress-conformance-infra/wasmplugin-ai-proxy-openai
- higress-conformance-ai-backend/wasmplugin-ai-proxy-qwen-compatible-mode
- config:
provider:
type: "qwen"
apiTokens: "fake-token"
customSettings:
- name: "max_tokens"
value: 123
overwrite: false
- name: "temperature"
value: 0.66
overwrite: true
apiTokens:
- fake_token
modelMapping:
'gpt-3': qwen-turbo
'gpt-35-turbo': qwen-plus
'gpt-4-*': qwen-max
'*': qwen-turbo
type: qwen
ingress:
- higress-conformance-infra/wasmplugin-ai-proxy-qwen
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0
- higress-conformance-ai-backend/wasmplugin-ai-proxy-qwen
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0
56 changes: 50 additions & 6 deletions test/e2e/conformance/utils/http/http.go
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ const (
ContentTypeFormUrlencoded = "application/x-www-form-urlencoded"
ContentTypeMultipartForm = "multipart/form-data"
ContentTypeTextPlain = "text/plain"
ContentTypeTextEventStream = "text/event-stream"
)

const (
@@ -140,11 +141,12 @@ type ExpectedRequest struct {

// Response defines expected properties of a response from a backend.
type Response struct {
StatusCode int
Headers map[string]string
Body []byte
ContentType string
AbsentHeaders []string
StatusCode int
Headers map[string]string
Body []byte
JsonBodyIgnoreFields []string
ContentType string
AbsentHeaders []string
}

// requiredConsecutiveSuccesses is the number of requests that must succeed in a row
@@ -601,6 +603,7 @@ func CompareResponse(cRes *roundtripper.CapturedResponse, expected Assertion) er

switch cTyp {
case ContentTypeTextPlain:
case ContentTypeTextEventStream:
if !bytes.Equal(expected.Response.ExpectedResponse.Body, cRes.Body) {
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Response.ExpectedResponse.Body), string(cRes.Body))
}
@@ -616,7 +619,7 @@ func CompareResponse(cRes *roundtripper.CapturedResponse, expected Assertion) er
return fmt.Errorf("failed to unmarshall CapturedResponse body %s, %s", string(cRes.Body), err.Error())
}

if !reflect.DeepEqual(eResBody, cResBody) {
if err := CompareJSONWithIgnoreFields(eResBody, cResBody, expected.Response.ExpectedResponse.JsonBodyIgnoreFields); err != nil {
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Response.ExpectedResponse.Body), string(cRes.Body))
}
case ContentTypeFormUrlencoded:
@@ -663,6 +666,47 @@ func CompareResponse(cRes *roundtripper.CapturedResponse, expected Assertion) er
}
return nil
}

// CompareJSONWithIgnoreFields compares two JSON objects, ignoring specified fields
func CompareJSONWithIgnoreFields(eResBody, cResBody map[string]interface{}, ignoreFields []string) error {
for key, eVal := range eResBody {
if contains(ignoreFields, key) {
continue
}

cVal, exists := cResBody[key]
if !exists {
return fmt.Errorf("field %s exists in expected response but not in captured response", key)
}

if !reflect.DeepEqual(eVal, cVal) {
return fmt.Errorf("field %s mismatch: expected %v, got %v", key, eVal, cVal)
}
}

// Check if captured response has extra fields (excluding ignored fields)
for key := range cResBody {
if contains(ignoreFields, key) {
continue
}

if _, exists := eResBody[key]; !exists {
return fmt.Errorf("field %s exists in captured response but not in expected response", key)
}
}

return nil
}

func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}

func ParseFormUrlencodedBody(body []byte) (map[string][]string, error) {
ret := make(map[string][]string)
kvs, err := url.ParseQuery(string(body))
2 changes: 2 additions & 0 deletions test/e2e/conformance/utils/suite/suite.go
Original file line number Diff line number Diff line change
@@ -136,6 +136,7 @@ func New(s Options) *ConformanceTestSuite {
"base/nacos.yaml",
"base/dubbo.yaml",
"base/opa.yaml",
"base/llm-mock.yaml",
}
}

@@ -173,6 +174,7 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T) {
"higress-conformance-infra",
"higress-conformance-app-backend",
"higress-conformance-web-backend",
"higress-conformance-ai-backend",
}
kubernetes.NamespacesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, namespaces)