Gate audio history by model input modalities (#33982)

## What changed

- Add `audio` to the model input modalities exposed by the protocol and generated app-server schemas.
- Preserve audio in prompts for models that advertise audio input, and replace historical audio with an omission marker for models that do not.

## Testing

- Cover prompt normalization with and without audio support.
- Extend the model-switching integration test to verify that switching from a multimodal model to a text-only model strips prior image and audio content.

GitOrigin-RevId: c18e47ad602720216303ff9abb5bca4d508391ee
This commit is contained in:
nhamidi-oai
2026-07-18 09:13:38 +00:00
committed by copyberry
parent 5c0e582c59
commit 56395bddaf
11 changed files with 146 additions and 20 deletions
+6 -4
View File
@@ -135,9 +135,8 @@ impl ContextManager {
}
/// Returns the history prepared for sending to the model. This applies a proper
/// normalization and drops un-suited items. When `input_modalities` does not
/// include `InputModality::Image`, images are stripped from messages and tool
/// outputs.
/// normalization and drops un-suited items. Unsupported image and audio content
/// is stripped from messages and tool outputs according to `input_modalities`.
pub(crate) fn for_prompt(mut self, input_modalities: &[InputModality]) -> Vec<ResponseItem> {
self.normalize_history(input_modalities);
self.items
@@ -355,7 +354,7 @@ impl ContextManager {
/// This function enforces a couple of invariants on the in-memory history:
/// 1. every call (function/custom) has a corresponding output entry
/// 2. every output has a corresponding call entry
/// 3. when images are unsupported, image content is stripped from messages and tool outputs
/// 3. unsupported image and audio content is stripped from messages and tool outputs
fn normalize_history(&mut self, input_modalities: &[InputModality]) {
// all function/tool calls must have a corresponding output
normalize::ensure_call_outputs_present(&mut self.items);
@@ -365,6 +364,9 @@ impl ContextManager {
// strip images when model does not support them
normalize::strip_images_when_unsupported(input_modalities, &mut self.items);
// strip audio when model does not support it
normalize::strip_audio_when_unsupported(input_modalities, &mut self.items);
}
fn process_item(&self, item: &ResponseItem, policy: TruncationPolicy) -> ResponseItem {
@@ -471,7 +471,7 @@ fn total_token_usage_includes_all_items_after_last_model_generated_item() {
}
#[test]
fn for_prompt_strips_images_when_model_does_not_support_images() {
fn for_prompt_strips_media_when_model_does_not_support_it() {
let items = vec![
ResponseItem::Message {
id: None,
@@ -484,6 +484,9 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
image_url: "https://example.com/img.png".to_string(),
detail: Some(DEFAULT_IMAGE_DETAIL),
},
ContentItem::InputAudio {
audio_url: "data:audio/wav;base64,YXVkaW8=".to_string(),
},
ContentItem::InputText {
text: "caption".to_string(),
},
@@ -554,6 +557,10 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
text: "image content omitted because you do not support image input"
.to_string(),
},
ContentItem::InputText {
text: "audio content omitted because you do not support audio input"
.to_string(),
},
ContentItem::InputText {
text: "caption".to_string(),
},
@@ -635,6 +642,21 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
} else {
panic!("expected Message");
}
let audio_message = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputAudio {
audio_url: "data:audio/wav;base64,YXVkaW8=".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
};
let with_audio = create_history_with_items(vec![audio_message.clone()]);
assert_eq!(
with_audio.for_prompt(&[InputModality::Text, InputModality::Audio]),
vec![audio_message]
);
}
#[test]
@@ -12,6 +12,8 @@ use tracing::info;
const IMAGE_CONTENT_OMITTED_PLACEHOLDER: &str =
"image content omitted because you do not support image input";
const AUDIO_CONTENT_OMITTED_PLACEHOLDER: &str =
"audio content omitted because you do not support audio input";
// Changing this value would change model-visible IDs and invalidate prompt caches.
const SYNTHETIC_OUTPUT_ID_NAMESPACE: Uuid = Uuid::from_u128(0x90d38d3e_6a5b_4d52_bfe2_2f1e634bfac4);
@@ -366,3 +368,26 @@ pub(crate) fn strip_images_when_unsupported(
}
}
}
/// Strip audio content from messages when the model does not support audio.
/// When `input_modalities` contains `InputModality::Audio`, no stripping is performed.
pub(crate) fn strip_audio_when_unsupported(
input_modalities: &[InputModality],
items: &mut [ResponseItem],
) {
if input_modalities.contains(&InputModality::Audio) {
return;
}
for item in items.iter_mut() {
if let ResponseItem::Message { content, .. } = item {
for content_item in content.iter_mut() {
if matches!(content_item, ContentItem::InputAudio { .. }) {
*content_item = ContentItem::InputText {
text: AUDIO_CONTENT_OMITTED_PLACEHOLDER.to_string(),
};
}
}
}
}
}