fix(codex): drop oversized message ids on Responses input replay

Codex assigns assistant message items server-side ids that can run
400+ chars (base64 encrypted blobs), but the Responses API caps
input[].id at 64 chars and rejects the whole request with a
non-retryable HTTP 400. Once a session captures one of these long
ids, every subsequent turn replays it and 400s forever, since the
history persists it in codex_message_items.

Add a 64-char length guard at both replay sites — the history-to-
input converter and the final preflight gate — so oversized ids are
dropped while short ids (msg_...) are kept for prefix-cache hits.
Mirrors the existing pattern for reasoning items, which already
strip their id before replay because store=False means the API
can't resolve ids server-side anyway.

Fixes #27038

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
joaomarcos 2026-07-10 15:04:56 -03:00 committed by Teknium
parent dabae386ef
commit 0b2907f586
2 changed files with 137 additions and 2 deletions

View file

@ -3,9 +3,11 @@ from types import SimpleNamespace
import pytest
from agent.codex_responses_adapter import (
_chat_messages_to_responses_input,
_format_responses_error,
_normalize_codex_response,
_preflight_codex_api_kwargs,
_preflight_codex_input_items,
)
@ -139,6 +141,128 @@ def test_normalize_codex_response_in_progress_message_still_incomplete():
assert finish_reason == "incomplete"
# ---------------------------------------------------------------------------
# Replayed assistant message items with an oversized server-assigned ``id``
# (Codex issues 400+ char base64 blobs) must never reach the API — the
# Responses endpoint caps input[].id at 64 chars and rejects the whole
# request with a non-retryable HTTP 400, permanently bricking the session
# (every subsequent turn replays the same bad id). Short ids (msg_...) are
# still worth keeping for prefix-cache hits, so this is a length guard, not
# a blanket strip.
# ---------------------------------------------------------------------------
_OVERSIZED_ITEM_ID = "x" * 408
_VALID_ITEM_ID = "msg_abc123"
def test_chat_messages_to_responses_input_drops_oversized_message_id():
messages = [
{
"role": "assistant",
"content": "pong",
"codex_message_items": [
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "pong"}],
"id": _OVERSIZED_ITEM_ID,
"phase": "final_answer",
}
],
}
]
items = _chat_messages_to_responses_input(messages)
message_item = next(item for item in items if item.get("type") == "message")
assert "id" not in message_item
assert message_item["phase"] == "final_answer"
assert message_item["content"] == [{"type": "output_text", "text": "pong"}]
def test_chat_messages_to_responses_input_keeps_short_message_id():
messages = [
{
"role": "assistant",
"content": "pong",
"codex_message_items": [
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "pong"}],
"id": _VALID_ITEM_ID,
}
],
}
]
items = _chat_messages_to_responses_input(messages)
message_item = next(item for item in items if item.get("type") == "message")
assert message_item["id"] == _VALID_ITEM_ID
def test_preflight_codex_input_items_drops_oversized_message_id():
items = _preflight_codex_input_items(
[
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "pong"}],
"id": _OVERSIZED_ITEM_ID,
"phase": "final_answer",
}
]
)
assert "id" not in items[0]
assert items[0]["phase"] == "final_answer"
def test_preflight_codex_input_items_keeps_short_message_id():
items = _preflight_codex_input_items(
[
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "pong"}],
"id": _VALID_ITEM_ID,
}
]
)
assert items[0]["id"] == _VALID_ITEM_ID
def test_preflight_codex_api_kwargs_drops_oversized_message_id_end_to_end():
kwargs = _preflight_codex_api_kwargs(
{
"model": "gpt-5.5",
"instructions": "You are Hermes.",
"input": [
{"role": "user", "content": "ping"},
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "pong"}],
"id": _OVERSIZED_ITEM_ID,
"phase": "final_answer",
},
],
"tools": [],
"store": False,
}
)
message_item = next(item for item in kwargs["input"] if item.get("type") == "message")
assert "id" not in message_item
# ---------------------------------------------------------------------------
# _preflight_codex_api_kwargs — built-in (provider-executed) tools must pass
# through validation. Regression guard for the xAI native web_search