Add WebRTC transport to realtime start (#16960)

Adds WebRTC startup to the experimental app-server
`thread/realtime/start` method with an optional transport enum. The
websocket path remains the default; WebRTC offers create the realtime
session through the shared start flow and emit the answer SDP via
`thread/realtime/sdp`.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-07 15:43:38 -07:00
committed by GitHub
parent 6c36e7d688
commit fb3dcfde1d
42 changed files with 1574 additions and 85 deletions

View File

@@ -22,6 +22,7 @@ pub use crate::default_client::CodexRequestBuilder;
pub use crate::error::StreamError;
pub use crate::error::TransportError;
pub use crate::request::Request;
pub use crate::request::RequestBody;
pub use crate::request::RequestCompression;
pub use crate::request::Response;
pub use crate::retry::RetryOn;

View File

@@ -12,12 +12,27 @@ pub enum RequestCompression {
Zstd,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestBody {
Json(Value),
Raw(Bytes),
}
impl RequestBody {
pub fn json(&self) -> Option<&Value> {
match self {
Self::Json(value) => Some(value),
Self::Raw(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Request {
pub method: Method,
pub url: String,
pub headers: HeaderMap,
pub body: Option<Value>,
pub body: Option<RequestBody>,
pub compression: RequestCompression,
pub timeout: Option<Duration>,
}
@@ -35,7 +50,12 @@ impl Request {
}
pub fn with_json<T: Serialize>(mut self, body: &T) -> Self {
self.body = serde_json::to_value(body).ok();
self.body = serde_json::to_value(body).ok().map(RequestBody::Json);
self
}
pub fn with_raw_body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(RequestBody::Raw(body.into()));
self
}

View File

@@ -2,6 +2,7 @@ use crate::default_client::CodexHttpClient;
use crate::default_client::CodexRequestBuilder;
use crate::error::TransportError;
use crate::request::Request;
use crate::request::RequestBody;
use crate::request::RequestCompression;
use crate::request::Response;
use async_trait::async_trait;
@@ -60,52 +61,63 @@ impl ReqwestTransport {
builder = builder.timeout(timeout);
}
if let Some(body) = body {
if compression != RequestCompression::None {
if headers.contains_key(http::header::CONTENT_ENCODING) {
match body {
Some(RequestBody::Raw(raw_body)) => {
if compression != RequestCompression::None {
return Err(TransportError::Build(
"request compression was requested but content-encoding is already set"
.to_string(),
"request compression cannot be used with raw bodies".to_string(),
));
}
let json = serde_json::to_vec(&body)
.map_err(|err| TransportError::Build(err.to_string()))?;
let pre_compression_bytes = json.len();
let compression_start = std::time::Instant::now();
let (compressed, content_encoding) = match compression {
RequestCompression::None => unreachable!("guarded by compression != None"),
RequestCompression::Zstd => (
zstd::stream::encode_all(std::io::Cursor::new(json), 3)
.map_err(|err| TransportError::Build(err.to_string()))?,
http::HeaderValue::from_static("zstd"),
),
};
let post_compression_bytes = compressed.len();
let compression_duration = compression_start.elapsed();
// Ensure the server knows to unpack the request body.
headers.insert(http::header::CONTENT_ENCODING, content_encoding);
if !headers.contains_key(http::header::CONTENT_TYPE) {
headers.insert(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static("application/json"),
);
}
tracing::info!(
pre_compression_bytes,
post_compression_bytes,
compression_duration_ms = compression_duration.as_millis(),
"Compressed request body with zstd"
);
builder = builder.headers(headers).body(compressed);
} else {
builder = builder.headers(headers).json(&body);
builder = builder.headers(headers).body(raw_body);
}
Some(RequestBody::Json(body)) => {
if compression != RequestCompression::None {
if headers.contains_key(http::header::CONTENT_ENCODING) {
return Err(TransportError::Build(
"request compression was requested but content-encoding is already set"
.to_string(),
));
}
let json = serde_json::to_vec(&body)
.map_err(|err| TransportError::Build(err.to_string()))?;
let pre_compression_bytes = json.len();
let compression_start = std::time::Instant::now();
let (compressed, content_encoding) = match compression {
RequestCompression::None => unreachable!("guarded by compression != None"),
RequestCompression::Zstd => (
zstd::stream::encode_all(std::io::Cursor::new(json), 3)
.map_err(|err| TransportError::Build(err.to_string()))?,
http::HeaderValue::from_static("zstd"),
),
};
let post_compression_bytes = compressed.len();
let compression_duration = compression_start.elapsed();
// Ensure the server knows to unpack the request body.
headers.insert(http::header::CONTENT_ENCODING, content_encoding);
if !headers.contains_key(http::header::CONTENT_TYPE) {
headers.insert(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static("application/json"),
);
}
tracing::info!(
pre_compression_bytes,
post_compression_bytes,
compression_duration_ms = compression_duration.as_millis(),
"Compressed request body with zstd"
);
builder = builder.headers(headers).body(compressed);
} else {
builder = builder.headers(headers).json(&body);
}
}
None => {
builder = builder.headers(headers);
}
} else {
builder = builder.headers(headers);
}
Ok(builder)
}
@@ -119,6 +131,14 @@ impl ReqwestTransport {
}
}
fn request_body_for_trace(req: &Request) -> String {
match req.body.as_ref() {
Some(RequestBody::Json(body)) => body.to_string(),
Some(RequestBody::Raw(body)) => format!("<raw body: {} bytes>", body.len()),
None => String::new(),
}
}
#[async_trait]
impl HttpTransport for ReqwestTransport {
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
@@ -127,7 +147,7 @@ impl HttpTransport for ReqwestTransport {
"{} to {}: {}",
req.method,
req.url,
req.body.as_ref().unwrap_or_default()
request_body_for_trace(&req)
);
}
@@ -159,7 +179,7 @@ impl HttpTransport for ReqwestTransport {
"{} to {}: {}",
req.method,
req.url,
req.body.as_ref().unwrap_or_default()
request_body_for_trace(&req)
);
}