Add realtime start instructions config override (#14270)

- add `realtime_start_instructions` config support
- thread it into realtime context updates, schema, docs, and tests
This commit is contained in:
Ahmed Ibrahim authored and Michael Bolin committed 2026-03-11 12:33:09 -07:00
1 parent 31bf1dbe63
commit 39c1bc1c68
7 files changed
+134 -3

No files matched your search

+4
View File
@@ -1788,6 +1788,10 @@
"experimental_compact_prompt_file": { "experimental_compact_prompt_file": {
"$ref": "#/definitions/AbsolutePathBuf" "$ref": "#/definitions/AbsolutePathBuf"
}, },
"experimental_realtime_start_instructions": {
"description": "Experimental / do not use. Replaces the built-in realtime start instructions inserted into developer messages when realtime becomes active.",
"type": "string"
},
"experimental_realtime_ws_backend_prompt": { "experimental_realtime_ws_backend_prompt": {
"description": "Experimental / do not use. Overrides only the realtime conversation websocket transport instructions (the `Op::RealtimeConversation` `/ws` session.update instructions) without changing normal prompts.", "description": "Experimental / do not use. Overrides only the realtime conversation websocket transport instructions (the `Op::RealtimeConversation` `/ws` session.update instructions) without changing normal prompts.",
"type": "string" "type": "string"
+32
View File
@@ -3965,6 +3965,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(), realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_start_instructions: None,
experimental_realtime_ws_base_url: None, experimental_realtime_ws_base_url: None,
experimental_realtime_ws_model: None, experimental_realtime_ws_model: None,
experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_backend_prompt: None,
@@ -4100,6 +4101,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(), realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_start_instructions: None,
experimental_realtime_ws_base_url: None, experimental_realtime_ws_base_url: None,
experimental_realtime_ws_model: None, experimental_realtime_ws_model: None,
experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_backend_prompt: None,
@@ -4233,6 +4235,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(), realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_start_instructions: None,
experimental_realtime_ws_base_url: None, experimental_realtime_ws_base_url: None,
experimental_realtime_ws_model: None, experimental_realtime_ws_model: None,
experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_backend_prompt: None,
@@ -4352,6 +4355,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(), realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_start_instructions: None,
experimental_realtime_ws_base_url: None, experimental_realtime_ws_base_url: None,
experimental_realtime_ws_model: None, experimental_realtime_ws_model: None,
experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_backend_prompt: None,
@@ -5261,6 +5265,34 @@ async fn feature_requirements_reject_legacy_aliases() {
); );
} }
#[test]
fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> {
let cfg: ConfigToml = toml::from_str(
r#"
experimental_realtime_start_instructions = "start instructions from config"
"#,
)
.expect("TOML deserialization should succeed");
assert_eq!(
cfg.experimental_realtime_start_instructions.as_deref(),
Some("start instructions from config")
);
let codex_home = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)?;
assert_eq!(
config.experimental_realtime_start_instructions.as_deref(),
Some("start instructions from config")
);
Ok(())
}
#[test] #[test]
fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> { fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> {
let cfg: ConfigToml = toml::from_str( let cfg: ConfigToml = toml::from_str(
+9
View File
@@ -470,6 +470,10 @@ pub struct Config {
/// context appended to websocket session instructions. An empty string /// context appended to websocket session instructions. An empty string
/// disables startup context injection entirely. /// disables startup context injection entirely.
pub experimental_realtime_ws_startup_context: Option<String>, pub experimental_realtime_ws_startup_context: Option<String>,
/// Experimental / do not use. Replaces the built-in realtime start
/// instructions inserted into developer messages when realtime becomes
/// active.
pub experimental_realtime_start_instructions: Option<String>,
/// When set, restricts ChatGPT login to a specific workspace identifier. /// When set, restricts ChatGPT login to a specific workspace identifier.
pub forced_chatgpt_workspace_id: Option<String>, pub forced_chatgpt_workspace_id: Option<String>,
@@ -1241,6 +1245,10 @@ pub struct ConfigToml {
/// context appended to websocket session instructions. An empty string /// context appended to websocket session instructions. An empty string
/// disables startup context injection entirely. /// disables startup context injection entirely.
pub experimental_realtime_ws_startup_context: Option<String>, pub experimental_realtime_ws_startup_context: Option<String>,
/// Experimental / do not use. Replaces the built-in realtime start
/// instructions inserted into developer messages when realtime becomes
/// active.
pub experimental_realtime_start_instructions: Option<String>,
pub projects: Option<HashMap<String, ProjectConfig>>, pub projects: Option<HashMap<String, ProjectConfig>>,
/// Controls the web search tool mode: disabled, cached, or live. /// Controls the web search tool mode: disabled, cached, or live.
@@ -2426,6 +2434,7 @@ impl Config {
experimental_realtime_ws_model: cfg.experimental_realtime_ws_model, experimental_realtime_ws_model: cfg.experimental_realtime_ws_model,
experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt, experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context, experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
forced_chatgpt_workspace_id, forced_chatgpt_workspace_id,
forced_login_method, forced_login_method,
include_apply_patch_tool: include_apply_patch_tool_flag, include_apply_patch_tool: include_apply_patch_tool_flag,
+11 -1
View File
@@ -75,7 +75,17 @@ pub(crate) fn build_realtime_update_item(
next.realtime_active, next.realtime_active,
) { ) {
(Some(true), false) => Some(DeveloperInstructions::realtime_end_message("inactive")), (Some(true), false) => Some(DeveloperInstructions::realtime_end_message("inactive")),
(Some(false), true) | (None, true) => Some(DeveloperInstructions::realtime_start_message()), (Some(false), true) | (None, true) => Some(
if let Some(instructions) = next
.config
.experimental_realtime_start_instructions
.as_deref()
{
DeveloperInstructions::realtime_start_message_with_instructions(instructions)
} else {
DeveloperInstructions::realtime_start_message()
},
),
(Some(true), true) | (Some(false), false) => None, (Some(true), true) | (Some(false), false) => None,
(None, false) => previous_turn_settings (None, false) => previous_turn_settings
.and_then(|settings| settings.realtime_active) .and_then(|settings| settings.realtime_active)
@@ -161,6 +161,25 @@ fn assert_request_contains_realtime_start(request: &responses::ResponsesRequest)
); );
} }
fn assert_request_contains_custom_realtime_start(
request: &responses::ResponsesRequest,
instructions: &str,
) {
let body = request.body_json().to_string();
assert!(
body.contains("<realtime_conversation>"),
"expected request to preserve the realtime wrapper"
);
assert!(
body.contains(instructions),
"expected request to use custom realtime start instructions"
);
assert!(
!body.contains("Realtime conversation started."),
"expected request to replace the default realtime start instructions"
);
}
fn assert_request_contains_realtime_end(request: &responses::ResponsesRequest) { fn assert_request_contains_realtime_end(request: &responses::ResponsesRequest) {
let body = request.body_json().to_string(); let body = request.body_json().to_string();
assert!( assert!(
@@ -1518,6 +1537,53 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_request_uses_custom_experimental_realtime_start_instructions() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = wiremock::MockServer::start().await;
let realtime_server = start_remote_realtime_server().await;
let custom_instructions = "custom realtime start instructions";
let mut builder = remote_realtime_test_codex_builder(&realtime_server).with_config({
let custom_instructions = custom_instructions.to_string();
move |config| {
config.experimental_realtime_start_instructions = Some(custom_instructions);
}
});
let test = builder.build(&server).await?;
let responses_mock = responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_assistant_message("m1", "REMOTE_FIRST_REPLY"),
responses::ev_completed("r1"),
]),
)
.await;
start_realtime_conversation(test.codex.as_ref()).await?;
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
assert_request_contains_custom_realtime_start(
&responses_mock.single_request(),
custom_instructions,
);
close_realtime_conversation(test.codex.as_ref()).await?;
realtime_server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end() -> Result<()> { async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end() -> Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
+5 -2
View File
@@ -520,9 +520,12 @@ impl DeveloperInstructions {
} }
pub fn realtime_start_message() -> Self { pub fn realtime_start_message() -> Self {
Self::realtime_start_message_with_instructions(REALTIME_START_INSTRUCTIONS.trim())
}
pub fn realtime_start_message_with_instructions(instructions: &str) -> Self {
DeveloperInstructions::new(format!( DeveloperInstructions::new(format!(
"{REALTIME_CONVERSATION_OPEN_TAG}\n{}\n{REALTIME_CONVERSATION_CLOSE_TAG}", "{REALTIME_CONVERSATION_OPEN_TAG}\n{instructions}\n{REALTIME_CONVERSATION_CLOSE_TAG}"
REALTIME_START_INSTRUCTIONS.trim()
)) ))
} }
+7
View File
@@ -49,4 +49,11 @@ Plan preset. The string value `none` means "no reasoning" (an explicit Plan
override), not "inherit the global default". There is currently no separate override), not "inherit the global default". There is currently no separate
config value for "follow the global default in Plan mode". config value for "follow the global default in Plan mode".
## Realtime start instructions
`experimental_realtime_start_instructions` lets you replace the built-in
developer message Codex inserts when realtime becomes active. It only affects
the realtime start message in prompt history and does not change websocket
backend prompt settings or the realtime end/inactive message.
Ctrl+C/Ctrl+D quitting uses a ~1 second double-press hint (`ctrl + c again to quit`). Ctrl+C/Ctrl+D quitting uses a ~1 second double-press hint (`ctrl + c again to quit`).