Expose and persist image generation IDs (#57552)

## What changed

Expose the backend-provided `generationId` on image-generation items and preserve it through legacy completion events, thread history reconstruction, and rollout migration. Default missing IDs to `None` for older history.

Update the generated JSON schemas, TypeScript types, and Python models to include the nullable field.

## Testing

Extend coverage for ID serialization round trips, completed-item notifications, persisted and resumed history, and legacy rollout migration.

GitOrigin-RevId: a7f06faa7ac6833417b257a535d2901c03b79135
This commit is contained in:
jimxie-oai
2026-10-27 19:43:27 +00:00
committed by copyberry
parent aa854dabf5
commit 0d6838318f
36 changed files with 216 additions and 13 deletions
@@ -5427,6 +5427,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -23211,6 +23211,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -20925,6 +20925,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1580,6 +1580,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1580,6 +1580,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1785,6 +1785,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2399,6 +2399,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1580,6 +1580,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2190,6 +2190,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2190,6 +2190,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2190,6 +2190,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2451,6 +2451,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2190,6 +2190,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2399,6 +2399,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2190,6 +2190,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1785,6 +1785,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -2190,6 +2190,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1785,6 +1785,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1785,6 +1785,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1785,6 +1785,14 @@
],
"default": null
},
"generationId": {
"default": null,
"description": "Image generation identifier for the generated image, when provided by the image backend.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -9,4 +9,8 @@ export type ImageGenerationItem = { id: string, status: string, revisedPrompt: s
/**
* Observed local inputs for a completed generation, absent on older items.
*/
inputImages?: Array<ImageGenerationInputImage>, } & ({ fileId: string, } | { result: string, });
inputImages?: Array<ImageGenerationInputImage>,
/**
* Image generation identifier for the generated image, when provided by the image backend.
*/
generationId: string | null, } & ({ fileId: string, } | { result: string, });
@@ -1043,7 +1043,7 @@ impl ThreadHistoryBuilder {
saved_path: payload.saved_path.clone(),
input_images: payload.input_images.clone(),
imagegen_request_id: None,
generation_id: None,
generation_id: payload.generation_id.clone(),
});
self.upsert_item_in_current_turn(item);
}
@@ -2929,6 +2929,7 @@ mod tests {
path: test_path_buf("/tmp/reference.png").abs(),
sha256: "ab".repeat(32),
}],
generation_id: None,
})),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
root_turn_id: None,
@@ -168,6 +168,7 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model(
image,
transparent_background,
saved_path: Some(saved_path),
generation_id,
..
}) = completed.item.payload
else {
@@ -175,6 +176,7 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model(
};
assert_eq!(status, "completed");
assert_eq!(revised_prompt.as_deref(), Some("paint a blue whale"));
assert_eq!(generation_id.as_deref(), Some("gen-image-123"));
assert_eq!(
image,
GeneratedImageRef::Inline {
@@ -398,6 +400,10 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history(
})
.context("persisted legacy history should contain the generated image")?;
assert_eq!(persisted_image.transparent_background, Some(true));
assert_eq!(
persisted_image.generation_id.as_deref(),
Some("gen-transparent")
);
assert_eq!(persisted_image.input_images, expected_inputs);
let resume_id = resumed
@@ -418,6 +424,10 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history(
})
.context("resumed legacy history should contain the generated image")?;
assert_eq!(resumed_image.transparent_background, Some(true));
assert_eq!(
resumed_image.generation_id.as_deref(),
Some("gen-transparent")
);
assert_eq!(resumed_image.input_images, expected_inputs);
Ok(())
@@ -1255,7 +1265,7 @@ async fn mount_image_response_with_background(server: &MockServer, background: &
.set_body_json(json!({
"created": 1,
"background": background,
"data": [{"b64_json": RESULT}],
"data": [{"b64_json": RESULT, "generation_id": "gen-transparent"}],
})),
)
.expect(1)
@@ -1269,7 +1279,7 @@ async fn mount_image_edit_response(server: &MockServer) {
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"created": 1,
"background": "transparent",
"data": [{"b64_json": RESULT}],
"data": [{"b64_json": RESULT, "generation_id": "gen-transparent"}],
})))
.expect(1)
.mount(server)
@@ -3099,6 +3099,7 @@ fn append_resume_redaction_history(
failure: None,
saved_path: Some(test_absolute_path("/tmp/ig-1.png")),
input_images: Vec::new(),
generation_id: None,
}),
]
.into_iter()
@@ -660,6 +660,7 @@ mod tests {
failure: None,
saved_path: Some(expected_path.clone()),
input_images: Vec::new(),
generation_id: None,
})],
},
)
@@ -114,6 +114,7 @@ fn legacy_end_event(item: &ImageGenerationItem) -> EventMsg {
failure: item.failure.clone(),
saved_path: item.saved_path.clone(),
input_images: item.input_images.clone(),
generation_id: item.generation_id.clone(),
})
}
+2 -4
View File
@@ -61,10 +61,8 @@ pub struct ImageGenerationItem {
#[schemars(skip)]
#[ts(skip)]
pub imagegen_request_id: Option<String>,
/// Backend generation ID for this exact image, retained only for in-process analytics.
#[serde(skip)]
#[schemars(skip)]
#[ts(skip)]
/// Image generation identifier for the generated image, when provided by the image backend.
#[serde(default)]
pub generation_id: Option<String>,
}
+9 -3
View File
@@ -43,6 +43,7 @@ fn image_generation_item_preserves_stable_wire_shape() {
"result": "cG5n",
"transparentBackground": null,
"failure": null,
"generationId": null,
})
);
assert_eq!(
@@ -126,6 +127,7 @@ fn image_generation_item_reads_transitional_file_wire_shape() {
"fileId": "file-1",
"transparentBackground": null,
"failure": null,
"generationId": null,
})
);
}
@@ -173,6 +175,7 @@ fn image_generation_item_preserves_authoritative_transparency() {
"result": "cG5n",
"transparentBackground": true,
"failure": null,
"generationId": null,
})
);
assert_eq!(
@@ -205,16 +208,19 @@ fn image_generation_request_id_stays_internal() {
let value = serde_json::to_value(&item).expect("serialize extension item");
assert!(value.get("imagegenRequestId").is_none());
assert!(value.get("generationId").is_none());
assert_eq!(value["generationId"], json!("gen-image-123"));
let ExtensionItem::ImageGeneration(round_tripped) =
serde_json::from_value::<ExtensionItem>(value).expect("deserialize extension item")
else {
panic!("expected image-generation item");
};
assert_eq!(round_tripped.imagegen_request_id, None);
assert_eq!(round_tripped.generation_id, None);
assert_eq!(
round_tripped.generation_id.as_deref(),
Some("gen-image-123")
);
assert!(!ImageGenerationItem::inline().contains("imagegenRequestId"));
assert!(!ImageGenerationItem::inline().contains("generationId"));
assert!(ImageGenerationItem::inline().contains("generationId: string | null"));
}
#[test]
+1
View File
@@ -471,6 +471,7 @@ impl ImageGenerationItem {
failure: None,
saved_path: self.saved_path.clone(),
input_images: Vec::new(),
generation_id: None,
})
}
}
+3
View File
@@ -3035,6 +3035,9 @@ pub struct ImageGenerationEndEvent {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
pub input_images: Vec<ImageGenerationInputImage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub generation_id: Option<String>,
}
// Conversation kept for backward compatibility.
+1
View File
@@ -69,6 +69,7 @@ fn legacy_image_generation_preserves_file_reference() {
failure: None,
saved_path: None,
input_images: Vec::new(),
generation_id: None,
});
let persisted = persisted_event_msg(&event, ThreadHistoryMode::Legacy)
@@ -223,7 +223,7 @@ pub(super) fn completed_item(
saved_path: event.saved_path.clone(),
input_images: event.input_images.clone(),
imagegen_request_id: None,
generation_id: None,
generation_id: event.generation_id.clone(),
})),
None,
)),
@@ -558,6 +558,7 @@ async fn migration_preserves_image_generation_failure_metadata() {
failure: expected_item.failure.clone(),
saved_path: expected_item.saved_path.clone(),
input_images: Vec::new(),
generation_id: expected_item.generation_id.clone(),
}));
let path = write_rollout(
home.path(),
@@ -609,7 +610,7 @@ async fn migration_preserves_image_generation_input_observations() {
sha256: "ab".repeat(32),
}],
imagegen_request_id: None,
generation_id: None,
generation_id: Some("gen-image-123".to_string()),
};
let image_completion =
RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent {
@@ -621,6 +622,7 @@ async fn migration_preserves_image_generation_input_observations() {
failure: expected_item.failure.clone(),
saved_path: expected_item.saved_path.clone(),
input_images: expected_item.input_images.clone(),
generation_id: expected_item.generation_id.clone(),
}));
let path = write_rollout(
home.path(),
@@ -5558,6 +5558,13 @@ class FileIdThreadItem(BaseModel):
),
] = None
failure: ImageGenerationFailure | None = None
generation_id: Annotated[
str | None,
Field(
alias="generationId",
description="Image generation identifier for the generated image, when provided by the image backend.",
),
] = None
id: str
input_images: Annotated[
list[ImageGenerationInputImage] | None,
@@ -5593,6 +5600,13 @@ class ResultThreadItem(BaseModel):
),
] = None
failure: ImageGenerationFailure | None = None
generation_id: Annotated[
str | None,
Field(
alias="generationId",
description="Image generation identifier for the generated image, when provided by the image backend.",
),
] = None
id: str
input_images: Annotated[
list[ImageGenerationInputImage] | None,