image-gen-core (#13290)

Core tool-calling for image-gen, handles requesting and receiving logic
for images using response API
This commit is contained in:
Won Park
2026-03-03 23:11:28 -08:00
committed by GitHub
parent 4f6c4bb143
commit fa2306b303
22 changed files with 766 additions and 45 deletions
+8 -1
View File
@@ -344,6 +344,9 @@ 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);
}
@@ -374,6 +377,7 @@ impl ContextManager {
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::CustomToolCall { .. }
| ResponseItem::Compaction { .. }
| ResponseItem::GhostSnapshot { .. }
@@ -402,7 +406,8 @@ fn truncate_function_output_payload(
}
/// API messages include every non-system item (user/assistant messages, reasoning,
/// tool calls, tool outputs, shell calls, and web-search calls).
/// tool calls, tool outputs, shell calls, web-search calls, and image-generation
/// calls).
fn is_api_message(message: &ResponseItem) -> bool {
match message {
ResponseItem::Message { role, .. } => role.as_str() != "system",
@@ -413,6 +418,7 @@ fn is_api_message(message: &ResponseItem) -> bool {
| ResponseItem::LocalShellCall { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::Compaction { .. } => true,
ResponseItem::GhostSnapshot { .. } => false,
ResponseItem::Other => false,
@@ -600,6 +606,7 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
ResponseItem::Reasoning { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::CustomToolCall { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::Compaction { .. } => true,
@@ -395,6 +395,97 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
}
}
#[test]
fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() {
let history = create_history_with_items(vec![
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,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hi".to_string(),
}],
end_turn: None,
phase: None,
},
]);
assert_eq!(
history.for_prompt(&default_input_modalities()),
vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputImage {
image_url: "data:image/png;base64,Zm9v".to_string(),
}],
end_turn: None,
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hi".to_string(),
}],
end_turn: None,
phase: None,
}
]
);
}
#[test]
fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() {
let history = create_history_with_items(vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "generate a lobster".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: "Zm9v".to_string(),
},
]);
assert_eq!(
history.for_prompt(&[InputModality::Text]),
vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "generate a lobster".to_string(),
}],
end_turn: None,
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "image content omitted because you do not support image input"
.to_string(),
}],
end_turn: None,
phase: None,
},
]
);
}
#[test]
fn get_history_for_prompt_drops_ghost_commits() {
let items = vec![ResponseItem::GhostSnapshot {
+26 -2
View File
@@ -1,10 +1,9 @@
use std::collections::HashSet;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::InputModality;
use std::collections::HashSet;
use crate::util::error_or_panic;
use tracing::info;
@@ -211,6 +210,31 @@ 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 { result, .. } => {
let image_url = if result.starts_with("data:") {
result
} else {
format!("data:image/png;base64,{result}")
};
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![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(