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 10:29:19 -07:00
committed by GitHub
parent 6b3d82daca
commit 958f93f899
7 changed files with 211 additions and 121 deletions

View File

@@ -443,6 +443,9 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
skip_if_no_network!(Ok(()));
let saved_path = std::env::temp_dir().join("ig_123.png");
let _ = std::fs::remove_file(&saved_path);
let server = MockServer::start().await;
let image_model_slug = "test-image-model";
let image_model = test_model_info(
@@ -527,19 +530,42 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
assert_eq!(requests.len(), 2, "expected two model requests");
let second_request = requests.last().expect("expected second request");
let image_generation_calls = second_request.inputs_of_type("image_generation_call");
assert_eq!(
second_request.message_input_image_urls("user"),
vec!["data:image/png;base64,Zm9v".to_string()]
image_generation_calls.len(),
1,
"expected generated image history to be replayed as an image_generation_call"
);
assert_eq!(
image_generation_calls[0]["id"].as_str(),
Some("ig_123"),
"expected the original image generation call id to be preserved"
);
assert_eq!(
image_generation_calls[0]["result"].as_str(),
Some("Zm9v"),
"expected the original generated image payload to be preserved"
);
assert!(
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Generated images are saved to")),
"second request should include the saved-path note in model-visible history"
);
let _ = std::fs::remove_file(&saved_path);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_change_from_generated_image_to_text_strips_prior_generated_image_content()
async fn model_change_from_generated_image_to_text_preserves_prior_generated_image_call()
-> Result<()> {
skip_if_no_network!(Ok(()));
let saved_path = std::env::temp_dir().join("ig_123.png");
let _ = std::fs::remove_file(&saved_path);
let server = MockServer::start().await;
let image_model_slug = "test-image-model";
let text_model_slug = "test-text-only-model";
@@ -631,17 +657,164 @@ async fn model_change_from_generated_image_to_text_strips_prior_generated_image_
assert_eq!(requests.len(), 2, "expected two model requests");
let second_request = requests.last().expect("expected second request");
let image_generation_calls = second_request.inputs_of_type("image_generation_call");
assert!(
second_request.message_input_image_urls("user").is_empty(),
"second request should strip generated image content for text-only models"
"second request should not rewrite generated images into message input images"
);
assert!(
image_generation_calls.len() == 1,
"second request should preserve the generated image call for text-only models"
);
assert_eq!(
image_generation_calls[0]["id"].as_str(),
Some("ig_123"),
"second request should preserve the original generated image call id"
);
assert_eq!(
image_generation_calls[0]["result"].as_str(),
Some(""),
"second request should strip generated image bytes for text-only models"
);
assert!(
second_request
.message_input_texts("user")
.iter()
.any(|text| text == "image content omitted because you do not support image input"),
"second request should include the image-omitted placeholder text"
.all(|text| text != "image content omitted because you do not support image input"),
"second request should not inject the image-omitted placeholder text"
);
assert!(
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Generated images are saved to")),
"second request should include the saved-path note in model-visible history"
);
let _ = std::fs::remove_file(&saved_path);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() -> Result<()> {
skip_if_no_network!(Ok(()));
let saved_path = std::env::temp_dir().join("ig_rollback.png");
let _ = std::fs::remove_file(&saved_path);
let server = MockServer::start().await;
let image_model_slug = "test-image-model";
let image_model = test_model_info(
image_model_slug,
"Test Image Model",
"supports image input",
default_input_modalities(),
);
mount_models_once(
&server,
ModelsResponse {
models: vec![image_model],
},
)
.await;
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
ev_image_generation_call("ig_rollback", "completed", "lobster", "Zm9v"),
ev_completed_with_tokens("resp-1", 10),
]),
sse_completed("resp-2"),
],
)
.await;
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(move |config| {
config.model = Some(image_model_slug.to_string());
});
let test = builder.build(&server).await?;
let models_manager = test.thread_manager.get_models_manager();
let _ = models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
test.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "generate a lobster".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::ThreadRollback { num_turns: 1 })
.await?;
wait_for_event(&test.codex, |ev| {
matches!(ev, EventMsg::ThreadRolledBack(_))
})
.await;
test.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "after rollback".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let requests = responses.requests();
assert_eq!(requests.len(), 2, "expected two model requests");
let second_request = requests.last().expect("expected second request");
assert!(
!second_request
.message_input_texts("user")
.iter()
.any(|text| text == "generate a lobster"),
"rollback should remove the rolled-back image-generation user turn"
);
assert!(
!second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Generated images are saved to")),
"rollback should remove the generated-image save note with the rolled-back turn"
);
assert!(
second_request
.inputs_of_type("image_generation_call")
.is_empty(),
"rollback should remove the generated image call with the rolled-back turn"
);
let _ = std::fs::remove_file(&saved_path);
Ok(())
}