Store generated image items as attachment references (#54398)

## What changed

- Upload generated images to the attachment store and expose `fileId` in image-generation items when available, with inline `result` fallback on upload or decoding failure.
- Preserve file references through legacy events, persisted rollouts, thread history, and task output summaries. Keep model-facing image preparation separate.
- Add attachment request tags and mark generated-image uploads with `usage=image_gen`, preserving the thread's ephemeral setting.
- Update public schemas and SDK types to accept either image representation, retain the Python `ImageGenerationThreadItem` alias, and prefer `fileId` when reading records containing both fields.

## Testing

Add coverage for file-backed generation and rollout persistence, legacy history replay, inline and transitional serialization, task output visibility, and Python SDK alias compatibility.

GitOrigin-RevId: f7a2bb5707e2f277348a547a988bc33c8e96d891
This commit is contained in:
Krish Chainani
2026-10-15 19:51:22 +00:00
committed by copyberry
parent 1ef30bcbe7
commit aabf13307a
64 changed files with 1229 additions and 125 deletions
+4
View File
@@ -282,3 +282,7 @@ fn persisted_item_completed_event<'a>(
},
}
}
#[cfg(test)]
#[path = "policy_tests.rs"]
mod tests;
+115
View File
@@ -0,0 +1,115 @@
use codex_extension_items::ExtensionItem;
use codex_extension_items::image_generation::GeneratedImageRef;
use codex_extension_items::image_generation::ImageGenerationItem;
use codex_protocol::ThreadId;
use codex_protocol::items::TurnItem;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ImageGenerationEndEvent;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::ThreadHistoryMode;
use super::persisted_event_msg;
fn image_item(image: GeneratedImageRef) -> ImageGenerationItem {
ImageGenerationItem {
id: "image-1".to_string(),
status: "completed".to_string(),
revised_prompt: Some("paint a blue whale".to_string()),
image,
transparent_background: Some(false),
failure: None,
saved_path: None,
imagegen_request_id: None,
generation_id: None,
}
}
#[test]
fn paginated_image_generation_preserves_file_reference() {
let event = EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".to_string(),
item: TurnItem::Extension(ExtensionItem::ImageGeneration(image_item(
GeneratedImageRef::File {
file_id: "file-1".to_string(),
},
))),
started_at_ms: Some(1),
completed_at_ms: 2,
});
let persisted = persisted_event_msg(&event, ThreadHistoryMode::Paginated)
.expect("expected persisted image-generation item");
let EventMsg::ItemCompleted(persisted) = persisted.as_ref() else {
panic!("expected persisted image-generation item");
};
let TurnItem::Extension(ExtensionItem::ImageGeneration(image)) = &persisted.item else {
panic!("expected image-generation extension item");
};
assert_eq!(
image,
&image_item(GeneratedImageRef::File {
file_id: "file-1".to_string(),
})
);
}
#[test]
fn legacy_image_generation_preserves_file_reference() {
let event = EventMsg::ImageGenerationEnd(ImageGenerationEndEvent {
call_id: "image-1".to_string(),
status: "completed".to_string(),
revised_prompt: Some("paint a blue whale".to_string()),
image: GeneratedImageRef::File {
file_id: "file-1".to_string(),
},
transparent_background: Some(false),
failure: None,
saved_path: None,
});
let persisted = persisted_event_msg(&event, ThreadHistoryMode::Legacy)
.expect("expected persisted legacy image-generation event");
let EventMsg::ImageGenerationEnd(persisted) = persisted.as_ref() else {
panic!("expected persisted legacy image-generation event");
};
assert_eq!(
persisted.image,
GeneratedImageRef::File {
file_id: "file-1".to_string(),
}
);
}
#[test]
fn inline_image_generation_result_is_preserved() {
let event = EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".to_string(),
item: TurnItem::Extension(ExtensionItem::ImageGeneration(image_item(
GeneratedImageRef::Inline {
result: "base64-image".to_string(),
},
))),
started_at_ms: Some(1),
completed_at_ms: 2,
});
let persisted = persisted_event_msg(&event, ThreadHistoryMode::Paginated)
.expect("expected persisted inline image-generation item");
let EventMsg::ItemCompleted(persisted) = persisted.as_ref() else {
panic!("expected persisted inline image-generation item");
};
let TurnItem::Extension(ExtensionItem::ImageGeneration(image)) = &persisted.item else {
panic!("expected inline image-generation extension item");
};
assert_eq!(
image,
&image_item(GeneratedImageRef::Inline {
result: "base64-image".to_string(),
})
);
}