Add optional animated GIF output to image generation (#59073)

## What changed

- Gate GIF output behind `features.image_generation_animation`, disabled by default.
- Accept `gif` options for generation and editing, with optional `frame_count` and `frame_duration_ms` controls. Use `gif: {}` for API defaults.
- Preserve animated output with `.gif` filenames and `image/gif` data URLs while providing a still preview to the model.
- Add guidance for simple animations and spritesheet-based GIF edits.

## Testing

Add integration coverage for GIF generation and editing, explicit frame controls and API defaults, saved animation bytes, and still-preview replay across follow-up turns.

GitOrigin-RevId: 08afa0316ae3675921652157836191a649f563cf
This commit is contained in:
Gan Tu
2026-11-03 06:24:24 +00:00
committed by copyberry
parent 29eed28082
commit f9e7b19df8
14 changed files with 548 additions and 43 deletions
@@ -262,6 +262,7 @@ mod tests {
let (response, imagegen_request_id) = client
.generate(
&ImageGenerationRequest {
output: None,
prompt: "a red fox in a field".to_string(),
background: Some(ImageBackground::Opaque),
model: "gpt-image-1.5".to_string(),
@@ -311,6 +312,7 @@ mod tests {
let (response, _) = client
.generate(
&ImageGenerationRequest {
output: None,
prompt: "test".to_string(),
background: None,
model: "gpt-image-2".to_string(),
@@ -341,6 +343,7 @@ mod tests {
let (response, imagegen_request_id) = client
.edit(
&ImageEditRequest {
output: None,
images: vec![
ImageReference::Inline {
image_url: "data:image/png;base64,Zm9v".to_string(),
@@ -389,6 +392,7 @@ mod tests {
let error = client
.generate(
&ImageGenerationRequest {
output: None,
prompt: "a red fox in a field".to_string(),
background: None,
model: "gpt-image-1.5".to_string(),
+25
View File
@@ -1,10 +1,13 @@
use codex_protocol::models::ImageReference;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct ImageGenerationRequest {
pub prompt: String,
#[serde(flatten)]
pub output: Option<ImageOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<ImageBackground>,
pub model: String,
@@ -20,6 +23,8 @@ pub struct ImageGenerationRequest {
pub struct ImageEditRequest {
pub images: Vec<ImageReference>,
pub prompt: String,
#[serde(flatten)]
pub output: Option<ImageOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<ImageBackground>,
pub model: String,
@@ -31,6 +36,26 @@ pub struct ImageEditRequest {
pub size: Option<String>,
}
/// Optional GIF controls accepted by the Images generation and edit endpoints.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
pub struct GifOptions {
/// Number of frames (2–16). Defaults to 9.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 2, max = 16))]
pub frame_count: Option<u32>,
/// Duration of each frame in milliseconds (20–2000). Defaults to 200.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 20, max = 2000))]
pub frame_duration_ms: Option<u32>,
}
/// The Images API expects the format and its controls as sibling fields.
#[derive(Debug, Clone, Copy, Serialize, PartialEq)]
#[serde(tag = "output_format", rename_all = "lowercase")]
pub enum ImageOutput {
Gif(GifOptions),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ImageBackground {
+2
View File
@@ -87,10 +87,12 @@ pub use crate::files::HostedFileUploadContext;
pub use crate::files::OPENAI_FILE_UPLOAD_LIMIT_BYTES;
pub use crate::files::upload_openai_file;
pub use crate::guardian_assessment::GuardianAssessment;
pub use crate::images::GifOptions;
pub use crate::images::ImageBackground;
pub use crate::images::ImageData;
pub use crate::images::ImageEditRequest;
pub use crate::images::ImageGenerationRequest;
pub use crate::images::ImageOutput;
pub use crate::images::ImageQuality;
pub use crate::images::ImageResponse;
pub use crate::provider::Provider;