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:
parent
31bf1dbe63
commit
39c1bc1c68
7 changed files with 134 additions and 3 deletions
|
|
@ -1788,6 +1788,10 @@
|
|||
"experimental_compact_prompt_file": {
|
||||
"$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": {
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -3965,6 +3965,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
|||
personality: Some(Personality::Pragmatic),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
experimental_realtime_ws_model: 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),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
experimental_realtime_ws_model: 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),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
experimental_realtime_ws_model: 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),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
experimental_realtime_ws_base_url: None,
|
||||
experimental_realtime_ws_model: 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]
|
||||
fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
|
|
|
|||
|
|
@ -470,6 +470,10 @@ pub struct Config {
|
|||
/// context appended to websocket session instructions. An empty string
|
||||
/// disables startup context injection entirely.
|
||||
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.
|
||||
pub forced_chatgpt_workspace_id: Option<String>,
|
||||
|
||||
|
|
@ -1241,6 +1245,10 @@ pub struct ConfigToml {
|
|||
/// context appended to websocket session instructions. An empty string
|
||||
/// disables startup context injection entirely.
|
||||
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>>,
|
||||
|
||||
/// 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_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
|
||||
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_login_method,
|
||||
include_apply_patch_tool: include_apply_patch_tool_flag,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,17 @@ pub(crate) fn build_realtime_update_item(
|
|||
next.realtime_active,
|
||||
) {
|
||||
(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,
|
||||
(None, false) => previous_turn_settings
|
||||
.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) {
|
||||
let body = request.body_json().to_string();
|
||||
assert!(
|
||||
|
|
@ -1518,6 +1537,53 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
|
|||
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)]
|
||||
async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
|
|
|||
|
|
@ -520,9 +520,12 @@ impl DeveloperInstructions {
|
|||
}
|
||||
|
||||
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!(
|
||||
"{REALTIME_CONVERSATION_OPEN_TAG}\n{}\n{REALTIME_CONVERSATION_CLOSE_TAG}",
|
||||
REALTIME_START_INSTRUCTIONS.trim()
|
||||
"{REALTIME_CONVERSATION_OPEN_TAG}\n{instructions}\n{REALTIME_CONVERSATION_CLOSE_TAG}"
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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`).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue