mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
94427aaf46
commit
6652e82dd0
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user