[router] Add get and cancel method for response api (#10387)
This commit is contained in:
@@ -994,6 +994,7 @@ mod router_policy_tests {
|
||||
#[cfg(test)]
|
||||
mod responses_endpoint_tests {
|
||||
use super::*;
|
||||
use reqwest::Client as HttpClient;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v1_responses_non_streaming() {
|
||||
@@ -1074,6 +1075,207 @@ mod responses_endpoint_tests {
|
||||
// We don't fully consume the stream in this test harness.
|
||||
ctx.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v1_responses_get() {
|
||||
let ctx = TestContext::new(vec![MockWorkerConfig {
|
||||
port: 18952,
|
||||
worker_type: WorkerType::Regular,
|
||||
health_status: HealthStatus::Healthy,
|
||||
response_delay_ms: 0,
|
||||
fail_rate: 0.0,
|
||||
}])
|
||||
.await;
|
||||
|
||||
let app = ctx.create_app().await;
|
||||
|
||||
// First create a response to obtain an id
|
||||
let payload = json!({
|
||||
"input": "Hello Responses API",
|
||||
"model": "mock-model",
|
||||
"stream": false
|
||||
});
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/responses")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(serde_json::to_string(&payload).unwrap()))
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let resp_id = body_json["id"].as_str().unwrap().to_string();
|
||||
|
||||
// Retrieve the response
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("/v1/responses/{}", resp_id))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let get_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(get_json["object"], "response");
|
||||
|
||||
ctx.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v1_responses_cancel() {
|
||||
let ctx = TestContext::new(vec![MockWorkerConfig {
|
||||
port: 18953,
|
||||
worker_type: WorkerType::Regular,
|
||||
health_status: HealthStatus::Healthy,
|
||||
response_delay_ms: 0,
|
||||
fail_rate: 0.0,
|
||||
}])
|
||||
.await;
|
||||
|
||||
let app = ctx.create_app().await;
|
||||
|
||||
// First create a response to obtain an id
|
||||
let payload = json!({
|
||||
"input": "Hello Responses API",
|
||||
"model": "mock-model",
|
||||
"stream": false
|
||||
});
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/responses")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(serde_json::to_string(&payload).unwrap()))
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let resp_id = body_json["id"].as_str().unwrap().to_string();
|
||||
|
||||
// Cancel the response
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(format!("/v1/responses/{}/cancel", resp_id))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let cancel_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(cancel_json["status"], "cancelled");
|
||||
|
||||
ctx.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v1_responses_delete_and_list_not_implemented() {
|
||||
let ctx = TestContext::new(vec![MockWorkerConfig {
|
||||
port: 18954,
|
||||
worker_type: WorkerType::Regular,
|
||||
health_status: HealthStatus::Healthy,
|
||||
response_delay_ms: 0,
|
||||
fail_rate: 0.0,
|
||||
}])
|
||||
.await;
|
||||
|
||||
let app = ctx.create_app().await;
|
||||
|
||||
// Use an arbitrary id for delete/list
|
||||
let resp_id = "resp-test-123";
|
||||
|
||||
let req = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(format!("/v1/responses/{}", resp_id))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_IMPLEMENTED);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("/v1/responses/{}/input", resp_id))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_IMPLEMENTED);
|
||||
|
||||
ctx.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v1_responses_get_multi_worker_fanout() {
|
||||
// Start two mock workers
|
||||
let ctx = TestContext::new(vec![
|
||||
MockWorkerConfig {
|
||||
port: 18960,
|
||||
worker_type: WorkerType::Regular,
|
||||
health_status: HealthStatus::Healthy,
|
||||
response_delay_ms: 0,
|
||||
fail_rate: 0.0,
|
||||
},
|
||||
MockWorkerConfig {
|
||||
port: 18961,
|
||||
worker_type: WorkerType::Regular,
|
||||
health_status: HealthStatus::Healthy,
|
||||
response_delay_ms: 0,
|
||||
fail_rate: 0.0,
|
||||
},
|
||||
])
|
||||
.await;
|
||||
|
||||
let app = ctx.create_app().await;
|
||||
|
||||
// Create a background response with a known id
|
||||
let rid = format!("resp_{}", 18960); // arbitrary unique id
|
||||
let payload = json!({
|
||||
"input": "Hello Responses API",
|
||||
"model": "mock-model",
|
||||
"background": true,
|
||||
"store": true,
|
||||
"request_id": rid,
|
||||
});
|
||||
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/responses")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(serde_json::to_string(&payload).unwrap()))
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// Using the router, GET should succeed by fanning out across workers
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("/v1/responses/{}", rid))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// Validate only one worker holds the metadata: direct calls
|
||||
let client = HttpClient::new();
|
||||
let mut ok_count = 0usize;
|
||||
for url in ctx.router.get_worker_urls() {
|
||||
let get_url = format!("{}/v1/responses/{}", url, rid);
|
||||
let res = client.get(get_url).send().await.unwrap();
|
||||
if res.status() == StatusCode::OK {
|
||||
ok_count += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(ok_count, 1, "exactly one worker should store the response");
|
||||
|
||||
ctx.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user