diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json
index 0403479949..fd463f4af1 100644
--- a/codex-rs/core/config.schema.json
+++ b/codex-rs/core/config.schema.json
@@ -570,6 +570,9 @@
"image_generation": {
"type": "boolean"
},
+ "image_resize_notice": {
+ "type": "boolean"
+ },
"imagegenext": {
"type": "boolean"
},
@@ -5198,6 +5201,9 @@
"image_generation": {
"type": "boolean"
},
+ "image_resize_notice": {
+ "type": "boolean"
+ },
"imagegenext": {
"type": "boolean"
},
diff --git a/codex-rs/core/src/compact_remote_history.rs b/codex-rs/core/src/compact_remote_history.rs
index 15706cd69d..39f3aa0212 100644
--- a/codex-rs/core/src/compact_remote_history.rs
+++ b/codex-rs/core/src/compact_remote_history.rs
@@ -1,12 +1,11 @@
use std::borrow::Borrow;
+use crate::context::ContextualUserFragment;
+use crate::context::ImageResizeNotice;
use crate::context_manager::estimate_item_token_count;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
-const ATTACHED_NOTICE_MARKERS: &[(&str, &str)] =
- &[("", "")];
-
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct HistoryItemGroup {
pub(crate) source: T,
@@ -51,9 +50,7 @@ fn is_attached_notice(notice: &ResponseItem) -> bool {
&& matches!(
content.as_slice(),
[ContentItem::InputText { text }]
- if ATTACHED_NOTICE_MARKERS.iter().any(|(opening, closing)| {
- text.starts_with(opening) && text.ends_with(closing)
- })
+ if ImageResizeNotice::matches_text(text)
)
)
}
diff --git a/codex-rs/core/src/context/image_resize_notice.rs b/codex-rs/core/src/context/image_resize_notice.rs
new file mode 100644
index 0000000000..e9248aa1b9
--- /dev/null
+++ b/codex-rs/core/src/context/image_resize_notice.rs
@@ -0,0 +1,74 @@
+use super::ContextualUserFragment;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum ImageResizeNoticeSource {
+ UserMessage,
+ ToolOutput,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct ResizedImage {
+ pub(crate) image_number: usize,
+ pub(crate) image_count: usize,
+ pub(crate) source_width: u32,
+ pub(crate) source_height: u32,
+ pub(crate) prepared_width: u32,
+ pub(crate) prepared_height: u32,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct ImageResizeNotice {
+ source: ImageResizeNoticeSource,
+ resized_images: Vec,
+}
+
+impl ImageResizeNotice {
+ pub(crate) fn new(source: ImageResizeNoticeSource, resized_images: Vec) -> Self {
+ Self {
+ source,
+ resized_images,
+ }
+ }
+}
+
+impl ContextualUserFragment for ImageResizeNotice {
+ fn role(&self) -> &'static str {
+ "developer"
+ }
+
+ fn requires_separate_message(&self) -> bool {
+ true
+ }
+
+ fn markers(&self) -> (&'static str, &'static str) {
+ Self::type_markers()
+ }
+
+ fn type_markers() -> (&'static str, &'static str) {
+ ("", "")
+ }
+
+ fn body(&self) -> String {
+ let source = match self.source {
+ ImageResizeNoticeSource::UserMessage => "user message",
+ ImageResizeNoticeSource::ToolOutput => "tool output",
+ };
+ let notices = self
+ .resized_images
+ .iter()
+ .map(|image| {
+ format!(
+ "Image {} of {} in the preceding {source} was resized from {}x{} to {}x{} pixels.",
+ image.image_number,
+ image.image_count,
+ image.source_width,
+ image.source_height,
+ image.prepared_width,
+ image.prepared_height,
+ )
+ })
+ .collect::>()
+ .join("\n");
+ format!("\n{notices}\n")
+ }
+}
diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs
index 2e69331475..0398d21a1d 100644
--- a/codex-rs/core/src/context/mod.rs
+++ b/codex-rs/core/src/context/mod.rs
@@ -9,6 +9,7 @@ mod environment_context;
mod environments_instructions;
mod guardian_followup_review_reminder;
mod hook_additional_context;
+mod image_resize_notice;
mod inter_agent_completion_message;
mod inter_agent_message;
mod internal_model_context;
@@ -48,6 +49,9 @@ pub(crate) use current_time_reminder::CurrentTimeReminder;
pub(crate) use environments_instructions::EnvironmentsInstructions;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
+pub(crate) use image_resize_notice::ImageResizeNotice;
+pub(crate) use image_resize_notice::ImageResizeNoticeSource;
+pub(crate) use image_resize_notice::ResizedImage;
pub(crate) use inter_agent_completion_message::InterAgentCompletionMessage;
pub(crate) use inter_agent_message::InterAgentMessage;
pub(crate) use inter_agent_message::InterAgentMessageType;
diff --git a/codex-rs/core/src/image_preparation.rs b/codex-rs/core/src/image_preparation.rs
index 4b19c7e053..36f617b129 100644
--- a/codex-rs/core/src/image_preparation.rs
+++ b/codex-rs/core/src/image_preparation.rs
@@ -1,3 +1,7 @@
+use crate::context::ContextualUserFragment;
+use crate::context::ImageResizeNotice;
+use crate::context::ImageResizeNoticeSource;
+use crate::context::ResizedImage;
use codex_analytics::ImageDetailSetting;
use codex_analytics::ImagePreparationMetadata;
use codex_protocol::models::ContentItem;
@@ -33,6 +37,20 @@ struct ImageOrigin<'a> {
item_id: Option<&'a str>,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum ImageResizeNoticeMode {
+ Disabled,
+ Enabled,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct PreparedImageResize {
+ source_width: u32,
+ source_height: u32,
+ prepared_width: u32,
+ prepared_height: u32,
+}
+
#[derive(Debug, thiserror::Error)]
enum ImagePreparationError {
#[error("remote image URLs are not supported")]
@@ -56,37 +74,51 @@ impl ImagePreparationError {
}
}
-pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) -> Vec {
+pub(crate) fn prepare_response_items(
+ items: &mut Vec,
+ resize_notice_mode: ImageResizeNoticeMode,
+) -> Vec {
let mut metadata = Vec::new();
- for item in items {
- match item {
+ let mut prepared_items = Vec::with_capacity(items.len());
+ for mut item in std::mem::take(items) {
+ let resize_notice = match &mut item {
ResponseItem::Message { role, content, .. } => {
- prepare_message_content(
+ let resized_images = prepare_message_content(
content,
ImageOrigin {
message_role: Some(role),
item_id: None,
},
+ if role == "user" {
+ resize_notice_mode
+ } else {
+ ImageResizeNoticeMode::Disabled
+ },
&mut metadata,
);
+ (!resized_images.is_empty()).then(|| {
+ ImageResizeNotice::new(ImageResizeNoticeSource::UserMessage, resized_images)
+ })
}
ResponseItem::FunctionCallOutput {
call_id, output, ..
}
| ResponseItem::CustomToolCallOutput {
call_id, output, ..
- } => {
- if let Some(content) = output.content_items_mut() {
- prepare_tool_output_content(
- content,
- ImageOrigin {
- message_role: None,
- item_id: Some(call_id),
- },
- &mut metadata,
- );
- }
- }
+ } => output.content_items_mut().and_then(|content| {
+ let resized_images = prepare_tool_output_content(
+ content,
+ ImageOrigin {
+ message_role: None,
+ item_id: Some(call_id),
+ },
+ resize_notice_mode,
+ &mut metadata,
+ );
+ (!resized_images.is_empty()).then(|| {
+ ImageResizeNotice::new(ImageResizeNoticeSource::ToolOutput, resized_images)
+ })
+ }),
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::AgentMessage { .. }
@@ -100,44 +132,93 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) -> Vec {}
+ | ResponseItem::Other => None,
+ };
+ prepared_items.push(item);
+ if let Some(resize_notice) = resize_notice {
+ prepared_items.push(ContextualUserFragment::into(resize_notice));
}
}
+ *items = prepared_items;
metadata
}
fn prepare_message_content(
items: &mut [ContentItem],
origin: ImageOrigin<'_>,
+ resize_notice_mode: ImageResizeNoticeMode,
metadata: &mut Vec,
-) {
+) -> Vec {
+ let image_count = items
+ .iter()
+ .filter(|item| matches!(item, ContentItem::InputImage { .. }))
+ .count();
+ let mut image_number = 0;
+ let mut resized_images = Vec::new();
for item in items {
- if let ContentItem::InputImage { image_url, detail } = item
- && let Err(error) = prepare_image(image_url, *detail, origin, metadata)
- {
- warn!(%error, "failed to prepare message image");
- *item = ContentItem::InputText {
- text: error.placeholder().to_string(),
- };
+ if let ContentItem::InputImage { image_url, detail } = item {
+ image_number += 1;
+ match prepare_image(image_url, *detail, origin, metadata) {
+ Ok(Some(resize)) if resize_notice_mode == ImageResizeNoticeMode::Enabled => {
+ resized_images.push(ResizedImage {
+ image_number,
+ image_count,
+ source_width: resize.source_width,
+ source_height: resize.source_height,
+ prepared_width: resize.prepared_width,
+ prepared_height: resize.prepared_height,
+ });
+ }
+ Ok(_) => {}
+ Err(error) => {
+ warn!(%error, "failed to prepare message image");
+ *item = ContentItem::InputText {
+ text: error.placeholder().to_string(),
+ };
+ }
+ }
}
}
+ resized_images
}
fn prepare_tool_output_content(
items: &mut [FunctionCallOutputContentItem],
origin: ImageOrigin<'_>,
+ resize_notice_mode: ImageResizeNoticeMode,
metadata: &mut Vec,
-) {
+) -> Vec {
+ let image_count = items
+ .iter()
+ .filter(|item| matches!(item, FunctionCallOutputContentItem::InputImage { .. }))
+ .count();
+ let mut image_number = 0;
+ let mut resized_images = Vec::new();
for item in items {
- if let FunctionCallOutputContentItem::InputImage { image_url, detail } = item
- && let Err(error) = prepare_image(image_url, *detail, origin, metadata)
- {
- warn!(%error, "failed to prepare tool output image");
- *item = FunctionCallOutputContentItem::InputText {
- text: error.placeholder().to_string(),
- };
+ if let FunctionCallOutputContentItem::InputImage { image_url, detail } = item {
+ image_number += 1;
+ match prepare_image(image_url, *detail, origin, metadata) {
+ Ok(Some(resize)) if resize_notice_mode == ImageResizeNoticeMode::Enabled => {
+ resized_images.push(ResizedImage {
+ image_number,
+ image_count,
+ source_width: resize.source_width,
+ source_height: resize.source_height,
+ prepared_width: resize.prepared_width,
+ prepared_height: resize.prepared_height,
+ });
+ }
+ Ok(_) => {}
+ Err(error) => {
+ warn!(%error, "failed to prepare tool output image");
+ *item = FunctionCallOutputContentItem::InputText {
+ text: error.placeholder().to_string(),
+ };
+ }
+ }
}
}
+ resized_images
}
fn is_remote_image_url(image_url: &str) -> bool {
@@ -157,12 +238,12 @@ fn prepare_image(
detail: Option,
origin: ImageOrigin<'_>,
metadata: &mut Vec,
-) -> Result<(), ImagePreparationError> {
+) -> Result