From b80b9d82719455c9a88b32259c701e211b78b871 Mon Sep 17 00:00:00 2001 From: lEWFkRAD Date: Thu, 27 Aug 2026 22:48:59 -0400 Subject: [PATCH] fix(codex): preserve assistant image slots in replay --- agent/codex_responses_adapter.py | 17 +++++++++++++++-- tests/agent/test_codex_responses_adapter.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index df90e81388..909437a6ab 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -194,8 +194,13 @@ def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> Lis continue if ptype in {"image_url", "input_image"}: if role == "assistant": - # Responses API rejects input_image on assistant messages - # (HTTP 400 on every replay) — drop, see docstring (#96816). + # Responses output messages cannot carry input_image. Keep a + # text marker so image-only assistant turns still survive in + # replay and later references retain their conversational slot. + converted.append({ + "type": "output_text", + "text": "[Assistant image omitted during replay]", + }) continue image_ref = part.get("image_url") detail = part.get("detail") @@ -1167,6 +1172,14 @@ def _preflight_codex_input_items( text = str(text or "") validated.append({"type": text_type, "text": sanitize_text(text)}) elif ptype in {"input_image", "image_url"}: + if role == "assistant": + # Enforce the same output-message invariant for + # raw request overrides as for normal history. + validated.append({ + "type": "output_text", + "text": "[Assistant image omitted during replay]", + }) + continue image_ref = part.get("image_url", "") detail = part.get("detail") if isinstance(image_ref, dict): diff --git a/tests/agent/test_codex_responses_adapter.py b/tests/agent/test_codex_responses_adapter.py index c9a6d7696c..d8da67a67b 100644 --- a/tests/agent/test_codex_responses_adapter.py +++ b/tests/agent/test_codex_responses_adapter.py @@ -30,6 +30,8 @@ def test_chat_content_drops_images_from_assistant_role(): assert _chat_content_to_responses_parts(content, role="assistant") == [ {"type": "output_text", "text": "generated image"}, + {"type": "output_text", "text": "[Assistant image omitted during replay]"}, + {"type": "output_text", "text": "[Assistant image omitted during replay]"}, ] @@ -46,6 +48,24 @@ def test_chat_content_keeps_images_on_user_role(): }] +def test_preflight_rewrites_raw_assistant_images_to_text_markers(): + raw = [{ + "role": "assistant", + "content": [{ + "type": "input_image", + "image_url": "https://example.invalid/p.png", + }], + }] + + assert _preflight_codex_input_items(raw) == [{ + "role": "assistant", + "content": [{ + "type": "output_text", + "text": "[Assistant image omitted during replay]", + }], + }] + + def _harmony_token(name: str) -> str: """Build a literal Harmony token without spelling it contiguously here.""" return f"<\x7c{name}\x7c>"