use crate::registry;
use aigw_openai::{ResponsesRequestConfig, build_responses_create_request};
use async_trait::async_trait;
use byokey_auth::AuthManager;
use byokey_types::{
ByokError, ChatRequest, ProviderId,
traits::{ByteStream, ProviderExecutor, ProviderResponse, Result},
};
use bytes::Bytes;
use futures_util::{SinkExt as _, StreamExt as _, stream::try_unfold};
use serde_json::Value;
use std::sync::Arc;
use tokio_tungstenite::tungstenite;
use super::codex::translate_codex_responses_sse;
const WS_URL: &str = "wss://chatgpt.com/backend-api/codex/ws";
const WS_BETA: &str = "responses_websockets=2026-02-06";
const CODEX_USER_AGENT: &str = "codex-tui/0.120.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464";
pub struct CodexWsExecutor {
auth: Arc<AuthManager>,
}
impl CodexWsExecutor {
pub fn new(auth: Arc<AuthManager>) -> Self {
Self { auth }
}
async fn token(&self) -> Result<String> {
let tok = self.auth.get_token(&ProviderId::Codex).await?;
Ok(tok.access_token)
}
}
#[async_trait]
impl ProviderExecutor for CodexWsExecutor {
async fn chat_completion(&self, request: ChatRequest) -> Result<ProviderResponse> {
let token = self.token().await?;
let aigw_request: aigw_core::model::ChatRequest =
serde_json::from_value(request.into_body())
.map_err(|e: serde_json::Error| ByokError::Translation(e.to_string()))?;
let responses_req =
build_responses_create_request(&aigw_request, &ResponsesRequestConfig::codex())
.map_err(|e| ByokError::Translation(e.to_string()))?;
let mut codex_body = serde_json::to_value(&responses_req)
.map_err(|e: serde_json::Error| ByokError::Translation(e.to_string()))?;
codex_body["stream"] = Value::Bool(true);
codex_body["type"] = Value::String("response.create".into());
let ws_request = http::Request::builder()
.uri(WS_URL)
.header("Authorization", format!("Bearer {token}"))
.header("OpenAI-Beta", WS_BETA)
.header("User-Agent", CODEX_USER_AGENT)
.header("Originator", "codex_cli_rs")
.header(
"Sec-WebSocket-Key",
tungstenite::handshake::client::generate_key(),
)
.header("Sec-WebSocket-Version", "13")
.header("Connection", "Upgrade")
.header("Upgrade", "websocket")
.header("Host", "chatgpt.com")
.body(())
.map_err(|e| ByokError::Http(format!("failed to build WS request: {e}")))?;
let (ws_stream, _response) = tokio_tungstenite::connect_async(ws_request)
.await
.map_err(|e| ByokError::Http(format!("WebSocket connect failed: {e}")))?;
let (mut sink, stream) = ws_stream.split();
let payload = serde_json::to_string(&codex_body)
.map_err(|e| ByokError::Http(format!("failed to serialize body: {e}")))?;
sink.send(tungstenite::Message::Text(payload.into()))
.await
.map_err(|e| ByokError::Http(format!("WebSocket send failed: {e}")))?;
let raw_stream: ByteStream = Box::pin(try_unfold(stream, |mut ws_rx| async move {
loop {
match ws_rx.next().await {
Some(Ok(tungstenite::Message::Text(text))) => {
let sse_line = format!("data: {text}\n\n");
return Ok(Some((Bytes::from(sse_line), ws_rx)));
}
Some(Ok(tungstenite::Message::Close(_))) | None => {
return Ok(None);
}
Some(Ok(_)) => {
}
Some(Err(e)) => {
return Err(ByokError::Http(format!("WebSocket recv error: {e}")));
}
}
}
}));
Ok(ProviderResponse::Stream(translate_codex_responses_sse(
raw_stream,
)))
}
fn supported_models(&self) -> Vec<String> {
registry::models_for_provider(&ProviderId::Codex)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_executor() -> CodexWsExecutor {
let (_client, auth) = crate::http_util::test_auth();
CodexWsExecutor::new(auth)
}
#[test]
fn test_supported_models_non_empty() {
let ex = make_executor();
assert!(!ex.supported_models().is_empty());
}
#[test]
fn test_supported_models_contains_o4_mini() {
let ex = make_executor();
assert!(ex.supported_models().iter().any(|m| m == "o4-mini"));
}
}