mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Support configurable realtime BEM channel prefixes (#34816)
## What changed - Route realtime V3 `bemTags` handoffs using `[ANALYSIS]`, `[COMMENTARY]`, and `[FINAL]` prefixes by default. - Add `codexResponseHandoffChannelPrefixes` to realtime start requests so clients can replace the accepted prefixes for individual channels. Each channel may accept multiple prefixes, and omitted channels retain their defaults. - Apply the configured prefixes to both complete and streamed handoff messages, without treating empty prefixes as matches. ## Testing - Cover request deserialization, default and custom routing, partial streamed prefixes, empty-prefix handling, and end-to-end V3 handoffs. GitOrigin-RevId: d0aef50aa71362a5beef2c6f6d28b8633da4e529
This commit is contained in:
committed by
copyberry
parent
08ae0fc0ce
commit
4ebd976312
@@ -3482,6 +3482,14 @@ mod tests {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: Some(CodexResponseHandoffMode::BemTags),
|
||||
codex_response_handoff_channel_prefixes: Some(std::collections::BTreeMap::from([
|
||||
("analysis".to_string(), vec!["[THINKING]".to_string()]),
|
||||
(
|
||||
"commentary".to_string(),
|
||||
vec!["[PROGRESS]".to_string(), "[UPDATE]".to_string()],
|
||||
),
|
||||
("final".to_string(), vec!["[DONE]".to_string()]),
|
||||
])),
|
||||
thread_id: "thr_123".to_string(),
|
||||
model: Some("realtime-treatment-model".to_string()),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -3514,6 +3522,11 @@ mod tests {
|
||||
"codexResponsesAsItems": null,
|
||||
"codexResponseItemPrefix": null,
|
||||
"codexResponseHandoffMode": "bemTags",
|
||||
"codexResponseHandoffChannelPrefixes": {
|
||||
"analysis": ["[THINKING]"],
|
||||
"commentary": ["[PROGRESS]", "[UPDATE]"],
|
||||
"final": ["[DONE]"]
|
||||
},
|
||||
"model": "realtime-treatment-model",
|
||||
"outputModality": "audio",
|
||||
"includeStartupContext": false,
|
||||
@@ -3549,6 +3562,7 @@ mod tests {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: "thr_123".to_string(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -3572,6 +3586,7 @@ mod tests {
|
||||
"codexResponsesAsItems": null,
|
||||
"codexResponseItemPrefix": null,
|
||||
"codexResponseHandoffMode": null,
|
||||
"codexResponseHandoffChannelPrefixes": null,
|
||||
"model": null,
|
||||
"outputModality": "audio",
|
||||
"includeStartupContext": null,
|
||||
@@ -3593,6 +3608,7 @@ mod tests {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: "thr_123".to_string(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -3616,6 +3632,7 @@ mod tests {
|
||||
"codexResponsesAsItems": null,
|
||||
"codexResponseItemPrefix": null,
|
||||
"codexResponseHandoffMode": null,
|
||||
"codexResponseHandoffChannelPrefixes": null,
|
||||
"model": null,
|
||||
"outputModality": "audio",
|
||||
"includeStartupContext": null,
|
||||
@@ -3837,6 +3854,7 @@ mod tests {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: "thr_123".to_string(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
|
||||
@@ -9,6 +9,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// EXPERIMENTAL - thread realtime audio chunk.
|
||||
@@ -87,6 +88,10 @@ pub struct ThreadRealtimeStartParams {
|
||||
/// default to `thinking`. Realtime V1 and V2 ignore this setting.
|
||||
#[ts(optional = nullable)]
|
||||
pub codex_response_handoff_mode: Option<CodexResponseHandoffMode>,
|
||||
/// Overrides BEM channel prefixes by `analysis`, `commentary`, or `final`.
|
||||
/// Omitted channels retain their default uppercase bracketed prefixes.
|
||||
#[ts(optional = nullable)]
|
||||
pub codex_response_handoff_channel_prefixes: Option<BTreeMap<String, Vec<String>>>,
|
||||
/// Overrides the configured realtime model for this session only.
|
||||
#[ts(optional = nullable)]
|
||||
pub model: Option<String>,
|
||||
|
||||
@@ -4462,3 +4462,28 @@ fn realtime_start_omitted_initial_items_remain_none() {
|
||||
|
||||
assert_eq!(params.initial_items, None);
|
||||
}
|
||||
#[test]
|
||||
fn realtime_start_deserializes_client_handoff_channel_prefixes() {
|
||||
let params = serde_json::from_value::<ThreadRealtimeStartParams>(json!({
|
||||
"threadId": "thread_123",
|
||||
"outputModality": "audio",
|
||||
"codexResponseHandoffChannelPrefixes": {
|
||||
"analysis": ["[THINKING]"],
|
||||
"commentary": ["[PROGRESS]", "[UPDATE]"],
|
||||
"final": ["[DONE]"]
|
||||
}
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
params.codex_response_handoff_channel_prefixes,
|
||||
Some(BTreeMap::from([
|
||||
("analysis".to_string(), vec!["[THINKING]".to_string()]),
|
||||
(
|
||||
"commentary".to_string(),
|
||||
vec!["[PROGRESS]".to_string(), "[UPDATE]".to_string()],
|
||||
),
|
||||
("final".to_string(), vec!["[DONE]".to_string()]),
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -983,7 +983,12 @@ for every automatic response. Pass `"commentary"` to route every response to
|
||||
commentary, or `"bemTags"` to route BEM commentary tags to `commentary`, final
|
||||
tags to `speakable`, and analysis tags to `commentary`. Unparsable BEM output
|
||||
falls back to `speakable`. BEM routing reads the raw envelope and preserves it
|
||||
in the appended text for the frontend model. This
|
||||
in the appended text for the frontend model. With `"bemTags"`, clients may pass
|
||||
`codexResponseHandoffChannelPrefixes` to override the accepted prefixes for
|
||||
individual channels, for example
|
||||
`{"analysis":["[THINKING]"],"commentary":["[PROGRESS]","[UPDATE]"],"final":["[DONE]"]}`.
|
||||
Omitted channels keep the hard-coded `[ANALYSIS]`, `[COMMENTARY]`, and `[FINAL]`
|
||||
defaults. This
|
||||
setting has no effect on V1 or V2. V3 handoffs never prepend the legacy `"Agent Final Message"` label. Older
|
||||
clients may continue to send the removed `codexResponseHandoffPrefix` field; the
|
||||
server ignores unknown request fields.
|
||||
|
||||
@@ -1078,6 +1078,8 @@ impl TurnRequestProcessor {
|
||||
codex_responses_as_items: params.codex_responses_as_items.unwrap_or(false),
|
||||
codex_response_item_prefix: params.codex_response_item_prefix,
|
||||
codex_response_handoff_mode: params.codex_response_handoff_mode.unwrap_or_default(),
|
||||
codex_response_handoff_channel_prefixes: params
|
||||
.codex_response_handoff_channel_prefixes,
|
||||
model: params.model,
|
||||
output_modality: params.output_modality,
|
||||
include_startup_context: params.include_startup_context.unwrap_or(true),
|
||||
|
||||
@@ -94,6 +94,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: "thr_123".to_string(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -224,6 +225,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: "thr_123".to_string(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
|
||||
@@ -61,6 +61,7 @@ use pretty_assertions::assert_eq;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -367,6 +368,7 @@ impl RealtimeE2eHarness {
|
||||
.unwrap_or(false)
|
||||
.then(|| RESPONSE_ITEM_PREFIX.to_string()),
|
||||
codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
codex_responses_as_items,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -424,6 +426,7 @@ impl RealtimeE2eHarness {
|
||||
.unwrap_or(false)
|
||||
.then(|| RESPONSE_ITEM_PREFIX.to_string()),
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
codex_responses_as_items,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -446,6 +449,7 @@ impl RealtimeE2eHarness {
|
||||
async fn start_frameless_bidi_realtime(
|
||||
&mut self,
|
||||
codex_response_handoff_mode: Option<CodexResponseHandoffMode>,
|
||||
codex_response_handoff_channel_prefixes: Option<BTreeMap<String, Vec<String>>>,
|
||||
initial_items: Option<Vec<ThreadRealtimeInitialItem>>,
|
||||
) -> Result<ThreadRealtimeStartedNotification> {
|
||||
let start_request_id = self
|
||||
@@ -456,6 +460,7 @@ impl RealtimeE2eHarness {
|
||||
flush_transcript_tail_on_session_end: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes,
|
||||
codex_responses_as_items: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -704,6 +709,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
model: Some("realtime-treatment-model".to_string()),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -982,6 +988,7 @@ async fn realtime_start_can_skip_startup_context() -> Result<()> {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -1076,6 +1083,7 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Text,
|
||||
@@ -1251,6 +1259,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -1347,6 +1356,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_id.clone(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -2120,6 +2130,7 @@ async fn websocket_v3_passes_initial_items_through_session_start() -> Result<()>
|
||||
let started = harness
|
||||
.start_frameless_bidi_realtime(
|
||||
/*codex_response_handoff_mode*/ None,
|
||||
/*codex_response_handoff_channel_prefixes*/ None,
|
||||
Some(vec![
|
||||
ThreadRealtimeInitialItem {
|
||||
role: ConversationTextRole::Developer,
|
||||
@@ -2158,10 +2169,27 @@ async fn websocket_v3_passes_initial_items_through_session_start() -> Result<()>
|
||||
async fn websocket_v3_routes_handoffs_by_session_mode() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
for (mode, expected_channels) in [
|
||||
(None, [None, None, None, None]),
|
||||
for (mode, channel_prefixes, texts, expected_channels) in [
|
||||
(
|
||||
None,
|
||||
None,
|
||||
[
|
||||
"[ANALYSIS]silent context",
|
||||
"[COMMENTARY]still working",
|
||||
"[FINAL]finished",
|
||||
"unparsable BEM output",
|
||||
],
|
||||
[None, None, None, None],
|
||||
),
|
||||
(
|
||||
Some(CodexResponseHandoffMode::Commentary),
|
||||
None,
|
||||
[
|
||||
"[ANALYSIS]silent context",
|
||||
"[COMMENTARY]still working",
|
||||
"[FINAL]finished",
|
||||
"unparsable BEM output",
|
||||
],
|
||||
[
|
||||
Some("commentary"),
|
||||
Some("commentary"),
|
||||
@@ -2171,6 +2199,36 @@ async fn websocket_v3_routes_handoffs_by_session_mode() -> Result<()> {
|
||||
),
|
||||
(
|
||||
Some(CodexResponseHandoffMode::BemTags),
|
||||
None,
|
||||
[
|
||||
"[ANALYSIS]silent context",
|
||||
"[COMMENTARY]still working",
|
||||
"[FINAL]finished",
|
||||
"unparsable BEM output",
|
||||
],
|
||||
[
|
||||
Some("commentary"),
|
||||
Some("commentary"),
|
||||
Some("speakable"),
|
||||
Some("speakable"),
|
||||
],
|
||||
),
|
||||
(
|
||||
Some(CodexResponseHandoffMode::BemTags),
|
||||
Some(BTreeMap::from([
|
||||
("analysis".to_string(), vec!["[THOUGHT]".to_string()]),
|
||||
(
|
||||
"commentary".to_string(),
|
||||
vec!["[PROGRESS]".to_string(), "[UPDATE]".to_string()],
|
||||
),
|
||||
("final".to_string(), vec!["[DONE]".to_string()]),
|
||||
])),
|
||||
[
|
||||
"[THOUGHT]silent context",
|
||||
"[UPDATE]still working",
|
||||
"[DONE]finished",
|
||||
"unparsable BEM output",
|
||||
],
|
||||
[
|
||||
Some("commentary"),
|
||||
Some("commentary"),
|
||||
@@ -2179,11 +2237,7 @@ async fn websocket_v3_routes_handoffs_by_session_mode() -> Result<()> {
|
||||
],
|
||||
),
|
||||
] {
|
||||
let analysis_text = "<|start|>assistant<|channel|>analysis<|message|>silent context<|end|>";
|
||||
let commentary_text =
|
||||
"<|start|>assistant<|channel|>commentary<|message|>still working<|end|>";
|
||||
let final_text = "<|start|>assistant<|channel|>final<|message|>finished<|end|>";
|
||||
let fallback_text = "unparsable BEM output";
|
||||
let [analysis_text, commentary_text, final_text, fallback_text] = texts;
|
||||
let analysis = responses::ev_assistant_message("msg-analysis", analysis_text);
|
||||
let commentary = responses::ev_assistant_message("msg-commentary", commentary_text);
|
||||
let final_answer = responses::ev_assistant_message("msg-final", final_text);
|
||||
@@ -2225,7 +2279,7 @@ async fn websocket_v3_routes_handoffs_by_session_mode() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
let started = harness
|
||||
.start_frameless_bidi_realtime(mode, /*initial_items*/ None)
|
||||
.start_frameless_bidi_realtime(mode, channel_prefixes, /*initial_items*/ None)
|
||||
.await?;
|
||||
assert_eq!(started.version, RealtimeConversationVersion::V3);
|
||||
let _ = harness
|
||||
@@ -2937,6 +2991,7 @@ async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_start.thread.id,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
@@ -2999,6 +3054,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> {
|
||||
codex_responses_as_items: None,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: None,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
|
||||
@@ -64,6 +64,7 @@ use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::header::AUTHORIZATION;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -138,6 +139,7 @@ struct RealtimeHandoffState {
|
||||
codex_responses_as_items: bool,
|
||||
codex_response_item_prefix: Option<String>,
|
||||
codex_response_handoff_mode: CodexResponseHandoffMode,
|
||||
codex_response_handoff_channel_prefixes: Arc<BTreeMap<String, Vec<String>>>,
|
||||
session_kind: RealtimeSessionKind,
|
||||
event_parser: RealtimeEventParser,
|
||||
}
|
||||
@@ -430,28 +432,6 @@ struct RealtimeInputChannels {
|
||||
}
|
||||
|
||||
impl RealtimeHandoffState {
|
||||
fn new(
|
||||
output_tx: Sender<RealtimeOutbound>,
|
||||
client_managed_handoffs: bool,
|
||||
codex_responses_as_items: bool,
|
||||
codex_response_item_prefix: Option<String>,
|
||||
codex_response_handoff_mode: CodexResponseHandoffMode,
|
||||
session_kind: RealtimeSessionKind,
|
||||
event_parser: RealtimeEventParser,
|
||||
) -> Self {
|
||||
Self {
|
||||
output_tx,
|
||||
last_output: Arc::new(Mutex::new(None)),
|
||||
stream: Arc::new(Mutex::new(RealtimeHandoffStreamState::default())),
|
||||
client_managed_handoffs,
|
||||
codex_responses_as_items,
|
||||
codex_response_item_prefix,
|
||||
codex_response_handoff_mode,
|
||||
session_kind,
|
||||
event_parser,
|
||||
}
|
||||
}
|
||||
|
||||
fn streams_handoff_append(&self) -> bool {
|
||||
self.event_parser == RealtimeEventParser::FramelessBidi
|
||||
&& !self.client_managed_handoffs
|
||||
@@ -484,6 +464,7 @@ struct RealtimeStart {
|
||||
codex_responses_as_items: bool,
|
||||
codex_response_item_prefix: Option<String>,
|
||||
codex_response_handoff_mode: CodexResponseHandoffMode,
|
||||
codex_response_handoff_channel_prefixes: Option<BTreeMap<String, Vec<String>>>,
|
||||
realtime_call_api_provider: Option<ApiProvider>,
|
||||
session_config: RealtimeSessionConfig,
|
||||
model_client: ModelClient,
|
||||
@@ -543,6 +524,7 @@ impl RealtimeConversationManager {
|
||||
codex_responses_as_items,
|
||||
codex_response_item_prefix,
|
||||
codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes,
|
||||
realtime_call_api_provider,
|
||||
session_config,
|
||||
model_client,
|
||||
@@ -566,15 +548,20 @@ impl RealtimeConversationManager {
|
||||
|
||||
let realtime_active = Arc::new(AtomicBool::new(true));
|
||||
let stop_token = CancellationToken::new();
|
||||
let handoff = RealtimeHandoffState::new(
|
||||
handoff_output_tx,
|
||||
let handoff = RealtimeHandoffState {
|
||||
output_tx: handoff_output_tx,
|
||||
last_output: Arc::new(Mutex::new(None)),
|
||||
stream: Arc::new(Mutex::new(RealtimeHandoffStreamState::default())),
|
||||
client_managed_handoffs,
|
||||
codex_responses_as_items,
|
||||
codex_response_item_prefix,
|
||||
codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes: Arc::new(
|
||||
codex_response_handoff_channel_prefixes.unwrap_or_default(),
|
||||
),
|
||||
session_kind,
|
||||
event_parser,
|
||||
);
|
||||
};
|
||||
let input_channels = RealtimeInputChannels {
|
||||
text_rx,
|
||||
handoff_output_rx,
|
||||
@@ -755,7 +742,10 @@ impl RealtimeConversationManager {
|
||||
return Ok(());
|
||||
}
|
||||
let phase = if handoff.routes_handoff_by_bem() {
|
||||
match bem_message_phase(&output_text) {
|
||||
match bem_message_phase(
|
||||
&output_text,
|
||||
&handoff.codex_response_handoff_channel_prefixes,
|
||||
) {
|
||||
Some(phase) => Some(phase),
|
||||
None => {
|
||||
warn!("BEM output did not contain a recognized channel header");
|
||||
@@ -855,9 +845,11 @@ impl RealtimeConversationManager {
|
||||
} else {
|
||||
phase
|
||||
},
|
||||
bem_channel_parser: handoff
|
||||
.routes_handoff_by_bem()
|
||||
.then(BemChannelParser::default),
|
||||
bem_channel_parser: handoff.routes_handoff_by_bem().then(|| {
|
||||
BemChannelParser::new(Arc::clone(
|
||||
&handoff.codex_response_handoff_channel_prefixes,
|
||||
))
|
||||
}),
|
||||
prefix_final_message: handoff.event_parser == RealtimeEventParser::V1,
|
||||
sent_bytes: 0,
|
||||
buffered_text: String::new(),
|
||||
@@ -1104,6 +1096,7 @@ struct PreparedRealtimeConversationStart {
|
||||
codex_responses_as_items: bool,
|
||||
codex_response_item_prefix: Option<String>,
|
||||
codex_response_handoff_mode: CodexResponseHandoffMode,
|
||||
codex_response_handoff_channel_prefixes: Option<BTreeMap<String, Vec<String>>>,
|
||||
realtime_call_api_provider: Option<ApiProvider>,
|
||||
requested_realtime_session_id: Option<String>,
|
||||
version: RealtimeWsVersion,
|
||||
@@ -1195,6 +1188,7 @@ async fn prepare_realtime_start(
|
||||
codex_responses_as_items: params.codex_responses_as_items,
|
||||
codex_response_item_prefix: params.codex_response_item_prefix,
|
||||
codex_response_handoff_mode: params.codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes: params.codex_response_handoff_channel_prefixes,
|
||||
realtime_call_api_provider,
|
||||
requested_realtime_session_id,
|
||||
version,
|
||||
@@ -1393,6 +1387,7 @@ async fn handle_start_inner(
|
||||
codex_responses_as_items,
|
||||
codex_response_item_prefix,
|
||||
codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes,
|
||||
realtime_call_api_provider,
|
||||
requested_realtime_session_id,
|
||||
version,
|
||||
@@ -1412,6 +1407,7 @@ async fn handle_start_inner(
|
||||
codex_responses_as_items,
|
||||
codex_response_item_prefix,
|
||||
codex_response_handoff_mode,
|
||||
codex_response_handoff_channel_prefixes,
|
||||
realtime_call_api_provider,
|
||||
session_config,
|
||||
model_client: sess.services.model_client.clone(),
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
const BEM_ASSISTANT_CHANNEL_PREFIX: &str = "<|start|>assistant<|channel|>";
|
||||
const BEM_MESSAGE_MARKER: &str = "<|message|>";
|
||||
|
||||
pub(super) fn message_phase(text: &str) -> Option<MessagePhase> {
|
||||
let channel_and_message = text.strip_prefix(BEM_ASSISTANT_CHANNEL_PREFIX)?;
|
||||
let (channel, _) = channel_and_message.split_once(BEM_MESSAGE_MARKER)?;
|
||||
match channel {
|
||||
"analysis" | "commentary" => Some(MessagePhase::Commentary),
|
||||
"final" => Some(MessagePhase::FinalAnswer),
|
||||
_ => None,
|
||||
pub(super) fn message_phase(
|
||||
text: &str,
|
||||
channel_prefixes: &BTreeMap<String, Vec<String>>,
|
||||
) -> Option<MessagePhase> {
|
||||
for (channel, default_prefix, phase) in [
|
||||
("analysis", "[ANALYSIS]", MessagePhase::Commentary),
|
||||
("commentary", "[COMMENTARY]", MessagePhase::Commentary),
|
||||
("final", "[FINAL]", MessagePhase::FinalAnswer),
|
||||
] {
|
||||
let matches = channel_prefixes.get(channel).map_or_else(
|
||||
|| text.starts_with(default_prefix),
|
||||
|prefixes| {
|
||||
prefixes
|
||||
.iter()
|
||||
.any(|prefix| !prefix.is_empty() && text.starts_with(prefix))
|
||||
},
|
||||
);
|
||||
if matches {
|
||||
return Some(phase);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Buffers a streamed BEM message until its channel header is complete.
|
||||
@@ -19,18 +33,26 @@ pub(super) fn message_phase(text: &str) -> Option<MessagePhase> {
|
||||
/// the frontend model can distinguish BEM `analysis` from `commentary`.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct ChannelParser {
|
||||
channel_prefixes: Arc<BTreeMap<String, Vec<String>>>,
|
||||
buffered_text: String,
|
||||
phase: Option<MessagePhase>,
|
||||
}
|
||||
|
||||
impl ChannelParser {
|
||||
pub(super) fn new(channel_prefixes: Arc<BTreeMap<String, Vec<String>>>) -> Self {
|
||||
Self {
|
||||
channel_prefixes,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, text: &str) -> Option<String> {
|
||||
if self.phase.is_some() {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
|
||||
self.buffered_text.push_str(text);
|
||||
self.phase = message_phase(&self.buffered_text);
|
||||
self.phase = message_phase(&self.buffered_text, &self.channel_prefixes);
|
||||
self.phase.as_ref()?;
|
||||
Some(std::mem::take(&mut self.buffered_text))
|
||||
}
|
||||
|
||||
@@ -2,34 +2,95 @@ use super::ChannelParser;
|
||||
use super::message_phase;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn maps_bem_channels_to_realtime_phases() {
|
||||
let channel_prefixes = BTreeMap::new();
|
||||
for (channel, expected) in [
|
||||
("analysis", MessagePhase::Commentary),
|
||||
("commentary", MessagePhase::Commentary),
|
||||
("final", MessagePhase::FinalAnswer),
|
||||
("ANALYSIS", MessagePhase::Commentary),
|
||||
("COMMENTARY", MessagePhase::Commentary),
|
||||
("FINAL", MessagePhase::FinalAnswer),
|
||||
] {
|
||||
assert_eq!(
|
||||
message_phase(&format!(
|
||||
"<|start|>assistant<|channel|>{channel}<|message|>text<|end|>"
|
||||
)),
|
||||
message_phase(&format!("[{channel}]text"), &channel_prefixes),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_prefixes_override_only_their_configured_channels() {
|
||||
let channel_prefixes = BTreeMap::from([
|
||||
(
|
||||
"analysis".to_string(),
|
||||
vec!["[THINKING]".to_string(), "[THOUGHT]".to_string()],
|
||||
),
|
||||
(
|
||||
"commentary".to_string(),
|
||||
vec!["[PROGRESS]".to_string(), "[UPDATE]".to_string()],
|
||||
),
|
||||
]);
|
||||
|
||||
for (text, expected) in [
|
||||
("[THINKING]reasoning", MessagePhase::Commentary),
|
||||
("[THOUGHT]reasoning", MessagePhase::Commentary),
|
||||
("[PROGRESS]working", MessagePhase::Commentary),
|
||||
("[UPDATE]working", MessagePhase::Commentary),
|
||||
("[FINAL]finished", MessagePhase::FinalAnswer),
|
||||
] {
|
||||
assert_eq!(message_phase(text, &channel_prefixes), Some(expected));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
message_phase("[ANALYSIS]old analysis prefix", &channel_prefixes),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
message_phase("[COMMENTARY]old commentary prefix", &channel_prefixes),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_client_prefixes_do_not_match_every_message() {
|
||||
let channel_prefixes = BTreeMap::from([("analysis".to_string(), vec![String::new()])]);
|
||||
|
||||
assert_eq!(message_phase("plain output", &channel_prefixes), None);
|
||||
assert_eq!(
|
||||
message_phase("[FINAL]finished", &channel_prefixes),
|
||||
Some(MessagePhase::FinalAnswer)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffers_streamed_text_until_the_bem_channel_is_complete() {
|
||||
let mut parser = ChannelParser::default();
|
||||
|
||||
assert_eq!(parser.push("<|start|>assistant<|channel|>com"), None);
|
||||
assert_eq!(parser.push("[COM"), None);
|
||||
assert_eq!(
|
||||
parser.push("mentary<|message|>progress"),
|
||||
Some("<|start|>assistant<|channel|>commentary<|message|>progress".to_string())
|
||||
parser.push("MENTARY]progress"),
|
||||
Some("[COMMENTARY]progress".to_string())
|
||||
);
|
||||
assert_eq!(parser.phase(), Some(MessagePhase::Commentary));
|
||||
assert_eq!(parser.push(" update"), Some(" update".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffers_a_client_prefix_until_the_streamed_header_is_complete() {
|
||||
let channel_prefixes = BTreeMap::from([(
|
||||
"commentary".to_string(),
|
||||
vec!["[PROGRESS]".to_string(), "[UPDATE]".to_string()],
|
||||
)]);
|
||||
let mut parser = ChannelParser::new(Arc::new(channel_prefixes));
|
||||
|
||||
assert_eq!(parser.push("[PRO"), None);
|
||||
assert_eq!(
|
||||
parser.push("GRESS]working"),
|
||||
Some("[PROGRESS]working".to_string())
|
||||
);
|
||||
assert_eq!(parser.phase(), Some(MessagePhase::Commentary));
|
||||
assert_eq!(parser.push("<|end|>"), Some("<|end|>".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -15,7 +15,10 @@ use codex_protocol::protocol::CodexResponseHandoffMode;
|
||||
use codex_protocol::protocol::RealtimeHandoffRequested;
|
||||
use codex_protocol::protocol::RealtimeTranscriptEntry;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[test]
|
||||
fn prefers_handoff_input_transcript_over_active_transcript() {
|
||||
@@ -147,15 +150,18 @@ fn wraps_realtime_delegation_input_with_xml_escaping_without_transcript() {
|
||||
#[tokio::test]
|
||||
async fn clears_active_handoff_explicitly() {
|
||||
let (tx, _rx) = bounded(1);
|
||||
let state = RealtimeHandoffState::new(
|
||||
tx,
|
||||
/*client_managed_handoffs*/ false,
|
||||
/*codex_responses_as_items*/ false,
|
||||
/*codex_response_item_prefix*/ None,
|
||||
CodexResponseHandoffMode::Thinking,
|
||||
RealtimeSessionKind::V1,
|
||||
/*event_parser*/ RealtimeEventParser::V1,
|
||||
);
|
||||
let state = RealtimeHandoffState {
|
||||
output_tx: tx,
|
||||
last_output: Arc::new(Mutex::new(None)),
|
||||
stream: Arc::new(Mutex::new(Default::default())),
|
||||
client_managed_handoffs: false,
|
||||
codex_responses_as_items: false,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: Arc::new(BTreeMap::new()),
|
||||
session_kind: RealtimeSessionKind::V1,
|
||||
event_parser: RealtimeEventParser::V1,
|
||||
};
|
||||
|
||||
state.stream.lock().await.active_handoff = Some("handoff_1".to_string());
|
||||
assert_eq!(
|
||||
|
||||
@@ -246,6 +246,7 @@ async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result<
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
|
||||
@@ -42,6 +42,7 @@ use core_test_support::wait_for_event_match;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
@@ -289,6 +290,7 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -438,6 +440,7 @@ async fn conversation_start_defaults_to_v2_and_gpt_realtime_1_5() -> Result<()>
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -528,6 +531,7 @@ async fn conversation_webrtc_frameless_chatgpt_sends_codex_headers_to_backend()
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: Some("session-override-model".to_string()),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: false,
|
||||
@@ -651,6 +655,7 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: Some("session-override-model".to_string()),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -841,6 +846,7 @@ async fn conversation_webrtc_start_uses_avas_query() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -942,6 +948,7 @@ async fn conversation_webrtc_default_v1_ignores_configured_v2_voice() -> Result<
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1005,6 +1012,7 @@ async fn conversation_webrtc_default_v1_rejects_explicit_v2_voice() -> Result<()
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1078,6 +1086,7 @@ async fn conversation_webrtc_start_uses_configured_call_base_url_for_avas() -> R
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1175,6 +1184,7 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1269,6 +1279,7 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1365,6 +1376,7 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() ->
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1459,6 +1471,7 @@ async fn assert_transport_close_tail_flush(
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1578,6 +1591,7 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1632,6 +1646,7 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1734,6 +1749,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1767,6 +1783,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1871,6 +1888,7 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -1943,6 +1961,7 @@ async fn conversation_uses_default_realtime_backend_prompt() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2023,6 +2042,7 @@ async fn conversation_uses_empty_instructions_for_null_or_empty_prompt() -> Resu
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2096,6 +2116,7 @@ async fn conversation_uses_explicit_start_voice() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2161,6 +2182,7 @@ async fn conversation_uses_configured_realtime_voice() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2214,6 +2236,7 @@ async fn conversation_rejects_voice_for_wrong_realtime_version() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2268,6 +2291,7 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() ->
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2348,6 +2372,7 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() -
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2422,6 +2447,7 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() ->
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2489,6 +2515,7 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2611,6 +2638,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2726,6 +2754,7 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<()
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2793,6 +2822,7 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() ->
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2881,6 +2911,7 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -2985,6 +3016,7 @@ async fn realtime_v2_noop_tool_call_returns_empty_function_output_without_respon
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3091,6 +3123,7 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3166,17 +3199,17 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re
|
||||
async fn conversation_flushes_assistant_deltas_every_200ms_for_v3_handoff() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let initial_commentary_text = "<|start|>assistant<|channel|>com";
|
||||
let first_commentary_delta = "mentary<|message|>seed first ";
|
||||
let second_commentary_delta = format!("{}<|end|>", "x".repeat(94));
|
||||
let initial_commentary_text = "[PRO";
|
||||
let first_commentary_delta = "GRESS]seed first ";
|
||||
let second_commentary_delta = "x".repeat(94);
|
||||
let commentary_text =
|
||||
format!("{initial_commentary_text}{first_commentary_delta}{second_commentary_delta}");
|
||||
let (gate_commentary_done_tx, gate_commentary_done_rx) = oneshot::channel();
|
||||
let commentary_item_added =
|
||||
responses::ev_message_item_added("msg_commentary", initial_commentary_text);
|
||||
let commentary_item_done = responses::ev_assistant_message("msg_commentary", &commentary_text);
|
||||
let initial_final_text = "<|start|>assistant<|channel|>fi";
|
||||
let final_delta = "nal<|message|>done<|end|>";
|
||||
let initial_final_text = "[DO";
|
||||
let final_delta = "NE]done";
|
||||
let final_text = format!("{initial_final_text}{final_delta}");
|
||||
let final_item_added = responses::ev_message_item_added("msg_final", initial_final_text);
|
||||
let final_item_done = responses::ev_assistant_message("msg_final", &final_text);
|
||||
@@ -3258,6 +3291,13 @@ async fn conversation_flushes_assistant_deltas_every_200ms_for_v3_handoff() -> R
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::BemTags,
|
||||
codex_response_handoff_channel_prefixes: Some(BTreeMap::from([
|
||||
(
|
||||
"commentary".to_string(),
|
||||
vec!["[PROGRESS]".to_string(), "[UPDATE]".to_string()],
|
||||
),
|
||||
("final".to_string(), vec!["[DONE]".to_string()]),
|
||||
])),
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3427,6 +3467,7 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() ->
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::BemTags,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3586,6 +3627,7 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3703,6 +3745,7 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3813,6 +3856,7 @@ async fn inbound_handoff_request_sends_transcript_delta_after_each_handoff() ->
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -3942,6 +3986,7 @@ async fn conversation_close_routes_only_remaining_transcript_tail_once() -> Resu
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -4032,6 +4077,7 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio(
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -4162,6 +4208,7 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -4322,6 +4369,7 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -4471,6 +4519,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
@@ -4631,6 +4680,7 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio()
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode:
|
||||
codex_protocol::protocol::CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
|
||||
@@ -177,6 +177,7 @@ fn start_params(version: RealtimeConversationVersion) -> ConversationStartParams
|
||||
codex_responses_as_items: false,
|
||||
codex_response_item_prefix: None,
|
||||
codex_response_handoff_mode: CodexResponseHandoffMode::Thinking,
|
||||
codex_response_handoff_channel_prefixes: None,
|
||||
model: None,
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
include_startup_context: true,
|
||||
|
||||
@@ -216,6 +216,8 @@ pub struct ConversationStartParams {
|
||||
/// Selects how automatic Codex handoffs are routed in Frameless Bidi sessions.
|
||||
/// Realtime V1 and V2 ignore this setting.
|
||||
pub codex_response_handoff_mode: CodexResponseHandoffMode,
|
||||
/// Optional client-selected BEM prefixes keyed by `analysis`, `commentary`, and `final`.
|
||||
pub codex_response_handoff_channel_prefixes: Option<BTreeMap<String, Vec<String>>>,
|
||||
/// Overrides the configured realtime model for this session only.
|
||||
pub model: Option<String>,
|
||||
/// Selects whether the realtime session should produce text or audio output.
|
||||
|
||||
Reference in New Issue
Block a user