Files
codex/codex-rs/core/src/realtime_conversation/bem.rs
T
jiayuhuang-openai 4ebd976312 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
2026-07-22 19:43:35 +00:00

72 lines
2.0 KiB
Rust

use codex_protocol::models::MessagePhase;
use std::collections::BTreeMap;
use std::sync::Arc;
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.
///
/// Once the channel is known, the original envelope is released unchanged so
/// 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.channel_prefixes);
self.phase.as_ref()?;
Some(std::mem::take(&mut self.buffered_text))
}
pub(super) fn phase(&self) -> Option<MessagePhase> {
self.phase.clone()
}
pub(super) fn finish(&mut self) -> String {
std::mem::take(&mut self.buffered_text)
}
}
#[cfg(test)]
#[path = "bem_tests.rs"]
mod tests;