realtime: add AVAS architecture override (#27720)

## Summary

Adds a `RealtimeConversationArchitecture` option for realtime
conversation startup, with `realtimeapi` as the default and `avas` as an
opt-in architecture.

The AVAS path is limited to realtime v1 conversational WebRTC starts,
and WebRTC call creation appends `intent=quicksilver&architecture=avas`
to `/v1/realtime/calls`. The existing sideband websocket still joins by
`call_id`.

This also exposes the per-session architecture override through
app-server v2 `thread/realtime/start` params and updates the config
schema for `[realtime].architecture`.

## Validation

- `just fmt`
- `just write-config-schema`
- `just test -p codex-api sends_avas_session_call_query_params`
- `just test -p codex-core -E
'test(~conversation_webrtc_start_uses_avas_architecture_query)'`
- `just test -p codex-core -E 'test(realtime_loads_from_config_toml)'`
- `just test -p codex-app-server-protocol -E
'test(~serialize_thread_realtime_start) |
test(generated_ts_optional_nullable_fields_only_in_params)'`
- `just test -p codex-app-server -E
'test(realtime_webrtc_start_emits_sdp_notification)'`
This commit is contained in:
Peter Bakkum
2026-06-12 11:11:13 -07:00
committed by GitHub
Unverified
parent 94427aaf46
commit 6652e82dd0
22 changed files with 521 additions and 11 deletions
@@ -6,8 +6,10 @@ use crate::error::ApiError;
use crate::provider::Provider;
use bytes::Bytes;
use codex_client::HttpTransport;
use codex_client::Request;
use codex_client::RequestBody;
use codex_client::RequestTelemetry;
use codex_protocol::protocol::RealtimeConversationArchitecture;
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
@@ -118,6 +120,22 @@ impl<T: HttpTransport> RealtimeCallClient<T> {
sdp: String,
session_config: RealtimeSessionConfig,
extra_headers: HeaderMap,
) -> Result<RealtimeCallResponse, ApiError> {
self.create_with_session_architecture_and_headers(
sdp,
session_config,
RealtimeConversationArchitecture::RealtimeApi,
extra_headers,
)
.await
}
pub async fn create_with_session_architecture_and_headers(
&self,
sdp: String,
session_config: RealtimeSessionConfig,
architecture: RealtimeConversationArchitecture,
extra_headers: HeaderMap,
) -> Result<RealtimeCallResponse, ApiError> {
trace!(target: "codex_api::realtime_websocket::wire", "realtime call request SDP: {sdp}");
// WebRTC can begin inference as soon as the peer connection comes up, so the initial
@@ -136,7 +154,13 @@ impl<T: HttpTransport> RealtimeCallClient<T> {
.map_err(|err| ApiError::Stream(format!("failed to encode realtime call: {err}")))?;
let resp = self
.session
.execute(Method::POST, Self::path(), extra_headers, Some(body))
.execute_with(
Method::POST,
Self::path(),
extra_headers,
Some(body),
|req| configure_realtime_call_request(req, architecture),
)
.await?;
let sdp = decode_sdp_response(resp.body.as_ref())?;
let call_id = decode_call_id_from_location(&resp.headers)?;
@@ -167,6 +191,7 @@ impl<T: HttpTransport> RealtimeCallClient<T> {
extra_headers,
/*body*/ None,
|req| {
configure_realtime_call_request(req, architecture);
req.headers.insert(
CONTENT_TYPE,
HeaderValue::from_static(MULTIPART_CONTENT_TYPE),
@@ -183,6 +208,30 @@ impl<T: HttpTransport> RealtimeCallClient<T> {
}
}
fn configure_realtime_call_request(
request: &mut Request,
architecture: RealtimeConversationArchitecture,
) {
match architecture {
RealtimeConversationArchitecture::RealtimeApi => {}
RealtimeConversationArchitecture::Avas => {
append_query_pair(&mut request.url, "intent", "quicksilver");
append_query_pair(&mut request.url, "architecture", "avas");
}
}
}
fn append_query_pair(url: &mut String, key: &str, value: &str) {
if url.contains('?') {
url.push('&');
} else {
url.push('?');
}
url.push_str(key);
url.push('=');
url.push_str(value);
}
fn realtime_session_json(session_config: RealtimeSessionConfig) -> Result<Value, ApiError> {
session_update_session_json(session_config)
.map_err(|err| ApiError::Stream(format!("failed to encode realtime call session: {err}")))
@@ -209,7 +258,7 @@ fn decode_call_id_from_location(headers: &HeaderMap) -> Result<String, ApiError>
.next()
.unwrap_or(location)
.rsplit('/')
.find(|segment| segment.starts_with("rtc_") && segment.len() > "rtc_".len())
.find(|segment| is_realtime_call_id_segment(segment))
.map(str::to_string)
.ok_or_else(|| {
ApiError::Stream(format!(
@@ -218,6 +267,21 @@ fn decode_call_id_from_location(headers: &HeaderMap) -> Result<String, ApiError>
})
}
fn is_realtime_call_id_segment(segment: &str) -> bool {
if segment.starts_with("rtc_") && segment.len() > "rtc_".len() {
return true;
}
if segment.len() != 36 {
return false;
}
segment.char_indices().all(|(index, ch)| match index {
8 | 13 | 18 | 23 => ch == '-',
_ => ch.is_ascii_hexdigit(),
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -459,6 +523,41 @@ mod tests {
);
}
#[tokio::test]
async fn sends_avas_session_call_query_params() {
let transport = CapturingTransport::new();
let client = RealtimeCallClient::new(
transport.clone(),
provider("https://api.openai.com/v1"),
Arc::new(DummyAuth),
);
let response = client
.create_with_session_architecture_and_headers(
"v=offer\r\n".to_string(),
realtime_session_config("sess-api"),
RealtimeConversationArchitecture::Avas,
HeaderMap::new(),
)
.await
.expect("request should succeed");
assert_eq!(
response,
RealtimeCallResponse {
sdp: "v=0\r\n".to_string(),
call_id: "rtc_test".to_string(),
}
);
let request = transport.last_request.lock().unwrap().clone().unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(
request.url,
"https://api.openai.com/v1/realtime/calls?intent=quicksilver&architecture=avas"
);
}
#[tokio::test]
async fn sends_backend_session_call_as_json_body() {
let transport = CapturingTransport::new();
@@ -541,4 +640,17 @@ mod tests {
"stream error: realtime call Location does not contain a call id: /v1/realtime/calls"
);
}
#[test]
fn accepts_uuid_call_id_from_location() {
let mut headers = HeaderMap::new();
headers.insert(
LOCATION,
HeaderValue::from_static("/v1/realtime/calls/019eb97d-8e9a-7ff3-94b0-ea019babd5d7"),
);
let call_id = decode_call_id_from_location(&headers).expect("UUID call id should parse");
assert_eq!(call_id, "019eb97d-8e9a-7ff3-94b0-ea019babd5d7");
}
}