Move default realtime prompt into core (#17165)

- Adds a core-owned realtime backend prompt template and preparation
path.
- Makes omitted realtime start prompts use the core default, while null
or empty prompts intentionally send empty instructions.
- Covers the core realtime path and app-server v2 path with integration
coverage.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-08 19:34:40 -07:00
committed by GitHub
parent 36586eafed
commit 4c2a1ae31b
17 changed files with 491 additions and 59 deletions

View File

@@ -133,7 +133,13 @@ pub struct McpServerRefreshConfig {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ConversationStartParams {
pub prompt: String,
#[serde(
default,
deserialize_with = "conversation_start_prompt_serde::deserialize",
serialize_with = "conversation_start_prompt_serde::serialize",
skip_serializing_if = "Option::is_none"
)]
pub prompt: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -148,6 +154,28 @@ pub enum ConversationStartTransport {
Webrtc { sdp: String },
}
mod conversation_start_prompt_serde {
use serde::Deserializer;
use serde::Serializer;
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error>
where
D: Deserializer<'de>,
{
serde_with::rust::double_option::deserialize(deserializer)
}
pub(crate) fn serialize<S>(
value: &Option<Option<String>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serde_with::rust::double_option::serialize(value, serializer)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RealtimeAudioFrame {
pub data: String,
@@ -4410,12 +4438,12 @@ mod tests {
},
});
let start = Op::RealtimeConversationStart(ConversationStartParams {
prompt: "be helpful".to_string(),
prompt: Some(Some("be helpful".to_string())),
session_id: Some("conv_1".to_string()),
transport: None,
});
let webrtc_start = Op::RealtimeConversationStart(ConversationStartParams {
prompt: "be helpful".to_string(),
prompt: Some(Some("be helpful".to_string())),
session_id: Some("conv_1".to_string()),
transport: Some(ConversationStartTransport::Webrtc {
sdp: "v=offer\r\n".to_string(),
@@ -4425,6 +4453,16 @@ mod tests {
text: "hello".to_string(),
});
let close = Op::RealtimeConversationClose;
let default_prompt_start = Op::RealtimeConversationStart(ConversationStartParams {
prompt: None,
session_id: None,
transport: None,
});
let null_prompt_start = Op::RealtimeConversationStart(ConversationStartParams {
prompt: Some(None),
session_id: None,
transport: None,
});
assert_eq!(
serde_json::to_value(&start).unwrap(),
@@ -4434,6 +4472,34 @@ mod tests {
"session_id": "conv_1"
})
);
assert_eq!(
serde_json::to_value(&default_prompt_start).unwrap(),
json!({
"type": "realtime_conversation_start"
})
);
assert_eq!(
serde_json::to_value(&null_prompt_start).unwrap(),
json!({
"type": "realtime_conversation_start",
"prompt": null
})
);
assert_eq!(
serde_json::from_value::<Op>(json!({
"type": "realtime_conversation_start"
}))
.unwrap(),
default_prompt_start
);
assert_eq!(
serde_json::from_value::<Op>(json!({
"type": "realtime_conversation_start",
"prompt": null
}))
.unwrap(),
null_prompt_start
);
assert_eq!(
serde_json::to_value(&audio).unwrap(),
json!({