#![cfg(not(target_family = "wasm"))]
use futures::{SinkExt, StreamExt};
use rig_core::completion::CompletionRequest;
use rig_core::error::ProviderError;
use rig_core::providers::openai::OpenAIConfig;
use rig_core::providers::openai::responses_api::websocket::ResponsesWebSocketEvent;
use rig_core::test_utils::streaming_conformance::{
self as conformance, fixtures::openai_responses,
};
use rig_core::test_utils::{RecordingHttpClient, SequencedStreamingHttpClient};
use rig_tungstenite::tokio_tungstenite::{accept_async, tungstenite::Message};
use tokio::net::TcpListener;
fn ws_script(chunks: conformance::WireChunks) -> Result<(Vec<String>, bool), ProviderError> {
let mut messages = Vec::new();
for chunk in chunks {
match chunk {
Ok(frame) => {
let bytes = frame.as_bytes().cloned().ok_or_else(|| {
ProviderError::Provider(
"typed-event frame fed to the websocket driver".to_string(),
)
})?;
let text = std::str::from_utf8(&bytes).map_err(|error| {
ProviderError::Provider(format!("non-UTF-8 fixture frame: {error}"))
})?;
messages.extend(
text.lines()
.filter_map(|line| line.strip_prefix("data:").map(str::trim))
.filter(|data| !data.is_empty() && *data != "[DONE]")
.map(ToOwned::to_owned),
);
}
Err(_) => return Ok((messages, true)),
}
}
Ok((messages, false))
}
fn spawn_server(listener: TcpListener, messages: Vec<String>, abort: bool) {
tokio::spawn(async move {
let Ok((stream, _)) = listener.accept().await else {
return;
};
let Ok(mut socket) = accept_async(stream).await else {
return;
};
let _ = socket.next().await;
for message in messages {
if socket.send(Message::text(message)).await.is_err() {
return;
}
}
if abort {
drop(socket);
} else {
let _ = socket.close(None).await;
}
});
}
async fn drain_openai_responses_websocket_events(
events: Vec<Result<ResponsesWebSocketEvent, ProviderError>>,
) -> Result<conformance::DrainedStream, ProviderError> {
let mut body = String::new();
let mut failed = false;
for event in events {
let frame = match event {
Ok(ResponsesWebSocketEvent::Item(chunk)) => serde_json::to_string(&chunk)?,
Ok(ResponsesWebSocketEvent::Response { kind, response }) => {
serde_json::json!({ "type": kind, "response": response }).to_string()
}
Ok(ResponsesWebSocketEvent::Unknown(value)) => value.value().to_string(),
Ok(ResponsesWebSocketEvent::Done(_)) => continue,
Ok(ResponsesWebSocketEvent::Error(_)) | Err(_) => {
failed = true;
break;
}
};
body.push_str(&format!("data: {frame}\n\n"));
}
let mut chunks = vec![Ok(bytes::Bytes::from(body))];
if failed {
chunks.push(Err(rig_core::http_client::Error::StreamEnded));
}
let model = OpenAIConfig::new("test-key")
.connect(SequencedStreamingHttpClient::new(chunks))
.responses("gpt-5.4");
let stream = model.stream(CompletionRequest::new("hello"))?;
Ok(conformance::fixtures::drain(stream).await)
}
fn driver() -> conformance::WireDriver {
conformance::WireDriver::new("openai-responses-websocket", |chunks| {
Box::pin(async move {
let (messages, abort) = ws_script(chunks)?;
let listener = TcpListener::bind("127.0.0.1:0").await.map_err(|error| {
ProviderError::Provider(format!("listener bind failed: {error}"))
})?;
let address = listener.local_addr().map_err(|error| {
ProviderError::Provider(format!("listener address failed: {error}"))
})?;
spawn_server(listener, messages, abort);
let bound = OpenAIConfig::new("test-key")
.with_base_url(format!("http://{address}/v1"))
.connect(RecordingHttpClient::new("{}"))
.responses("gpt-5.4");
let mut session = bound.responses_websocket().connect().await?;
session.send(CompletionRequest::new("hello")).await?;
let mut events = Vec::new();
loop {
match session.next_event().await {
Ok(event) => {
let terminal = event.is_terminal();
events.push(Ok(event));
if terminal {
break;
}
}
Err(error) => {
events.push(Err(error));
break;
}
}
}
drain_openai_responses_websocket_events(events).await
})
})
}
fn fixture() -> conformance::ProviderWireFixture {
conformance::ProviderWireFixture {
driver: driver(),
..openai_responses::fixture()
}
}
pub mod openai_responses_websocket_suite {
use super::*;
rig_core::streaming_conformance_suite! {
provider: "openai_responses_websocket",
fixture: fixture(),
manifest: [partial_tool_args, zero_usage_terminal, malformed_frame, unknown_event_frame, refusal],
}
}
const SUITE_FAMILIES: &[&str] = &[openai_responses_websocket_suite::WIRE_FAMILY];
#[test]
fn suite_families_are_registered_wire_families() {
for family in SUITE_FAMILIES {
assert!(
rig_core::test_utils::streaming_conformance::WIRE_FAMILIES.contains(family),
"suite names wire family {family:?}, absent from WIRE_FAMILIES"
);
}
}