Skip to content

Commit 6f79e07

Browse files
[CI] e2e: add Response API basic operations tests (#826)
* e2e: add Response API basic operations tests Add E2E tests for Response API basic operations: - POST /v1/responses - Create a new response - GET /v1/responses/{id} - Retrieve a response - DELETE /v1/responses/{id} - Delete a response - GET /v1/responses/{id}/input_items - List input items Signed-off-by: Jintao Zhang <[email protected]> * ci: add Response API tests to Docker Compose CI - Enable Response API in config/config.yaml - Add Response API test steps to integration-test-docker.yml: - POST /v1/responses (create) - GET /v1/responses/{id} (retrieve) - GET /v1/responses/{id}/input_items (list input items) - DELETE /v1/responses/{id} (delete and verify 404) Signed-off-by: Jintao Zhang <[email protected]> --------- Signed-off-by: Jintao Zhang <[email protected]>
1 parent 5b18a79 commit 6f79e07

File tree

6 files changed

+868
-0
lines changed

6 files changed

+868
-0
lines changed

.github/workflows/integration-test-docker.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,93 @@ jobs:
182182
echo "⚠️ Response may not contain expected fields, but request succeeded"
183183
fi
184184
185+
- name: Test Response API - Create Response
186+
run: |
187+
echo "Testing Response API: POST /v1/responses..."
188+
189+
response=$(curl -s -X POST http://localhost:8801/v1/responses \
190+
-H "Content-Type: application/json" \
191+
-d '{
192+
"model": "qwen3",
193+
"input": "What is 2 + 2?",
194+
"store": true
195+
}')
196+
197+
echo "Response: $response"
198+
199+
# Extract response ID for subsequent tests
200+
response_id=$(echo "$response" | jq -r '.id // empty')
201+
if [ -n "$response_id" ] && [[ "$response_id" == resp_* ]]; then
202+
echo "✅ Response API create test passed (id=$response_id)"
203+
echo "RESPONSE_ID=$response_id" >> $GITHUB_ENV
204+
else
205+
echo "❌ Response API create test failed - invalid or missing response ID"
206+
exit 1
207+
fi
208+
209+
- name: Test Response API - Get Response
210+
run: |
211+
echo "Testing Response API: GET /v1/responses/$RESPONSE_ID..."
212+
213+
response=$(curl -s -X GET "http://localhost:8801/v1/responses/$RESPONSE_ID" \
214+
-H "Content-Type: application/json")
215+
216+
echo "Response: $response"
217+
218+
# Verify response ID matches
219+
got_id=$(echo "$response" | jq -r '.id // empty')
220+
if [ "$got_id" = "$RESPONSE_ID" ]; then
221+
echo "✅ Response API get test passed"
222+
else
223+
echo "❌ Response API get test failed - ID mismatch (expected=$RESPONSE_ID, got=$got_id)"
224+
exit 1
225+
fi
226+
227+
- name: Test Response API - Get Input Items
228+
run: |
229+
echo "Testing Response API: GET /v1/responses/$RESPONSE_ID/input_items..."
230+
231+
response=$(curl -s -X GET "http://localhost:8801/v1/responses/$RESPONSE_ID/input_items" \
232+
-H "Content-Type: application/json")
233+
234+
echo "Response: $response"
235+
236+
# Verify it's a list
237+
object_type=$(echo "$response" | jq -r '.object // empty')
238+
if [ "$object_type" = "list" ]; then
239+
echo "✅ Response API input_items test passed"
240+
else
241+
echo "❌ Response API input_items test failed - expected object=list, got=$object_type"
242+
exit 1
243+
fi
244+
245+
- name: Test Response API - Delete Response
246+
run: |
247+
echo "Testing Response API: DELETE /v1/responses/$RESPONSE_ID..."
248+
249+
response=$(curl -s -X DELETE "http://localhost:8801/v1/responses/$RESPONSE_ID" \
250+
-H "Content-Type: application/json")
251+
252+
echo "Response: $response"
253+
254+
# Verify deletion
255+
deleted=$(echo "$response" | jq -r '.deleted // empty')
256+
if [ "$deleted" = "true" ]; then
257+
echo "✅ Response API delete test passed"
258+
else
259+
echo "❌ Response API delete test failed - expected deleted=true"
260+
exit 1
261+
fi
262+
263+
# Verify 404 on subsequent get
264+
get_response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8801/v1/responses/$RESPONSE_ID")
265+
if [ "$get_response" = "404" ]; then
266+
echo "✅ Response API delete verification passed (404 on get)"
267+
else
268+
echo "❌ Response API delete verification failed - expected 404, got $get_response"
269+
exit 1
270+
fi
271+
185272
- name: Show service logs on failure
186273
if: failure()
187274
run: |

config/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ bert_model:
33
threshold: 0.6
44
use_cpu: true
55

6+
# Response API Configuration
7+
# Enables OpenAI Response API support with conversation chaining
8+
response_api:
9+
enabled: true
10+
store_backend: "memory" # Options: "memory", "milvus", "redis"
11+
ttl_seconds: 86400 # 24 hours
12+
max_responses: 1000
13+
614
semantic_cache:
715
enabled: true
816
backend_type: "memory" # Options: "memory", "milvus", or "hybrid"

e2e/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The framework follows a **separation of concerns** design:
1818
- **istio**: Tests Semantic Router with Istio service mesh integration
1919
- **production-stack**: Tests vLLM Production Stack configurations
2020
- **llm-d**: Tests Semantic Router with LLM-D distributed inference
21+
- **response-api**: Tests Response API endpoints (POST/GET/DELETE /v1/responses)
2122
- **dynamo**: Tests with Nvidia Dynamo (future)
2223

2324
## Directory Structure
@@ -82,6 +83,15 @@ The framework includes the following test cases (all in `e2e/testcases/`):
8283
| `pii-detection` | PII detection and blocking | 10 PII types, detection rate, block rate |
8384
| `jailbreak-detection` | Jailbreak attack detection | 10 attack types, detection rate, block rate |
8485

86+
### Response API Tests
87+
88+
| Test Case | Description | Metrics |
89+
|-----------|-------------|---------|
90+
| `response-api-create` | POST /v1/responses - Create a new response | Response ID validation, status check |
91+
| `response-api-get` | GET /v1/responses/{id} - Retrieve a response | Response retrieval, ID matching |
92+
| `response-api-delete` | DELETE /v1/responses/{id} - Delete a response | Deletion confirmation, 404 verification |
93+
| `response-api-input-items` | GET /v1/responses/{id}/input_items - List input items | Input items list, pagination |
94+
8595
### Signal-Decision Engine Tests
8696

8797
| Test Case | Description | Metrics |
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package responseapi
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"k8s.io/client-go/kubernetes"
9+
"k8s.io/client-go/tools/clientcmd"
10+
11+
"github.com/vllm-project/semantic-router/e2e/pkg/framework"
12+
"github.com/vllm-project/semantic-router/e2e/pkg/helm"
13+
"github.com/vllm-project/semantic-router/e2e/pkg/helpers"
14+
15+
// Import testcases package to register all test cases via their init() functions
16+
_ "github.com/vllm-project/semantic-router/e2e/testcases"
17+
)
18+
19+
// Profile implements the Response API test profile
20+
type Profile struct {
21+
verbose bool
22+
kubeConfig string
23+
}
24+
25+
// NewProfile creates a new Response API profile
26+
func NewProfile() *Profile {
27+
return &Profile{}
28+
}
29+
30+
// Name returns the profile name
31+
func (p *Profile) Name() string {
32+
return "response-api"
33+
}
34+
35+
// Description returns the profile description
36+
func (p *Profile) Description() string {
37+
return "Tests Response API endpoints (POST/GET/DELETE /v1/responses)"
38+
}
39+
40+
// Setup deploys all required components for Response API testing
41+
func (p *Profile) Setup(ctx context.Context, opts *framework.SetupOptions) error {
42+
p.verbose = opts.Verbose
43+
p.kubeConfig = opts.KubeConfig
44+
p.log("Setting up Response API test environment")
45+
46+
deployer := helm.NewDeployer(opts.KubeConfig, opts.Verbose)
47+
48+
// Step 1: Deploy Semantic Router with Response API enabled
49+
p.log("Step 1/3: Deploying Semantic Router with Response API")
50+
if err := p.deploySemanticRouter(ctx, deployer, opts); err != nil {
51+
return fmt.Errorf("failed to deploy semantic router: %w", err)
52+
}
53+
54+
// Step 2: Deploy Envoy Gateway
55+
p.log("Step 2/3: Deploying Envoy Gateway")
56+
if err := p.deployEnvoyGateway(ctx, deployer); err != nil {
57+
return fmt.Errorf("failed to deploy envoy gateway: %w", err)
58+
}
59+
60+
// Step 3: Verify all components are ready
61+
p.log("Step 3/3: Verifying all components are ready")
62+
if err := p.verifyEnvironment(ctx, opts); err != nil {
63+
return fmt.Errorf("failed to verify environment: %w", err)
64+
}
65+
66+
p.log("Response API test environment setup complete")
67+
return nil
68+
}
69+
70+
// Teardown cleans up all deployed resources
71+
func (p *Profile) Teardown(ctx context.Context, opts *framework.TeardownOptions) error {
72+
p.verbose = opts.Verbose
73+
p.log("Tearing down Response API test environment")
74+
75+
deployer := helm.NewDeployer(opts.KubeConfig, opts.Verbose)
76+
77+
p.log("Uninstalling Envoy Gateway")
78+
_ = deployer.Uninstall(ctx, "eg", "envoy-gateway-system")
79+
80+
p.log("Uninstalling Semantic Router")
81+
_ = deployer.Uninstall(ctx, "semantic-router", "vllm-semantic-router-system")
82+
83+
p.log("Response API test environment teardown complete")
84+
return nil
85+
}
86+
87+
// GetTestCases returns the list of test cases for this profile
88+
func (p *Profile) GetTestCases() []string {
89+
return []string{
90+
// Response API basic operations
91+
"response-api-create",
92+
"response-api-get",
93+
"response-api-delete",
94+
"response-api-input-items",
95+
}
96+
}
97+
98+
// GetServiceConfig returns the service configuration for accessing the deployed service
99+
func (p *Profile) GetServiceConfig() framework.ServiceConfig {
100+
return framework.ServiceConfig{
101+
LabelSelector: "gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=semantic-router",
102+
Namespace: "envoy-gateway-system",
103+
PortMapping: "8080:80",
104+
}
105+
}
106+
107+
func (p *Profile) deploySemanticRouter(ctx context.Context, deployer *helm.Deployer, opts *framework.SetupOptions) error {
108+
imageTag := opts.ImageTag
109+
if imageTag == "" {
110+
imageTag = "latest"
111+
}
112+
113+
return deployer.Install(ctx, helm.InstallOptions{
114+
ReleaseName: "semantic-router",
115+
Chart: "deploy/helm/semantic-router",
116+
Namespace: "vllm-semantic-router-system",
117+
ValuesFiles: []string{"e2e/profiles/response-api/values.yaml"},
118+
Set: map[string]string{
119+
"image.repository": "ghcr.io/vllm-project/semantic-router/extproc",
120+
"image.tag": imageTag,
121+
},
122+
Wait: true,
123+
Timeout: "300s",
124+
})
125+
}
126+
127+
func (p *Profile) deployEnvoyGateway(ctx context.Context, deployer *helm.Deployer) error {
128+
return deployer.Install(ctx, helm.InstallOptions{
129+
ReleaseName: "eg",
130+
Chart: "oci://docker.io/envoyproxy/gateway-helm",
131+
Namespace: "envoy-gateway-system",
132+
Wait: true,
133+
Timeout: "300s",
134+
})
135+
}
136+
137+
func (p *Profile) verifyEnvironment(ctx context.Context, opts *framework.SetupOptions) error {
138+
config, err := clientcmd.BuildConfigFromFlags("", opts.KubeConfig)
139+
if err != nil {
140+
return fmt.Errorf("failed to build kubeconfig: %w", err)
141+
}
142+
143+
client, err := kubernetes.NewForConfig(config)
144+
if err != nil {
145+
return fmt.Errorf("failed to create kubernetes client: %w", err)
146+
}
147+
148+
// Wait for semantic router deployment
149+
p.log("Waiting for Semantic Router deployment...")
150+
if err := p.waitForDeployment(ctx, client, "vllm-semantic-router-system", "semantic-router"); err != nil {
151+
return fmt.Errorf("semantic router deployment not ready: %w", err)
152+
}
153+
154+
p.log("All components are ready")
155+
return nil
156+
}
157+
158+
func (p *Profile) waitForDeployment(ctx context.Context, client *kubernetes.Clientset, namespace, name string) error {
159+
timeout := 5 * time.Minute
160+
interval := 5 * time.Second
161+
deadline := time.Now().Add(timeout)
162+
163+
for time.Now().Before(deadline) {
164+
if err := helpers.CheckDeployment(ctx, client, namespace, name, p.verbose); err == nil {
165+
return nil
166+
}
167+
time.Sleep(interval)
168+
}
169+
170+
return fmt.Errorf("timeout waiting for deployment %s/%s", namespace, name)
171+
}
172+
173+
func (p *Profile) log(msg string) {
174+
if p.verbose {
175+
fmt.Printf("[response-api] %s\n", msg)
176+
}
177+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Response API E2E Test Profile Values
2+
# This configuration enables Response API for testing
3+
4+
replicaCount: 1
5+
6+
image:
7+
repository: ghcr.io/vllm-project/semantic-router/extproc
8+
tag: latest
9+
pullPolicy: IfNotPresent
10+
11+
# Response API Configuration
12+
responseApi:
13+
enabled: true
14+
storeBackend: "memory"
15+
ttlSeconds: 86400
16+
maxResponses: 1000
17+
18+
# Semantic Cache (required for some tests)
19+
semanticCache:
20+
enabled: true
21+
backendType: "memory"
22+
similarityThreshold: 0.8
23+
maxEntries: 1000
24+
ttlSeconds: 3600
25+
26+
# vLLM Endpoints - use mock backend for testing
27+
vllmEndpoints:
28+
- name: "test-endpoint"
29+
address: "mock-vllm"
30+
port: 8000
31+
weight: 1
32+
33+
# Model configuration
34+
modelConfig:
35+
"MoM":
36+
useReasoning: false
37+
preferredEndpoints: ["test-endpoint"]
38+
39+
# Minimal classifier configuration
40+
classifier:
41+
categoryModel:
42+
modelId: "models/all-MiniLM-L12-v2"
43+
threshold: 0.6
44+
useCpu: true
45+
46+
# Categories
47+
categories:
48+
- name: other
49+
description: "General knowledge and miscellaneous topics"
50+
51+
# Strategy
52+
strategy: "priority"
53+
54+
# Decisions
55+
decisions:
56+
- name: "default_decision"
57+
description: "Default catch-all decision"
58+
priority: 1
59+
rules:
60+
operator: "OR"
61+
conditions:
62+
- type: "domain"
63+
name: "other"
64+
modelRefs:
65+
- model: "MoM"
66+
useReasoning: false
67+
68+
defaultModel: "MoM"
69+
70+
# Service configuration
71+
service:
72+
type: ClusterIP
73+
port: 8080
74+
75+
# Resources
76+
resources:
77+
limits:
78+
cpu: 500m
79+
memory: 512Mi
80+
requests:
81+
cpu: 100m
82+
memory: 128Mi

0 commit comments

Comments
 (0)