remove flag for image preparation (#29429)

## What

- make Fjord's centralized response-item image preparation unconditional
for new and resumed history
- have local user images and `view_image` outputs always defer decoding
and resizing to that path
- retain `resize_all_images` as an ignored, removed compatibility key
for released clients
- delete the flag-off producer paths and obsolete policy-specific tests

## Why

Centralized preparation is now the intended image path. Keeping the
runtime feature checks also kept two image-processing implementations
alive and allowed client config to select the legacy behavior.

This is a clean replacement for #28975, rebuilt from the latest `main`.

## How

`prepare_response_items` now runs whenever items enter history and
whenever persisted history is reconstructed. Producers emit deferred
image data, so malformed images become the existing model-visible
placeholder instead of failing the session at the producer.

## Test plan

- `just fmt`
- `just fix -p codex-core -p codex-features`
- `just test -p codex-features` — 52 passed
- focused affected `codex-core` set — 20 passed
- `just test -p codex-core handle_accepts_explicit_high_detail` — 1
passed
- full `just test -p codex-core` attempt — 2,723 passed; 88 unrelated
environment failures from read-only `~/.codex` SQLite state and
unavailable integration helper binaries
This commit is contained in:
rka-oai
2026-06-22 10:05:11 -07:00
committed by GitHub
Unverified
parent e98d43ac37
commit e79d72d75d
18 changed files with 72 additions and 221 deletions
@@ -38,6 +38,8 @@ use tempfile::TempDir;
use tokio::time::timeout;
use wiremock::MockServer;
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
// macOS and Windows Bazel CI can spend tens of seconds starting app-server
// subprocesses or processing test RPCs under load.
#[cfg(any(target_os = "macos", windows))]
@@ -650,7 +652,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
text: "dynamic-ok".to_string(),
},
DynamicToolCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,AAA".to_string(),
image_url: TINY_PNG_DATA_URL.to_string(),
},
];
let content_items = response_content_items
@@ -695,7 +697,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
text: "dynamic-ok".to_string(),
},
DynamicToolCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,AAA".to_string(),
image_url: TINY_PNG_DATA_URL.to_string(),
},
])
);
@@ -721,7 +723,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
},
{
"type": "input_image",
"image_url": "data:image/png;base64,AAA",
"image_url": TINY_PNG_DATA_URL,
"detail": "high"
}
])
@@ -28,13 +28,13 @@ use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
const RESULT: &str = "cG5n";
const RESULT: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
const TINY_PNG_BYTES: &[u8] = &[
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1,
122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 248, 207, 192, 240, 31, 0,
5, 0, 1, 255, 137, 153, 61, 29, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
];
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=";
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
#[derive(Clone, Copy)]
enum ImagegenTestMode {
@@ -116,7 +116,7 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
assert_eq!(status, "completed");
assert_eq!(revised_prompt.as_deref(), Some("paint a blue whale"));
assert_eq!(result, RESULT);
assert_eq!(std::fs::read(&saved_path)?, b"png");
assert_eq!(std::fs::read(&saved_path)?, TINY_PNG_BYTES);
let requests = response_mock.requests();
assert_eq!(requests.len(), 2);
+1 -1
View File
@@ -81,7 +81,7 @@ pub(crate) async fn build_prompt_input_from_session(
.await;
if !input.is_empty() {
let response_item = sess.response_item_from_user_input(turn_context.as_ref(), input);
let response_item = sess.response_item_from_user_input(input);
sess.record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&response_item))
.await;
}
+9 -34
View File
@@ -1372,17 +1372,11 @@ impl Session {
} = self
.reconstruct_history_from_rollout(turn_context, rollout_items)
.await;
if turn_context
.config
.features
.enabled(Feature::ResizeAllImages)
{
// Keep the recorded rollout unchanged. Prepare its reconstructed history before
// installing it, so legacy images are processed once for this resume or fork and
// will be processed again if the rollout is reconstructed in a future session.
// This meets image resizing requirements without modifying persisted rollouts.
prepare_response_items(&mut history);
}
// Keep the recorded rollout unchanged. Prepare its reconstructed history before
// installing it, so legacy images are processed once for this resume or fork and
// will be processed again if the rollout is reconstructed in a future session.
// This meets image resizing requirements without modifying persisted rollouts.
prepare_response_items(&mut history);
{
let mut state = self.state.lock().await;
state.replace_history(history, reference_context_item);
@@ -2689,13 +2683,7 @@ impl Session {
items: &'a [ResponseItem],
) -> Cow<'a, [ResponseItem]> {
let mut items = Cow::Borrowed(items);
if turn_context
.config
.features
.enabled(Feature::ResizeAllImages)
{
prepare_response_items(items.to_mut());
}
prepare_response_items(items.to_mut());
if turn_context.config.features.enabled(Feature::ItemIds) {
Self::assign_missing_response_item_ids(items)
} else {
@@ -2734,23 +2722,10 @@ impl Session {
items
}
pub(crate) fn response_item_from_user_input(
&self,
turn_context: &TurnContext,
input: Vec<UserInput>,
) -> ResponseItem {
let local_image_preparation = if turn_context
.config
.features
.enabled(Feature::ResizeAllImages)
{
LocalImagePreparation::Defer
} else {
LocalImagePreparation::Process
};
pub(crate) fn response_item_from_user_input(&self, input: Vec<UserInput>) -> ResponseItem {
ResponseItem::from(ResponseInputItem::from_user_input(
input,
local_image_preparation,
LocalImagePreparation::Defer,
))
}
@@ -3666,7 +3641,7 @@ impl Session {
// Persist the user message to history, but emit the turn item from `UserInput` so
// UI-only `text_elements` are preserved. `ResponseItem::Message` does not carry
// those spans, and `record_response_item_and_emit_turn_item` would drop them.
let response_item = self.response_item_from_user_input(turn_context, input.to_vec());
let response_item = self.response_item_from_user_input(input.to_vec());
self.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
.await;
let mut user_message_item = UserMessageItem::new(input);
+3 -11
View File
@@ -1694,12 +1694,11 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
}
#[tokio::test]
async fn resize_all_images_prepares_failures_before_history_insertion() {
async fn prepares_image_failures_before_history_insertion() {
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::ResizeAllImages);
let _ = config.features.enable(Feature::ItemIds);
},
)
@@ -1764,15 +1763,8 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
}
#[tokio::test]
async fn resize_all_images_prepares_resumed_history_before_installing_it() {
let (session, _turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::ResizeAllImages);
},
)
.await;
async fn prepares_resumed_history_before_installing_it() {
let (session, _turn_context) = make_session_and_context().await;
let resumed_item = ResponseItem::Message {
id: None,
role: "user".to_string(),
+3 -25
View File
@@ -1,4 +1,3 @@
use codex_features::Feature;
use codex_protocol::items::ImageViewItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
@@ -8,9 +7,7 @@ use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::InputModality;
use codex_utils_image::PromptImageMode;
use codex_utils_image::data_url_from_bytes;
use codex_utils_image::load_for_prompt_bytes;
use serde::Deserialize;
use crate::function_tool::FunctionCallError;
@@ -195,24 +192,8 @@ impl ViewImageHandler {
DEFAULT_IMAGE_DETAIL
};
let image_url = if turn.config.features.enabled(Feature::ResizeAllImages) {
// The history insertion path owns image decoding and resizing when this is enabled.
data_url_from_bytes("application/octet-stream", &file_bytes)
} else {
let image_mode = if use_original_detail {
PromptImageMode::Original
} else {
PromptImageMode::ResizeToFit
};
load_for_prompt_bytes(abs_path.as_path(), file_bytes, image_mode)
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to process image at `{}`: {error}",
abs_path.display()
))
})?
.into_data_url()
};
// The history insertion path owns image decoding and resizing.
let image_url = data_url_from_bytes("application/octet-stream", &file_bytes);
let item = TurnItem::ImageView(ImageViewItem {
id: call_id,
@@ -420,9 +401,6 @@ mod tests {
})
.await;
let Err(FunctionCallError::RespondToModel(message)) = result else {
panic!("expected image processing error");
};
assert!(message.contains("unable to process image"), "{message}");
result.expect("explicit high detail should be accepted");
}
}
+2 -2
View File
@@ -860,7 +860,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
async fn resume_replays_image_tool_outputs_with_detail() {
skip_if_no_network!();
let image_url = "data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEAAUAmJaACdLoB+AADsAD+8ut//NgVzXPv9//S4P0uD9Lg/9KQAAA=";
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
let function_call_id = "view-image-call";
let custom_call_id = "js-repl-call";
let thread_id = ThreadId::default();
@@ -888,7 +888,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
id: None,
name: "view_image".to_string(),
namespace: None,
arguments: "{\"path\":\"/tmp/example.webp\"}".to_string(),
arguments: "{\"path\":\"/tmp/example.png\"}".to_string(),
call_id: function_call_id.to_string(),
metadata: None,
}),
+6 -14
View File
@@ -2681,7 +2681,7 @@ async fn code_mode_can_output_images_via_global_helper() -> Result<()> {
&server,
"use exec to return images",
r#"
image("data:image/png;base64,AAA");
image("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==");
"#,
)
.await?;
@@ -2706,7 +2706,7 @@ image("data:image/png;base64,AAA");
items[1],
serde_json::json!({
"type": "input_image",
"image_url": "data:image/png;base64,AAA",
"image_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==",
"detail": "high"
}),
);
@@ -2715,17 +2715,14 @@ image("data:image/png;base64,AAA");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn resize_all_images_replaces_malformed_code_mode_image() -> Result<()> {
async fn code_mode_replaces_malformed_image() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let (_test, second_mock) = run_code_mode_turn_with_config(
let (_test, second_mock) = run_code_mode_turn(
&server,
"use exec to return an image",
r#"image("data:image/png;base64,AAA");"#,
|config| {
let _ = config.features.enable(Feature::ResizeAllImages);
},
)
.await?;
@@ -2746,7 +2743,7 @@ async fn resize_all_images_replaces_malformed_code_mode_image() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn resize_all_images_resizes_explicit_original_code_mode_image() -> Result<()> {
async fn code_mode_resizes_explicit_original_image() -> Result<()> {
skip_if_no_network!(Ok(()));
let original_dimensions = (6401, 100);
@@ -2772,12 +2769,7 @@ async fn resize_all_images_resizes_explicit_original_code_mode_image() -> Result
"use exec to return a large original-detail image",
&code,
"gpt-5.3-codex",
|config| {
config
.features
.enable(Feature::ResizeAllImages)
.expect("resize_all_images should be enabled");
},
|_| {},
)
.await?;
+1 -1
View File
@@ -4403,7 +4403,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
)
.await
.expect("override thread settings");
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII="
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="
.to_string();
codex
.submit(Op::UserInput {
@@ -42,9 +42,11 @@ use wiremock::matchers::path;
const TINY_PNG_BYTES: &[u8] = &[
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1,
122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 248, 207, 192, 240, 31, 0,
5, 0, 1, 255, 137, 153, 61, 29, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
];
const TINY_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
fn image_generation_extensions(auth: &CodexAuth) -> Arc<ExtensionRegistry<Config>> {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth.clone());
@@ -148,7 +150,7 @@ async fn extension_tool_uses_granted_turn_permissions() -> Result<()> {
.and(path("/v1/images/edits"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"created": 1,
"data": [{"b64_json": "cG5n"}],
"data": [{"b64_json": TINY_PNG_BASE64}],
})))
.expect(1)
.mount(&server)
@@ -299,7 +301,7 @@ async fn extension_tool_uses_granted_turn_permissions() -> Result<()> {
let output = request.function_call_output(image_call_id);
let image = &output["output"][0];
assert_eq!(image["type"], "input_image");
assert_eq!(image["image_url"], "data:image/png;base64,cG5n");
assert_eq!(image["image_url"], TINY_PNG_DATA_URL);
Ok(())
}
+1 -1
View File
@@ -200,7 +200,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
..
} = test_codex().build(&server).await?;
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=".to_string();
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string();
let response = sse(vec![
ev_response_created("resp-1"),
+1 -1
View File
@@ -506,7 +506,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
let _ = models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII="
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="
.to_string();
test.codex
+1 -1
View File
@@ -55,7 +55,7 @@ fn has_hosted_tool(tools: &[Value], tool_type: &str) -> bool {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responses_lite_strips_data_image_detail_without_resize_all_images() -> Result<()> {
async fn responses_lite_strips_data_image_detail() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
-5
View File
@@ -24,7 +24,6 @@ use codex_core::config::Config;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::HttpRequestParams;
use codex_features::Feature;
use codex_login::CodexAuth;
use codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY;
use codex_models_manager::manager::RefreshStrategy;
@@ -1455,10 +1454,6 @@ async fn stdio_image_responses_resize_large_image() -> anyhow::Result<()> {
let rmcp_test_server_bin = remote_aware_stdio_server_bin()?;
let fixture = test_codex()
.with_config(move |config| {
config
.features
.enable(Feature::ResizeAllImages)
.expect("resize_all_images should be enabled");
insert_mcp_server(
config,
server_name,
+1 -1
View File
@@ -474,7 +474,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
let rmcp_test_server_bin = stdio_server_bin()?;
// 1x1 PNG data URL
let openai_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/ee9bQAAAABJRU5ErkJggg==";
let openai_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
let mut builder = test_codex().with_config(move |config| {
let mut servers = config.mcp_servers.get().clone();
+6 -108
View File
@@ -7,7 +7,6 @@ use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
use codex_exec_server::RemoveOptions;
use codex_features::Feature;
use codex_login::CodexAuth;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile;
@@ -181,15 +180,10 @@ async fn write_workspace_png(
async fn assert_user_turn_local_image_resizes_to(
original_dimensions: (u32, u32),
expected_dimensions: (u32, u32),
resize_policy: TestImageResizePolicy,
) -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex().with_config(move |config| {
if resize_policy == TestImageResizePolicy::AllImages {
let _ = config.features.enable(Feature::ResizeAllImages);
}
});
let mut builder = test_codex();
let test = builder.build_with_remote_env(&server).await?;
let TestCodex {
codex,
@@ -263,42 +257,25 @@ async fn assert_user_turn_local_image_resizes_to(
Ok(())
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum TestImageResizePolicy {
Legacy,
AllImages,
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
assert_user_turn_local_image_resizes_to((2304, 864), (2048, 768), TestImageResizePolicy::Legacy)
.await
assert_user_turn_local_image_resizes_to((2304, 864), (2048, 768)).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_turn_with_vertical_local_image_resizes_to_square_bounds() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
assert_user_turn_local_image_resizes_to(
(1024, 4096),
(512, 2048),
TestImageResizePolicy::Legacy,
)
.await
assert_user_turn_local_image_resizes_to((1024, 4096), (512, 2048)).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn resize_all_images_applies_patch_budget_to_local_user_image() -> anyhow::Result<()> {
async fn user_turn_local_image_applies_patch_budget() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
assert_user_turn_local_image_resizes_to(
(2048, 2048),
(1600, 1600),
TestImageResizePolicy::AllImages,
)
.await
assert_user_turn_local_image_resizes_to((2048, 2048), (1600, 1600)).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1209,11 +1186,10 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
async fn view_image_tool_turns_invalid_image_into_placeholder() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build_with_remote_env(&server).await?;
let TestCodex {
@@ -1222,84 +1198,6 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
..
} = &test;
let rel_path = "assets/example.json";
let abs_path =
write_workspace_file(&test, rel_path, br#"{ "message": "hello" }"#.to_vec()).await?;
let call_id = "view-image-non-image";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
let first_response = sse(vec![
ev_response_created("resp-1"),
ev_function_call(call_id, "view_image", &arguments),
ev_completed("resp-1"),
]);
responses::mount_sse_once(&server, first_response).await;
let second_response = sse(vec![
ev_assistant_message("msg-1", "done"),
ev_completed("resp-2"),
]);
let mock = responses::mount_sse_once(&server, second_response).await;
let session_model = session_configured.model.clone();
codex
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please use the view_image tool to read the json file".into(),
text_elements: Vec::new(),
}],
session_model,
))
.await?;
wait_for_event_with_timeout(
codex,
|event| matches!(event, EventMsg::TurnComplete(_)),
VIEW_IMAGE_TURN_COMPLETE_TIMEOUT,
)
.await;
let request = mock.single_request();
assert!(
request.inputs_of_type("input_image").is_empty(),
"non-image file should not produce an input_image message"
);
let (error_text, success) = request
.function_call_output_content_and_success(call_id)
.expect("function_call_output should be present");
assert_eq!(success, None);
let error_text = error_text.expect("error text present");
let expected_error = format!(
"unable to process image at `{}`: unsupported image `application/json`",
abs_path.display()
);
assert!(
error_text.contains(&expected_error),
"error should describe unsupported file type: {error_text}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn resize_all_images_turns_invalid_view_image_into_placeholder() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_config(|config| {
let _ = config.features.enable(Feature::ResizeAllImages);
});
let test = builder.build_with_remote_env(&server).await?;
let TestCodex {
codex,
session_configured,
..
} = &test;
let rel_path = "assets/invalid-image.json";
write_workspace_file(&test, rel_path, br#"{ "message": "hello" }"#.to_vec()).await?;
let call_id = "view-image-invalid-placeholder";
+4 -4
View File
@@ -193,7 +193,7 @@ pub enum Feature {
ImageGeneration,
/// Replace hosted image generation with the standalone image-generation extension.
ImageGenExt,
/// Resize all inline data-URL images before recording them in history.
/// Removed compatibility flag for always-on centralized image preparation.
ResizeAllImages,
/// Generate Responses API item IDs for client-created history items.
ItemIds,
@@ -472,7 +472,7 @@ impl Features {
"tool_search" | "apps_mcp_path_override" => {
continue;
}
"image_detail_original" => {
"image_detail_original" | "resize_all_images" => {
continue;
}
"plugin_hooks" => {
@@ -1160,8 +1160,8 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::ResizeAllImages,
key: "resize_all_images",
stage: Stage::UnderDevelopment,
default_enabled: false,
stage: Stage::Removed,
default_enabled: true,
},
FeatureSpec {
id: Feature::ItemIds,
+17
View File
@@ -476,6 +476,23 @@ fn from_sources_ignores_removed_image_detail_original_feature_key() {
assert_eq!(features, Features::with_defaults());
}
#[test]
fn from_sources_ignores_removed_resize_all_images_feature_key() {
let features_toml =
FeaturesToml::from(BTreeMap::from([("resize_all_images".to_string(), false)]));
let features = Features::from_sources(
FeatureConfigSource {
features: Some(&features_toml),
..Default::default()
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
assert_eq!(features, Features::with_defaults());
}
#[test]
fn from_sources_ignores_removed_undo_feature_key() {
let features_toml = FeaturesToml::from(BTreeMap::from([("undo".to_string(), true)]));