use super::*;
use crate::cancel::CancellationToken;
use crate::http::{build_request, handle_error_response};
use crate::types::Message as Msg;
use crate::types::{ThinkingConfig, ThinkingType};
use futures::StreamExt;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn mock_responses_json(text: &str, model: &str) -> Value {
serde_json::json!({
"id": "resp_abc123",
"object": "response",
"status": "completed",
"created": 1700000000,
"model": model,
"system_fingerprint": "fp_mock",
"output": [{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": text}
]
}],
"usage": {
"input_tokens": 10,
"output_tokens": 5
}
})
}
fn make_adapter() -> OpenAiResponsesAdapter {
OpenAiResponsesAdapter::new()
}
#[test]
fn test_build_request_body_basic() {
let adapter = make_adapter();
let req = CompletionRequest::new(
"test-model",
vec![Msg::system("You are helpful."), Msg::user("Hello")],
);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["model"], "test-model");
assert_eq!(body["input"][0]["role"], "user");
assert_eq!(body["input"][0]["content"], "Hello");
assert_eq!(body["instructions"], "You are helpful.");
assert!(body.get("stream").is_none());
}
#[test]
fn test_build_request_body_default_model() {
let adapter = make_adapter();
let req = CompletionRequest {
model: None,
..CompletionRequest::new("ignored", vec![Msg::user("Hi")])
};
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["model"], "gpt-4o");
}
#[test]
fn test_build_request_body_stream() {
let adapter = make_adapter();
let req = CompletionRequest::new("test-model", vec![Msg::user("Hello")]);
let body = adapter.build_request_body(&req, true).unwrap();
assert_eq!(body["stream"], true);
}
#[test]
fn test_build_request_body_with_temperature() {
let adapter = make_adapter();
let mut req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
req.temperature = Some(0.7);
let body = adapter.build_request_body(&req, false).unwrap();
let temp = body["temperature"].as_f64().unwrap();
assert!((temp - 0.7).abs() < 1e-6);
}
#[test]
fn test_build_request_body_with_tools() {
let adapter = make_adapter();
let mut req = CompletionRequest::new("test-model", vec![Msg::user("Weather?")]);
req.tools = Some(vec![ToolDefinition {
name: "get_weather".into(),
description: "Get the weather".into(),
parameters: serde_json::json!({"type": "object"}),
strict: None,
}]);
let body = adapter.build_request_body(&req, false).unwrap();
assert!(body.get("tools").is_some());
assert_eq!(body["tools"][0]["type"], "function");
assert_eq!(body["tools"][0]["function"]["name"], "get_weather");
}
#[test]
fn test_build_request_body_with_thinking() {
let adapter = make_adapter();
let mut req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
req.thinking = Some(ThinkingConfig {
thinking_type: ThinkingType::Enabled { budget_tokens: Some(4096) },
display: None,
});
let body = adapter.build_request_body(&req, false).unwrap();
assert!(body.get("thinking").is_some());
assert_eq!(body["thinking"]["type"], "enabled");
assert_eq!(body["thinking"]["budget_tokens"], 4096);
}
#[test]
fn test_build_request_body_with_user() {
let adapter = make_adapter();
let mut req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
req.user = Some("user-abc".into());
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["user"], "user-abc");
}
#[test]
fn test_build_request_body_with_max_tokens() {
let adapter = make_adapter();
let mut req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
req.max_tokens = Some(100);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["max_tokens"], 100);
}
#[test]
fn test_build_auth_headers_bearer() {
let adapter = make_adapter();
let headers = adapter.build_auth_headers(&AuthMethod::Bearer { token: "sk-test".into() });
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "Authorization");
assert_eq!(headers[0].1, "Bearer sk-test");
}
#[test]
fn test_build_auth_headers_none() {
let adapter = make_adapter();
let headers = adapter.build_auth_headers(&AuthMethod::None);
assert!(headers.is_empty());
}
#[test]
fn test_build_auth_headers_api_key() {
let adapter = make_adapter();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".into(),
key: "my-key".into(),
});
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "x-api-key");
assert_eq!(headers[0].1, "my-key");
}
#[test]
fn test_parse_response_success() {
let adapter = make_adapter();
let json = mock_responses_json("Hello!", "test-model");
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.content.as_deref(), Some("Hello!"));
assert_eq!(resp.model, "test-model");
assert_eq!(resp.finish_reason, FinishReason::Stop);
assert_eq!(resp.usage.prompt_tokens, 10);
assert_eq!(resp.usage.completion_tokens, 5);
assert_eq!(resp.usage.total_tokens, 15);
assert!(resp.tool_calls.is_empty());
assert_eq!(resp.id.as_deref(), Some("resp_abc123"));
assert_eq!(resp.created, Some(1700000000));
assert_eq!(resp.system_fingerprint.as_deref(), Some("fp_mock"));
assert!(resp.refusal.is_none());
assert!(resp.thinking.is_none());
}
#[test]
fn test_parse_response_multiple_content_parts() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_456",
"status": "completed",
"output": [{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "Hello"},
{"type": "output_text", "text": "World"}
]
}],
"usage": { "input_tokens": 5, "output_tokens": 2 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.content.as_deref(), Some("Hello\nWorld"));
}
#[test]
fn test_parse_response_no_usage() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_789",
"status": "completed",
"output": [{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "No usage"}]
}]
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.usage.prompt_tokens, 0);
assert_eq!(resp.usage.completion_tokens, 0);
}
#[test]
fn test_parse_response_no_model() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_nm",
"status": "completed",
"output": [{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi"}]
}],
"usage": { "input_tokens": 1, "output_tokens": 1 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.model, "unknown");
}
#[test]
fn test_parse_response_with_tool_calls() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_tc",
"status": "completed",
"model": "test-model",
"output": [
{
"type": "function_call",
"id": "call_1",
"name": "get_weather",
"arguments": "{\"city\": \"NYC\"}",
"status": "completed"
}
],
"usage": { "input_tokens": 10, "output_tokens": 5 }
});
let resp = adapter.parse_response(&json).unwrap();
assert!(resp.content.is_none());
assert_eq!(resp.tool_calls.len(), 1);
assert_eq!(resp.tool_calls[0].id, "call_1");
assert_eq!(resp.tool_calls[0].function_name, "get_weather");
assert_eq!(resp.tool_calls[0].arguments["city"], "NYC");
assert_eq!(resp.finish_reason, FinishReason::Stop);
}
#[test]
fn test_parse_response_incomplete_max_tokens() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_inc",
"model": "test-model",
"status": "incomplete",
"incomplete_details": { "reason": "max_output_tokens" },
"output": [{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "truncated"}]
}],
"usage": { "input_tokens": 10, "output_tokens": 100 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.finish_reason, FinishReason::MaxTokens);
}
#[test]
fn test_parse_response_incomplete_content_filter() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_cf",
"model": "test-model",
"status": "incomplete",
"incomplete_details": { "reason": "content_filter" },
"output": [],
"usage": { "input_tokens": 5, "output_tokens": 0 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.finish_reason, FinishReason::ContentFilter);
}
#[test]
fn test_parse_response_with_refusal_content() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_ref",
"created": 1700000001,
"status": "completed",
"system_fingerprint": "fp_ref",
"model": "test-model",
"output": [{
"type": "message",
"role": "assistant",
"content": [
{"type": "refusal", "text": "I cannot answer that."}
]
}],
"usage": { "input_tokens": 1, "output_tokens": 1 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.refusal.as_deref(), Some("I cannot answer that."));
assert_eq!(resp.finish_reason, FinishReason::Refusal);
assert!(resp.content.is_none());
assert_eq!(resp.id.as_deref(), Some("resp_ref"));
assert_eq!(resp.created, Some(1700000001));
assert_eq!(resp.system_fingerprint.as_deref(), Some("fp_ref"));
}
#[test]
fn test_parse_response_with_reasoning() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_reas",
"created": 1700000002,
"status": "completed",
"model": "test-model",
"output": [{
"type": "message",
"role": "assistant",
"content": [
{"type": "reasoning", "text": "Let me think step by step..."},
{"type": "output_text", "text": "The answer is 42."}
]
}],
"usage": { "input_tokens": 5, "output_tokens": 3 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.thinking.as_deref(), Some("Let me think step by step..."));
assert_eq!(resp.content.as_deref(), Some("The answer is 42."));
assert_eq!(resp.finish_reason, FinishReason::Stop);
}
#[test]
fn test_parse_response_incomplete_refusal_reason() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_inc_ref",
"model": "test-model",
"status": "incomplete",
"incomplete_details": { "reason": "refusal" },
"output": [],
"usage": { "input_tokens": 5, "output_tokens": 0 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.finish_reason, FinishReason::Refusal);
}
#[test]
fn test_parse_response_pause_turn_status() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_pt",
"model": "test-model",
"status": "pause_turn",
"output": [{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "awaiting user..."}]
}],
"usage": { "input_tokens": 3, "output_tokens": 2 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.finish_reason, FinishReason::PauseTurn);
}
#[test]
fn test_parse_response_unknown_finish_reason_defaults_to_stop() {
let adapter = make_adapter();
let json = serde_json::json!({
"id": "resp_unk",
"model": "test-model",
"status": "weird_status",
"output": [{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "hmm"}]
}],
"usage": { "input_tokens": 1, "output_tokens": 1 }
});
let resp = adapter.parse_response(&json).unwrap();
assert_eq!(resp.finish_reason, FinishReason::Stop);
}
#[test]
fn test_parse_sse_event_content_delta() {
let adapter = make_adapter();
let data = r#"{"type":"response.output_text.delta","delta":"Hello","item_id":"i1"}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(event, Some(StreamEvent::ContentDelta { delta: "Hello".into() }));
}
#[test]
fn test_parse_sse_event_content_delta_empty_returns_none() {
let adapter = make_adapter();
let data = r#"{"type":"response.output_text.delta","delta":""}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(event, None);
}
#[test]
fn test_parse_sse_event_done_with_usage() {
let adapter = make_adapter();
let data = r#"{"type":"response.done","response":{"id":"resp_1","status":"completed","usage":{"input_tokens":10,"output_tokens":5}}}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(
event,
Some(StreamEvent::Done {
finish_reason: FinishReason::Stop,
usage: Some(TokenUsage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
cached_tokens: None,
..Default::default()
}),
})
);
}
#[test]
fn test_parse_sse_event_done_no_usage() {
let adapter = make_adapter();
let data = r#"{"type":"response.done","response":{"id":"resp_1","status":"completed"}}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(
event,
Some(StreamEvent::Done { finish_reason: FinishReason::Stop, usage: None })
);
}
#[test]
fn test_parse_sse_event_done_incomplete_max_tokens() {
let adapter = make_adapter();
let data = r#"{"type":"response.done","response":{"id":"resp_2","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"usage":{"input_tokens":5,"output_tokens":100}}}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(
event,
Some(StreamEvent::Done {
finish_reason: FinishReason::MaxTokens,
usage: Some(TokenUsage {
prompt_tokens: 5,
completion_tokens: 100,
total_tokens: 105,
cached_tokens: None,
..Default::default()
}),
})
);
}
#[test]
fn test_parse_sse_event_done_sentinel() {
let adapter = make_adapter();
let event = adapter.parse_sse_event("[DONE]").unwrap();
assert!(event.is_none(), "expected [DONE] to be ignored (None)");
}
#[test]
fn test_parse_sse_event_unknown_type_returns_none() {
let adapter = make_adapter();
let data = r#"{"type":"response.unknown_event","data":"something"}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(event, None);
}
#[test]
fn test_parse_sse_event_invalid_json_returns_error() {
let adapter = make_adapter();
let result = adapter.parse_sse_event("not json");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ProviderError::Format(_)));
}
#[test]
fn test_parse_sse_event_tool_call_delta() {
let adapter = make_adapter();
let data = r#"{"type":"response.function_call_arguments.delta","item_id":"fc_0","delta":"{\"city\":\"NYC\"}"}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(
event,
Some(StreamEvent::ToolCallDelta {
index: 0,
id: None,
function_name: None,
arguments_delta: "{\"city\":\"NYC\"}".into(),
})
);
}
#[test]
fn test_protocol_name() {
assert_eq!(make_adapter().protocol_name(), "openai_responses");
}
#[test]
fn test_endpoint_path() {
assert_eq!(make_adapter().endpoint_path(), "/responses");
}
#[tokio::test]
async fn test_complete_non_streaming() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(mock_responses_json("Hello world!", "test-model")),
)
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req =
CompletionRequest::new("test-model", vec![Msg::system("Be helpful."), Msg::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let result = adapter.parse_response(&response_json).unwrap();
assert_eq!(result.content.as_deref(), Some("Hello world!"));
assert_eq!(result.model, "test-model");
assert_eq!(result.finish_reason, FinishReason::Stop);
assert_eq!(result.usage.prompt_tokens, 10);
assert_eq!(result.usage.completion_tokens, 5);
}
#[tokio::test]
async fn test_complete_401_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(ResponseTemplate::new(401).set_body_string("Invalid API key"))
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "bad-key".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
assert_eq!(resp.status().as_u16(), 401);
let status = resp.status().as_u16();
let error_body = resp.text().await.unwrap_or_default();
let err = handle_error_response(status, &reqwest::header::HeaderMap::new(), &error_body);
assert!(matches!(err, ProviderError::Auth(_)));
}
#[tokio::test]
async fn test_complete_429_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(
ResponseTemplate::new(429)
.set_body_string("Rate limited")
.insert_header("Retry-After", "30"),
)
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
assert_eq!(resp.status().as_u16(), 429);
let status = resp.status().as_u16();
let headers_map = resp.headers().clone();
let error_body = resp.text().await.unwrap_or_default();
let err = handle_error_response(status, &headers_map, &error_body);
match err {
ProviderError::RateLimit { retry_after_ms } => {
assert_eq!(retry_after_ms, 30_000);
}
other => panic!("Expected RateLimit, got {:?}", other),
}
}
#[tokio::test]
async fn test_complete_500_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
assert_eq!(resp.status().as_u16(), 500);
let status = resp.status().as_u16();
let error_body = resp.text().await.unwrap_or_default();
let err = handle_error_response(status, &reqwest::header::HeaderMap::new(), &error_body);
match err {
ProviderError::Internal { status: s, message } => {
assert_eq!(s, 500);
assert!(message.contains("Internal Server Error"));
}
other => panic!("Expected Internal, got {:?}", other),
}
}
#[tokio::test]
async fn test_stream_responses_basic() {
let mock_server = MockServer::start().await;
let sse_body = "\
event: response.output_text.delta\n\
data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello\"}\n\
\n\
event: response.output_text.delta\n\
data: {\"type\":\"response.output_text.delta\",\"delta\":\" world\"}\n\
\n\
event: response.done\n\
data: {\"type\":\"response.done\",\"response\":{\"id\":\"resp_1\",\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\
\n\
data: [DONE]\n\n";
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(ResponseTemplate::new(200).set_body_string(sse_body.to_owned()))
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
let body = adapter.build_request_body(&req, true).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
assert!(resp.status().is_success());
let stream = crate::http::create_sse_stream(resp, None);
let lines: Vec<String> =
stream.filter_map(|r| futures::future::ready(r.ok())).collect().await;
let mut events: Vec<StreamEvent> = Vec::new();
for line in &lines {
if line.is_empty() {
continue;
}
if line.strip_prefix("event: ").is_some() {
continue;
}
if let Some(data) = line.strip_prefix("data: ") {
if let Ok(Some(event)) = adapter.parse_sse_event(data) {
events.push(event);
}
continue;
}
}
assert_eq!(events.len(), 3, "expected 3 events: 2 deltas + response.done, got {events:?}");
assert_eq!(events[0], StreamEvent::ContentDelta { delta: "Hello".into() });
assert_eq!(events[1], StreamEvent::ContentDelta { delta: " world".into() });
assert_eq!(
events[2],
StreamEvent::Done {
finish_reason: FinishReason::Stop,
usage: Some(TokenUsage {
prompt_tokens: 5,
completion_tokens: 2,
total_tokens: 7,
cached_tokens: None,
..Default::default()
}),
}
);
}
#[tokio::test]
async fn test_stream_exactly_one_done_event() {
let mock_server = MockServer::start().await;
let sse_body = "\
event: response.done\n\
data: {\"type\":\"response.done\",\"response\":{\"id\":\"resp_1\",\"status\":\"completed\"}}\n\
\n\
data: [DONE]\n\n";
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(ResponseTemplate::new(200).set_body_string(sse_body.to_owned()))
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("done only")]);
let body = adapter.build_request_body(&req, true).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
let stream = crate::http::create_sse_stream(resp, None);
let lines: Vec<String> =
stream.filter_map(|r| futures::future::ready(r.ok())).collect().await;
let mut done_count = 0;
for line in &lines {
if line.strip_prefix("event: ").is_some() {
continue;
}
if let Some(data) = line.strip_prefix("data: ") {
if let Ok(Some(event)) = adapter.parse_sse_event(data) {
if matches!(event, StreamEvent::Done { .. }) {
done_count += 1;
}
}
}
}
assert_eq!(done_count, 1, "expected exactly 1 Done event from response.done only");
}
#[tokio::test]
async fn test_stream_cancellation() {
let mock_server = MockServer::start().await;
let sse_body = "\
event: response.output_text.delta\n\
data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello\"}\n\
\n\
data: [DONE]\n\n";
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(ResponseTemplate::new(200).set_body_string(sse_body.to_owned()))
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
let body = adapter.build_request_body(&req, true).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let resp = build_request(&reqwest::Client::new(), &url, body, headers, None).await.unwrap();
let cancel = CancellationToken::new();
cancel.cancel();
let stream = crate::http::create_sse_stream(resp, Some(cancel));
let lines: Vec<String> =
stream.filter_map(|r| futures::future::ready(r.ok())).collect().await;
assert!(
lines.len() < 5,
"expected very few lines with immediate cancellation, got {}",
lines.len()
);
}
#[tokio::test]
async fn test_stream_timeout() {
let mock_server = MockServer::start().await;
let sse_body = "\
event: response.output_text.delta\n\
data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello\"}\n\
\n\
data: [DONE]\n\n";
Mock::given(method("POST"))
.and(path("/responses"))
.respond_with(ResponseTemplate::new(200).set_body_string(sse_body.to_owned()))
.mount(&mock_server)
.await;
let adapter = make_adapter();
let auth = AuthMethod::Bearer { token: "sk-test".into() };
let req = CompletionRequest::new("test-model", vec![Msg::user("Hi")]);
let body = adapter.build_request_body(&req, true).unwrap();
let headers = adapter.build_auth_headers(&auth);
let url = format!("{}/responses", mock_server.uri());
let result = build_request(
&reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(1))
.build()
.unwrap(),
&url,
body,
headers,
Some(std::time::Duration::from_millis(1)),
)
.await;
match result {
Err(ProviderError::Timeout { .. }) => {} Err(e) => panic!("expected Timeout, got {:?}", e),
Ok(_) => panic!("expected timeout error, got success"),
}
}
#[test]
fn test_parse_sse_event_unknown_finish_reason_defaults_to_stop() {
let adapter = make_adapter();
let data = r#"{"type":"response.done","response":{"id":"resp_1","status":"weird_status"}}"#;
let event = adapter.parse_sse_event(data).unwrap();
assert_eq!(
event,
Some(StreamEvent::Done { finish_reason: FinishReason::Stop, usage: None })
);
}