Allow custom Codex instructions for realtime transitions (#36408)

## What changed

- Add optional `realtimeStartInstructions` and `realtimeEndInstructions` fields to `thread/realtime/start`.
- Apply the instructions when Codex enters and leaves realtime mode, while preserving the existing defaults when either field is omitted.
- Limit each field to 8,192 estimated tokens.

## Testing

- Cover custom entry and exit instructions across realtime state transitions.
- Cover token-limit validation for both fields.

GitOrigin-RevId: b9cfa1ff22e084d8a6b2ed56bd27d95ffdd6b6bd
This commit is contained in:
guinness-oai
2026-07-31 23:51:33 +00:00
committed by copyberry
parent 64b2a3008e
commit 670f69416b
18 changed files with 395 additions and 33 deletions
@@ -5,13 +5,17 @@ use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RealtimeEndInstructions {
reason: String,
instructions: Option<String>,
}
impl RealtimeEndInstructions {
pub(crate) fn new(reason: impl Into<String>) -> Self {
pub(crate) fn new() -> Self {
Self { instructions: None }
}
pub(crate) fn with_instructions(instructions: impl Into<String>) -> Self {
Self {
reason: reason.into(),
instructions: Some(instructions.into()),
}
}
}
@@ -32,11 +36,11 @@ impl ContextualUserFragment for RealtimeEndInstructions {
)
}
fn matches_text(text: &str) -> bool {
text.contains(END_INSTRUCTIONS.trim())
}
fn body(&self) -> String {
format!("\n{}\n\nReason: {}\n", END_INSTRUCTIONS.trim(), self.reason)
let instructions = self
.instructions
.as_deref()
.unwrap_or_else(|| END_INSTRUCTIONS.trim());
format!("\n{instructions}\n")
}
}
@@ -12,6 +12,7 @@ use serde::Serialize;
pub(crate) struct RealtimeState {
snapshot: RealtimeSnapshot,
start_instructions: Option<String>,
end_instructions: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
@@ -20,10 +21,15 @@ pub(crate) struct RealtimeSnapshot {
}
impl RealtimeState {
pub(crate) fn new(active: bool, start_instructions: Option<&str>) -> Self {
pub(crate) fn new(
active: bool,
start_instructions: Option<&str>,
end_instructions: Option<&str>,
) -> Self {
Self {
snapshot: RealtimeSnapshot { active },
start_instructions: start_instructions.map(str::to_string),
end_instructions: end_instructions.map(str::to_string),
}
}
@@ -37,7 +43,12 @@ impl RealtimeState {
fn render_transition(&self, previous_active: bool) -> Option<Box<dyn ContextualUserFragment>> {
match (previous_active, self.snapshot.active) {
(false, true) => Some(self.render_start()),
(true, false) => Some(Box::new(RealtimeEndInstructions::new("inactive"))),
(true, false) => Some(match self.end_instructions.as_deref() {
Some(instructions) => {
Box::new(RealtimeEndInstructions::with_instructions(instructions))
}
None => Box::new(RealtimeEndInstructions::new()),
}),
(false, false) | (true, true) => None,
}
}
@@ -52,9 +63,7 @@ impl WorldStateSection for RealtimeState {
}
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
role == "developer"
&& RealtimeStartInstructions::matches_text(text)
&& !RealtimeEndInstructions::matches_text(text)
role == "developer" && RealtimeStartInstructions::matches_text(text)
}
fn has_retained_fragment_matcher() -> bool {
@@ -2,7 +2,7 @@ use super::super::test_support::render_section_cases;
use super::*;
fn state(active: bool, start_instructions: Option<&str>) -> RealtimeState {
RealtimeState::new(active, start_instructions)
RealtimeState::new(active, start_instructions, /*end_instructions*/ None)
}
#[test]
@@ -34,10 +34,10 @@ fn snapshots() {
}
#[test]
fn retained_fragment_matcher_only_matches_starts() {
fn retained_fragment_matcher_matches_realtime_fragments() {
let start = RealtimeStartWithInstructions::new("custom instructions").render();
let end = RealtimeEndInstructions::new("inactive").render();
let end = RealtimeEndInstructions::new().render();
assert!(RealtimeState::matches_legacy_fragment("developer", &start));
assert!(!RealtimeState::matches_legacy_fragment("developer", &end));
assert!(RealtimeState::matches_legacy_fragment("developer", &end));
}
@@ -50,8 +50,6 @@ None
Realtime conversation ended.
Subsequent user input will return to typed text rather than transcript-style text. Do not assume recognition errors or missing punctuation once realtime has ended. Resume normal chat behavior.
Reason: inactive
</realtime_conversation>
Unknown -> {"active":true} (role - developer)
+51 -3
View File
@@ -92,6 +92,7 @@ const REALTIME_STARTUP_CONTEXT_TOKEN_BUDGET: usize = 5_300;
const REALTIME_ASSISTANT_OUTPUT_TOKEN_BUDGET: usize = 1_000;
const REALTIME_INITIAL_ITEMS_MAX_COUNT: usize = 128;
const REALTIME_INITIAL_ITEMS_MAX_TOKENS: usize = 8_192;
const REALTIME_MODE_INSTRUCTIONS_MAX_TOKENS: usize = 8_192;
const HANDOFF_STREAM_FLUSH_INTERVAL: Duration = Duration::from_millis(200);
const HANDOFF_STREAM_TRUNCATION_MARKER: &str = "\n…output truncated…\n";
const AGENT_FINAL_MESSAGE_PREFIX: &str = "\"Agent Final Message\":\n\n";
@@ -122,6 +123,13 @@ enum RealtimeFanoutTaskStop {
pub(crate) struct RealtimeConversationManager {
state: Mutex<Option<ConversationState>>,
mode_instructions: Mutex<Option<RealtimeModeInstructions>>,
}
#[derive(Clone, Debug)]
pub(crate) struct RealtimeModeInstructions {
pub(crate) start: Option<String>,
pub(crate) end: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -484,9 +492,14 @@ impl RealtimeConversationManager {
pub(crate) fn new() -> Self {
Self {
state: Mutex::new(None),
mode_instructions: Mutex::new(None),
}
}
pub(crate) async fn mode_instructions(&self) -> Option<RealtimeModeInstructions> {
self.mode_instructions.lock().await.clone()
}
pub(crate) async fn running_state(&self) -> Option<()> {
let state = self.state.lock().await;
state
@@ -504,7 +517,11 @@ impl RealtimeConversationManager {
)
}
async fn start(&self, start: RealtimeStart) -> CodexResult<RealtimeStartOutput> {
async fn start(
&self,
start: RealtimeStart,
mode_instructions: RealtimeModeInstructions,
) -> CodexResult<RealtimeStartOutput> {
let previous_state = {
let mut guard = self.state.lock().await;
guard.take()
@@ -513,7 +530,9 @@ impl RealtimeConversationManager {
stop_conversation_state(state, RealtimeFanoutTaskStop::Await).await;
}
self.start_inner(start).await
let output = self.start_inner(start).await?;
*self.mode_instructions.lock().await = Some(mode_instructions);
Ok(output)
}
async fn start_inner(&self, start: RealtimeStart) -> CodexResult<RealtimeStartOutput> {
@@ -1104,6 +1123,8 @@ struct PreparedRealtimeConversationStart {
codex_response_item_prefix: Option<String>,
codex_response_handoff_mode: CodexResponseHandoffMode,
codex_response_handoff_channel_prefixes: Option<BTreeMap<String, Vec<String>>>,
realtime_start_instructions: Option<String>,
realtime_end_instructions: Option<String>,
realtime_call_api_provider: Option<ApiProvider>,
requested_realtime_session_id: Option<String>,
version: RealtimeWsVersion,
@@ -1198,6 +1219,8 @@ async fn prepare_realtime_start(
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_start_instructions: params.realtime_start_instructions,
realtime_end_instructions: params.realtime_end_instructions,
realtime_call_api_provider,
requested_realtime_session_id,
version,
@@ -1229,6 +1252,25 @@ pub(crate) async fn build_realtime_session_config(
version: RealtimeWsVersion,
configured_voice: ConfiguredRealtimeVoice,
) -> CodexResult<RealtimeSessionConfig> {
for (name, instructions) in [
(
"realtime start instructions",
params.realtime_start_instructions.as_deref(),
),
(
"realtime end instructions",
params.realtime_end_instructions.as_deref(),
),
] {
if instructions.is_some_and(|instructions| {
approx_token_count(instructions) > REALTIME_MODE_INSTRUCTIONS_MAX_TOKENS
}) {
return Err(CodexErr::InvalidRequest(format!(
"{name} must not exceed {REALTIME_MODE_INSTRUCTIONS_MAX_TOKENS} estimated tokens"
)));
}
}
let config = sess.get_config().await;
let prompt = prepare_realtime_backend_prompt(
params.prompt.clone(),
@@ -1398,6 +1440,8 @@ async fn handle_start_inner(
codex_response_item_prefix,
codex_response_handoff_mode,
codex_response_handoff_channel_prefixes,
realtime_start_instructions,
realtime_end_instructions,
realtime_call_api_provider,
requested_realtime_session_id,
version,
@@ -1409,6 +1453,10 @@ async fn handle_start_inner(
ConversationStartTransport::Websocket => None,
ConversationStartTransport::Webrtc { sdp } => Some(sdp),
};
let mode_instructions = RealtimeModeInstructions {
start: realtime_start_instructions,
end: realtime_end_instructions,
};
let start = RealtimeStart {
api_provider,
realtime_sideband_base_url,
@@ -1424,7 +1472,7 @@ async fn handle_start_inner(
model_client: sess.services.model_client.clone(),
sdp,
};
let start_output = sess.conversation.start(start).await?;
let start_output = sess.conversation.start(start, mode_instructions).await?;
info!("realtime conversation started");
+1 -1
View File
@@ -8808,7 +8808,7 @@ async fn record_context_updates_emits_realtime_end_when_session_stops_being_live
assert!(
developer_texts
.iter()
.any(|text| text.contains("Reason: inactive")),
.any(|text| text.contains("<realtime_conversation>")),
"expected a realtime end update, got {developer_texts:?}"
);
}
+11 -4
View File
@@ -96,12 +96,19 @@ impl Session {
{
world_state.add_section(ContextWindowGuidanceState::new(guidance));
}
let realtime_mode_instructions = self.conversation.mode_instructions().await;
world_state.add_section(RealtimeState::new(
turn_context.realtime_active,
turn_context
.config
.experimental_realtime_start_instructions
.as_deref(),
realtime_mode_instructions
.as_ref()
.and_then(|instructions| instructions.start.as_deref())
.or(turn_context
.config
.experimental_realtime_start_instructions
.as_deref()),
realtime_mode_instructions
.as_ref()
.and_then(|instructions| instructions.end.as_deref()),
));
world_state.add_section(AgentsMdState::new(step_context.loaded_agents_md.as_deref()));
if turn_context.config.include_permissions_instructions {
+3 -5
View File
@@ -253,6 +253,8 @@ async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result<
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -300,10 +302,6 @@ fn assert_request_contains_realtime_start(request: &responses::ResponsesRequest)
body.contains("<realtime_conversation>"),
"expected request to restate realtime instructions"
);
assert!(
!body.contains("Reason: inactive"),
"expected request to use realtime start instructions"
);
}
fn assert_request_contains_custom_realtime_start(
@@ -332,7 +330,7 @@ fn assert_request_contains_realtime_end(request: &responses::ResponsesRequest) {
"expected request to restate realtime instructions"
);
assert!(
body.contains("Reason: inactive"),
body.contains("Realtime conversation ended."),
"expected request to use realtime end instructions"
);
}
@@ -296,6 +296,8 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -446,6 +448,8 @@ async fn conversation_start_defaults_to_v2_and_gpt_realtime_1_5() -> Result<()>
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -537,6 +541,8 @@ async fn conversation_webrtc_frameless_chatgpt_sends_codex_headers_to_backend()
output_modality: RealtimeOutputModality::Audio,
include_startup_context: false,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -661,6 +667,8 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -852,6 +860,8 @@ async fn conversation_webrtc_start_uses_avas_query() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -954,6 +964,8 @@ async fn conversation_webrtc_default_v1_ignores_configured_v2_voice() -> Result<
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -1018,6 +1030,8 @@ async fn conversation_webrtc_default_v1_rejects_explicit_v2_voice() -> Result<()
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -1092,6 +1106,8 @@ async fn conversation_webrtc_start_uses_configured_call_base_url_for_avas() -> R
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -1203,6 +1219,8 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join(
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -1307,6 +1325,8 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: Some(ConversationStartTransport::Webrtc {
@@ -1404,6 +1424,8 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() ->
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -1499,6 +1521,8 @@ async fn assert_transport_close_tail_flush(
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -1619,6 +1643,8 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -1674,6 +1700,8 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -1777,6 +1805,8 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("old".to_string())),
realtime_session_id: Some("conv_old".to_string()),
transport: None,
@@ -1811,6 +1841,8 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("new".to_string())),
realtime_session_id: Some("conv_new".to_string()),
transport: None,
@@ -1916,6 +1948,8 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -1989,6 +2023,8 @@ async fn conversation_uses_default_realtime_backend_prompt() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: None,
realtime_session_id: None,
transport: None,
@@ -2070,6 +2106,8 @@ async fn conversation_uses_empty_instructions_for_null_or_empty_prompt() -> Resu
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt,
realtime_session_id: None,
transport: None,
@@ -2144,6 +2182,8 @@ async fn conversation_uses_explicit_start_voice() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2210,6 +2250,8 @@ async fn conversation_uses_configured_realtime_voice() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2264,6 +2306,8 @@ async fn conversation_rejects_voice_for_wrong_realtime_version() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2319,6 +2363,8 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() ->
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("prompt from op".to_string())),
realtime_session_id: None,
transport: None,
@@ -2400,6 +2446,8 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() -
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("prompt from op".to_string())),
realtime_session_id: None,
transport: None,
@@ -2475,6 +2523,8 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() ->
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("prompt from op".to_string())),
realtime_session_id: None,
transport: None,
@@ -2543,6 +2593,8 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2666,6 +2718,8 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2782,6 +2836,8 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<()
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2850,6 +2906,8 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() ->
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -2939,6 +2997,8 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3044,6 +3104,8 @@ async fn realtime_v2_noop_tool_call_returns_empty_function_output_without_respon
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3151,6 +3213,8 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3325,6 +3389,8 @@ async fn conversation_flushes_assistant_deltas_every_200ms_for_v3_handoff() -> R
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3495,6 +3561,8 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() ->
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3655,6 +3723,8 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3773,6 +3843,8 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -3884,6 +3956,8 @@ async fn inbound_handoff_request_sends_transcript_delta_after_each_handoff() ->
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -4014,6 +4088,8 @@ async fn conversation_close_routes_only_remaining_transcript_tail_once() -> Resu
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -4105,6 +4181,8 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio(
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -4236,6 +4314,8 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -4397,6 +4477,8 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -4547,6 +4629,8 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -4708,6 +4792,8 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio()
output_modality: RealtimeOutputModality::Audio,
include_startup_context: true,
initial_items: Vec::new(),
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,
@@ -150,6 +150,32 @@ async fn initial_items_enforce_aggregate_token_limit() -> Result<()> {
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn realtime_start_instructions_enforce_token_limit() -> Result<()> {
skip_if_no_network!(Ok(()));
let mut params = start_params(RealtimeConversationVersion::V3);
params.realtime_start_instructions = Some("x".repeat(8_192 * 4 + 1));
assert_start_error(
params,
"realtime start instructions must not exceed 8192 estimated tokens",
)
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn realtime_end_instructions_enforce_token_limit() -> Result<()> {
skip_if_no_network!(Ok(()));
let mut params = start_params(RealtimeConversationVersion::V3);
params.realtime_end_instructions = Some("x".repeat(8_192 * 4 + 1));
assert_start_error(
params,
"realtime end instructions must not exceed 8192 estimated tokens",
)
.await
}
async fn assert_start_error(params: ConversationStartParams, expected_error: &str) -> Result<()> {
let api_server = start_mock_server().await;
let test = test_codex().build_with_auto_env(&api_server).await?;
@@ -195,6 +221,8 @@ fn start_params(version: RealtimeConversationVersion) -> ConversationStartParams
role: ConversationTextRole::Assistant,
},
],
realtime_start_instructions: None,
realtime_end_instructions: None,
prompt: Some(Some("backend prompt".to_string())),
realtime_session_id: None,
transport: None,