Show failed Codex image generations in the chat (#5810)

When a Codex image request fails, Codex completes the imageGeneration
item with status "failed", an empty result, and an optional failure
record. The image mapper only knew how to render a successful image, so
it returned nothing and the item vanished from the live timeline and
from reopened history. Failed items now map to a failed image_generation
tool call carrying the failure details.

Closes #5619

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: 3ae3ae <83870598+3ae3ae@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-01 03:57:56 +02:00
committed by GitHub
co-authored by 3ae3ae
parent 4893629ff5
commit b701cb2dcd
2 changed files with 110 additions and 0 deletions
@@ -6192,6 +6192,107 @@ describe("Codex app-server provider", () => {
]);
});
test.each([
[
"a usage limit",
{ type: "usageLimitExceeded", limitId: "image_gen", resetsAt: 1786150800 },
{ type: "usageLimitExceeded", limitId: "image_gen", resetsAt: 1786150800 },
],
["no failure details", null, { message: "Image generation failed" }],
])(
"emits failed imageGeneration items with %s as failed tool calls",
(_label, failure, expectedError) => {
const session = createSession();
const events: AgentStreamEvent[] = [];
session.subscribe((event) => events.push(event));
asInternals(session).handleNotification("item/completed", {
item: {
id: "image-generation-failed",
type: "imageGeneration",
status: "failed",
revisedPrompt: "paint a blue whale",
result: "",
transparentBackground: null,
failure,
savedPath: null,
},
});
expect(events).toEqual([
{
type: "timeline",
provider: "codex",
turnId: "test-turn",
item: {
type: "tool_call",
callId: "image-generation-failed",
name: "image_generation",
status: "failed",
error: expectedError,
detail: {
type: "unknown",
input: { prompt: "paint a blue whale" },
output: null,
},
},
},
]);
},
);
test("keeps failed imageGeneration items in persisted history", async () => {
const session = createSession();
session.client = {
request: vi.fn(async () => ({
thread: {
turns: [
{
items: [
{
type: "imageGeneration",
id: "image-generation-failed",
status: "failed",
revisedPrompt: "paint a blue whale",
result: "",
transparentBackground: null,
failure: { type: "usageLimitExceeded", limitId: "image_gen", resetsAt: null },
savedPath: null,
},
],
},
],
},
})),
};
await asInternals(session).loadPersistedHistory(session.client);
const history: AgentStreamEvent[] = [];
for await (const event of session.streamHistory()) {
history.push(event);
}
expect(history).toEqual([
{
type: "timeline",
provider: "codex",
item: {
type: "tool_call",
callId: "image-generation-failed",
name: "image_generation",
status: "failed",
error: { type: "usageLimitExceeded", limitId: "image_gen", resetsAt: null },
detail: {
type: "unknown",
input: { prompt: "paint a blue whale" },
output: null,
},
},
},
]);
});
test("emits usage_updated on token usage updates and keeps usage on turn completion", () => {
const session = createSession();
const events: AgentStreamEvent[] = [];
@@ -1949,6 +1949,15 @@ function mapCodexThreadImageItem(
});
}
if (normalizedItem.status === "failed") {
return mapCodexToolCallEnvelope({
callId: firstStringField(normalizedItem, ["id"]),
name: "image_generation",
input: { prompt: firstStringField(normalizedItem, ["revisedPrompt", "revised_prompt"]) },
error: normalizedItem.failure ?? { message: "Image generation failed" },
});
}
const savedPath = firstStringField(normalizedItem, ["savedPath", "saved_path"]);
const result = codexImageOutputFromResult(normalizedItem.result);
return renderProviderImageOutputAsAssistantMarkdown(