mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
31bf1dbe63
commit
39c1bc1c68
7 files changed
+134
-3
No files matched your search
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(()));
|
||||||
|
|||||||
@@ -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()
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`).
|
||||||
Reference in new issue
Block a user