mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## What changed - Accept `fileId` alongside the existing `url` form for app-server image inputs, and forward file references to the Responses API as `file_id`. Update generated schemas and client types. - Preserve file references, image detail hints, and mixed inline/file image ordering through user-message events, thread history, and rollout migration. Retain file images when truncating tool output. - Pass file references through image preparation without resolving them, while keeping resize-notice numbering correct. Omit them from unsupported TUI display and Guardian image context. - Reject image-edit requests whose recent-image window includes a file reference, preventing selection of an older inline image instead. ## Testing Add coverage for serialization, request and rollout preservation, mixed-image history ordering, incomplete ordering metadata, tool-output truncation, and rejection of unsupported image-edit selections. GitOrigin-RevId: 6ca20a8577155cc934b720803c3b7b3bffdf972a
97 lines
3.3 KiB
Rust
97 lines
3.3 KiB
Rust
use std::io;
|
|
use std::io::Read;
|
|
use std::path::Path;
|
|
|
|
use codex_utils_image::MAX_PROMPT_IMAGE_INPUT_BYTES;
|
|
use codex_utils_image::PromptImageMode;
|
|
use codex_utils_image::data_url_from_bytes;
|
|
use codex_utils_image::load_for_prompt_bytes;
|
|
|
|
use crate::models::ImageDetail;
|
|
use crate::models::ImageReference;
|
|
use crate::user_input::UserInput;
|
|
|
|
/// Maximum accepted decoded byte length for prompt audio inputs.
|
|
///
|
|
/// This matches the Responses API audio input limit.
|
|
pub const MAX_PROMPT_AUDIO_INPUT_BYTES: usize = 50 * 1024 * 1024;
|
|
|
|
/// Snapshots local image and audio input into portable, validated data URLs.
|
|
pub fn snapshot_local_user_input(input: &mut UserInput) -> io::Result<()> {
|
|
match input {
|
|
UserInput::LocalImage { path, detail } => {
|
|
let image_detail = *detail;
|
|
let mode = match image_detail {
|
|
Some(ImageDetail::Original) => PromptImageMode::Original,
|
|
Some(ImageDetail::Auto | ImageDetail::Low | ImageDetail::High) | None => {
|
|
PromptImageMode::ResizeToFit
|
|
}
|
|
};
|
|
let file_bytes = read_bounded_local_media(path, MAX_PROMPT_IMAGE_INPUT_BYTES, "image")?;
|
|
let image = load_for_prompt_bytes(path, file_bytes, mode).map_err(io::Error::other)?;
|
|
*input = UserInput::Image {
|
|
image: ImageReference::Inline {
|
|
image_url: image.into_data_url(),
|
|
},
|
|
detail: image_detail,
|
|
};
|
|
}
|
|
UserInput::LocalAudio { path } => {
|
|
let mime = audio_mime_for_path(path).ok_or_else(|| {
|
|
io::Error::new(io::ErrorKind::InvalidData, "unsupported audio format")
|
|
})?;
|
|
let file_bytes = read_bounded_local_media(path, MAX_PROMPT_AUDIO_INPUT_BYTES, "audio")?;
|
|
*input = UserInput::Audio {
|
|
audio_url: data_url_from_bytes(mime, &file_bytes),
|
|
};
|
|
}
|
|
UserInput::Text { .. }
|
|
| UserInput::Image { .. }
|
|
| UserInput::Audio { .. }
|
|
| UserInput::Skill { .. }
|
|
| UserInput::Mention { .. } => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn read_bounded_local_media(path: &Path, max_bytes: usize, kind: &str) -> io::Result<Vec<u8>> {
|
|
let file = std::fs::File::open(path)?;
|
|
if file.metadata()?.len() > max_bytes as u64 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("{kind} input exceeds {max_bytes} bytes"),
|
|
));
|
|
}
|
|
|
|
let mut bytes = Vec::new();
|
|
file.take(max_bytes as u64 + 1).read_to_end(&mut bytes)?;
|
|
if bytes.len() > max_bytes {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("{kind} input exceeds {max_bytes} bytes"),
|
|
));
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
pub(crate) fn audio_mime_for_path(path: &Path) -> Option<&'static str> {
|
|
let extension = path.extension()?.to_str()?;
|
|
if extension.eq_ignore_ascii_case("wav") {
|
|
Some("audio/wav")
|
|
} else if extension.eq_ignore_ascii_case("mp3") {
|
|
Some("audio/mpeg")
|
|
} else if extension.eq_ignore_ascii_case("m4a") {
|
|
Some("audio/mp4")
|
|
} else if extension.eq_ignore_ascii_case("webm") {
|
|
Some("audio/webm")
|
|
} else if extension.eq_ignore_ascii_case("ogg") {
|
|
Some("audio/ogg")
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "local_media_tests.rs"]
|
|
mod tests;
|