use base64::Engine;
use base64::engine::general_purpose;
use crate::config::OpenAIConfig;
use crate::error::OpenAIAgentError;
use futures_util::{SinkExt, StreamExt};
use rand::RngCore;
use rustls::crypto::CryptoProvider;
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream};
use tokio_tungstenite::tungstenite::{handshake::client::Request, Message};
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use url::Url;
#[derive(Serialize, Deserialize, Debug)]
pub struct RealtimeEvent {
pub r#type: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ServerEvent {
pub event_type: String,
}
pub struct WebSocketClient {
config: OpenAIConfig,
connection: Option<WebSocketStream<MaybeTlsStream<TcpStream>>>,
}
impl WebSocketClient {
pub fn new(config: OpenAIConfig) -> Result<Self, OpenAIAgentError> {
CryptoProvider::get_default();
if config.api_key().is_empty() {
return Err(OpenAIAgentError::Config(
"API key not provided".to_string(),
));
}
Ok(Self {
config,
connection: None,
})
}
pub async fn connect(&mut self, model_name: &str) -> Result<(), OpenAIAgentError> {
let base_url = self.config.base_url();
let ws_base = base_url
.replace("https://", "wss://")
.replace("http://", "ws://")
.trim_end_matches('/')
.to_string();
let realtime_path = format!("{}/realtime", ws_base);
let url = Url::parse_with_params(&realtime_path, &[("model", model_name)])
.map_err(|e| OpenAIAgentError::Config(format!("Invalid WebSocket URL: {}", e)))?;
println!("Connecting to Realtime API... {}", url);
let mut key = [0u8; 16];
rand::thread_rng().fill_bytes(&mut key);
let key_base64 = general_purpose::STANDARD.encode(&key);
let host = url.host_str().ok_or_else(|| {
OpenAIAgentError::Config("URL missing host information".to_string())
})?;
let url_str = url.to_string();
let mut request = url_str.into_client_request().map_err(|e| {
OpenAIAgentError::Config(format!("Request conversion error: {}", e))
})?;
{
let headers = request.headers_mut();
headers.insert("Host", host.parse().unwrap());
headers.insert("Authorization", format!("Bearer {}", self.config.api_key()).parse().unwrap());
headers.insert("OpenAI-Beta", "realtime=v1".parse().unwrap());
headers.insert("Sec-WebSocket-Key", key_base64.parse().unwrap());
headers.insert("Sec-WebSocket-Version", "13".parse().unwrap());
}
let (ws_stream, response) = connect_async(request)
.await
.map_err(|e| OpenAIAgentError::Request(format!("WebSocket connection failed: {}", e)))?;
println!("Connected to Realtime API with HTTP status: {}", response.status());
self.connection = Some(ws_stream);
Ok(())
}
pub async fn send_event(&mut self, event: &RealtimeEvent) -> Result<(), OpenAIAgentError> {
let connection = match &mut self.connection {
Some(conn) => conn,
None => return Err(OpenAIAgentError::Request("Not connected".to_string())),
};
let payload = serde_json::to_string(event)
.map_err(OpenAIAgentError::Serialization)?;
connection
.send(Message::Text(payload.into()))
.await
.map_err(|e| OpenAIAgentError::Request(format!("Failed to send event: {}", e)))?;
Ok(())
}
pub async fn process_incoming<F>(&mut self, mut on_event: F) -> Result<(), OpenAIAgentError>
where
F: FnMut(ServerEvent) -> Result<(), OpenAIAgentError>,
{
let connection = match &mut self.connection {
Some(conn) => conn,
None => return Err(OpenAIAgentError::Request("Not connected".to_string())),
};
while let Some(msg_result) = connection.next().await {
let msg = msg_result
.map_err(|e| OpenAIAgentError::Request(format!("WebSocket read error: {}", e)))?;
match msg {
Message::Text(txt) => {
match serde_json::from_str::<ServerEvent>(&txt) {
Ok(server_event) => {
on_event(server_event)?;
}
Err(parse_err) => {
eprintln!("Failed to parse inbound JSON: {}", parse_err);
}
}
}
Message::Binary(bin) => {
println!("Got {} bytes of binary data", bin.len());
}
Message::Close(frame) => {
println!("Server closed the connection: {:?}", frame);
break; }
_ => {
}
}
}
Ok(())
}
pub async fn close(&mut self) -> Result<(), OpenAIAgentError> {
if let Some(conn) = &mut self.connection {
conn.close(None)
.await
.map_err(|e| OpenAIAgentError::Request(format!("Failed to close connection: {}", e)))?;
self.connection = None;
}
Ok(())
}
}
impl Drop for WebSocketClient {
fn drop(&mut self) {
if self.connection.is_some() {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.block_on(async {
let _ = self.close().await;
});
}
}
}
}