sending back imagaegencall response back to responseapi (#14558)

Sending back the ResponseItem::ImageGenerationCall as is, because it is
now supported from the API-side.
This commit is contained in:
Won Park
2026-03-13 17:29:19 +00:00
committed by GitHub
parent 6b3d82daca
commit 958f93f899
7 changed files with 211 additions and 121 deletions
+11 -17
View File
@@ -59,6 +59,7 @@ use codex_app_server_protocol::AppInfo;
use codex_otel::TelemetryAuthMode;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelsResponse;
@@ -154,15 +155,6 @@ fn developer_input_texts(items: &[ResponseItem]) -> Vec<&str> {
.collect()
}
fn default_image_save_developer_message_text() -> String {
let image_output_dir = crate::stream_events_utils::default_image_generation_output_dir();
format!(
"Generated images are saved to {} as {} by default.",
image_output_dir.display(),
image_output_dir.join("<image_id>.png").display(),
)
}
fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> ToolCallRuntime {
let router = Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
@@ -3203,13 +3195,12 @@ async fn build_initial_context_omits_default_image_save_location_without_image_h
}
#[tokio::test]
async fn handle_output_item_done_records_image_save_message_after_successful_save() {
async fn handle_output_item_done_records_image_save_history_message() {
let (session, turn_context) = make_session_and_context().await;
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
let call_id = "ig_history_records_message";
let expected_saved_path = crate::stream_events_utils::default_image_generation_output_dir()
.join(format!("{call_id}.png"));
let expected_saved_path = std::env::temp_dir().join(format!("{call_id}.png"));
let _ = std::fs::remove_file(&expected_saved_path);
let item = ResponseItem::ImageGenerationCall {
id: call_id.to_string(),
@@ -3229,9 +3220,13 @@ async fn handle_output_item_done_records_image_save_message_after_successful_sav
.expect("image generation item should succeed");
let history = session.clone_history().await;
let expected_message: ResponseItem =
DeveloperInstructions::new(default_image_save_developer_message_text()).into();
assert_eq!(history.raw_items(), &[expected_message, item]);
let save_message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.",
std::env::temp_dir().display(),
std::env::temp_dir().join("<image_id>.png").display(),
))
.into();
assert_eq!(history.raw_items(), &[save_message, item]);
assert_eq!(
std::fs::read(&expected_saved_path).expect("saved file"),
b"foo"
@@ -3245,8 +3240,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
let call_id = "ig_history_no_message";
let expected_saved_path = crate::stream_events_utils::default_image_generation_output_dir()
.join(format!("{call_id}.png"));
let expected_saved_path = std::env::temp_dir().join(format!("{call_id}.png"));
let _ = std::fs::remove_file(&expected_saved_path);
let item = ResponseItem::ImageGenerationCall {
id: call_id.to_string(),
@@ -344,9 +344,6 @@ impl ContextManager {
// all outputs must have a corresponding function/tool call
normalize::remove_orphan_outputs(&mut self.items);
//rewrite image_gen_calls to messages to support stateless input
normalize::rewrite_image_generation_calls_for_stateless_input(&mut self.items);
// strip images when model does not support them
normalize::strip_images_when_unsupported(input_modalities, &mut self.items);
}
@@ -398,7 +398,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
}
#[test]
fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() {
fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
let history = create_history_with_items(vec![
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
@@ -420,25 +420,11 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() {
assert_eq!(
history.for_prompt(&default_input_modalities()),
vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "Image Generation Call".to_string(),
},
ContentItem::InputText {
text: "Image ID: ig_123".to_string(),
},
ContentItem::InputText {
text: "Prompt: lobster".to_string(),
},
ContentItem::InputImage {
image_url: "data:image/png;base64,Zm9v".to_string(),
},
],
end_turn: None,
phase: None,
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
},
ResponseItem::Message {
id: None,
@@ -454,7 +440,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() {
}
#[test]
fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() {
fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
let history = create_history_with_items(vec![
ResponseItem::Message {
id: None,
@@ -485,26 +471,11 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() {
end_turn: None,
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "Image Generation Call".to_string(),
},
ContentItem::InputText {
text: "Image ID: ig_123".to_string(),
},
ContentItem::InputText {
text: "Prompt: lobster".to_string(),
},
ContentItem::InputText {
text: "image content omitted because you do not support image input"
.to_string(),
},
],
end_turn: None,
phase: None,
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: String::new(),
},
]
);
+3 -42
View File
@@ -289,48 +289,6 @@ where
}
}
pub(crate) fn rewrite_image_generation_calls_for_stateless_input(items: &mut Vec<ResponseItem>) {
let original_items = std::mem::take(items);
*items = original_items
.into_iter()
.map(|item| match item {
ResponseItem::ImageGenerationCall {
id,
revised_prompt,
result,
..
} => {
let image_url = if result.starts_with("data:") {
result
} else {
format!("data:image/png;base64,{result}")
};
let revised_prompt = revised_prompt.unwrap_or_default();
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "Image Generation Call".to_string(),
},
ContentItem::InputText {
text: format!("Image ID: {id}"),
},
ContentItem::InputText {
text: format!("Prompt: {revised_prompt}"),
},
ContentItem::InputImage { image_url },
],
end_turn: None,
phase: None,
}
}
_ => item,
})
.collect();
}
/// Strip image content from messages and tool outputs when the model does not support images.
/// When `input_modalities` contains `InputModality::Image`, no stripping is performed.
pub(crate) fn strip_images_when_unsupported(
@@ -377,6 +335,9 @@ pub(crate) fn strip_images_when_unsupported(
*content_items = normalized_content_items;
}
}
ResponseItem::ImageGenerationCall { result, .. } => {
result.clear();
}
_ => {}
}
}
+3 -8
View File
@@ -73,15 +73,11 @@ async fn save_image_generation_result(call_id: &str, result: &str) -> Result<Pat
if file_stem.is_empty() {
file_stem = "generated_image".to_string();
}
let path = default_image_generation_output_dir().join(format!("{file_stem}.png"));
let path = std::env::temp_dir().join(format!("{file_stem}.png"));
tokio::fs::write(&path, bytes).await?;
Ok(path)
}
pub(crate) fn default_image_generation_output_dir() -> PathBuf {
std::env::temp_dir()
}
/// Persist a completed model response item and record any cited memory usage.
pub(crate) async fn record_completed_response_item(
sess: &Session,
@@ -214,7 +210,6 @@ pub(crate) async fn handle_output_item_done(
.emit_turn_item_completed(&ctx.turn_context, turn_item)
.await;
}
record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item)
.await;
let last_agent_message = last_assistant_message_from_item(&item, plan_mode);
@@ -310,7 +305,7 @@ pub(crate) async fn handle_non_tool_response_item(
match save_image_generation_result(&image_item.id, &image_item.result).await {
Ok(path) => {
image_item.saved_path = Some(path.to_string_lossy().into_owned());
let image_output_dir = default_image_generation_output_dir();
let image_output_dir = std::env::temp_dir();
let message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.",
image_output_dir.display(),
@@ -324,7 +319,7 @@ pub(crate) async fn handle_non_tool_response_item(
.await;
}
Err(err) => {
let output_dir = default_image_generation_output_dir();
let output_dir = std::env::temp_dir();
tracing::warn!(
call_id = %image_item.id,
output_dir = %output_dir.display(),
@@ -1,4 +1,3 @@
use super::default_image_generation_output_dir;
use super::handle_non_tool_response_item;
use super::last_assistant_message_from_item;
use super::save_image_generation_result;
@@ -71,7 +70,7 @@ fn last_assistant_message_from_item_returns_none_for_plan_only_hidden_message()
#[tokio::test]
async fn save_image_generation_result_saves_base64_to_png_in_temp_dir() {
let expected_path = default_image_generation_output_dir().join("ig_save_base64.png");
let expected_path = std::env::temp_dir().join("ig_save_base64.png");
let _ = std::fs::remove_file(&expected_path);
let saved_path = save_image_generation_result("ig_save_base64", "Zm9v")
@@ -95,7 +94,7 @@ async fn save_image_generation_result_rejects_data_url_payload() {
#[tokio::test]
async fn save_image_generation_result_overwrites_existing_file() {
let existing_path = default_image_generation_output_dir().join("ig_overwrite.png");
let existing_path = std::env::temp_dir().join("ig_overwrite.png");
std::fs::write(&existing_path, b"existing").expect("seed existing image");
let saved_path = save_image_generation_result("ig_overwrite", "Zm9v")
@@ -109,7 +108,7 @@ async fn save_image_generation_result_overwrites_existing_file() {
#[tokio::test]
async fn save_image_generation_result_sanitizes_call_id_for_temp_dir_output_path() {
let expected_path = default_image_generation_output_dir().join("___ig___.png");
let expected_path = std::env::temp_dir().join("___ig___.png");
let _ = std::fs::remove_file(&expected_path);
let saved_path = save_image_generation_result("../ig/..", "Zm9v")