diff --git a/test/e2e/conformance/tests/go-wasm-ai-proxy.go b/test/e2e/conformance/tests/go-wasm-ai-proxy.go index ebd590f12a..6383f75f92 100644 --- a/test/e2e/conformance/tests/go-wasm-ai-proxy.go +++ b/test/e2e/conformance/tests/go-wasm-ai-proxy.go @@ -277,6 +277,29 @@ data: [DONE] }, }, }, + { + Meta: http.AssertionMeta{ + TestCaseName: "qwen case 3: non-streaming request", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "dashscope.aliyuncs.com", + Path: "/v1/chat/completions", + Method: "POST", + ContentType: http.ContentTypeApplicationJson, + Body: []byte(`{"model":"gpt-3","messages":[{"role":"user","content":"你好,你是谁?"}],"stream":false}`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + ContentType: http.ContentTypeApplicationJson, + JsonBodyIgnoreFields: []string{"created"}, + Body: []byte(`{"id":"chatcmpl-llm-mock","choices":[{"index":0,"message":{"role":"assistant","content":"你好,你是谁?"},"finish_reason":"stop"}],"created":1738218357,"model":"qwen-turbo","object":"chat.completion","usage":{"prompt_tokens":9,"completion_tokens":1,"total_tokens":10}}`), + }, + }, + }, } t.Run("WasmPlugins ai-proxy", func(t *testing.T) { for _, testcase := range testcases { diff --git a/test/e2e/conformance/tests/go-wasm-ai-proxy.yaml b/test/e2e/conformance/tests/go-wasm-ai-proxy.yaml index 74e42d954c..5cdf1ac0c2 100644 --- a/test/e2e/conformance/tests/go-wasm-ai-proxy.yaml +++ b/test/e2e/conformance/tests/go-wasm-ai-proxy.yaml @@ -87,6 +87,25 @@ spec: port: number: 3000 --- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wasmplugin-ai-proxy-qwen + namespace: higress-conformance-ai-backend +spec: + ingressClassName: higress + rules: + - host: "dashscope.aliyuncs.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: llm-mock-service + port: + number: 3000 +--- apiVersion: extensions.higress.io/v1alpha1 kind: WasmPlugin metadata: @@ -143,4 +162,16 @@ spec: qwenEnableCompatible: true ingress: - higress-conformance-ai-backend/wasmplugin-ai-proxy-qwen-compatible-mode - url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0 + - config: + provider: + apiTokens: + - fake_token + modelMapping: + 'gpt-3': qwen-turbo + 'gpt-35-turbo': qwen-plus + 'gpt-4-*': qwen-max + '*': qwen-turbo + type: qwen + ingress: + - higress-conformance-ai-backend/wasmplugin-ai-proxy-qwen + url: file:///opt/plugins/wasm-go/extensions/ai-proxy/plugin.wasm diff --git a/test/e2e/conformance/utils/http/http.go b/test/e2e/conformance/utils/http/http.go index dcf9166fd9..a48cb1c767 100644 --- a/test/e2e/conformance/utils/http/http.go +++ b/test/e2e/conformance/utils/http/http.go @@ -141,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 @@ -618,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: @@ -665,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))