mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
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:
@@ -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));
|
||||
}
|
||||
|
||||
-2
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user