mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-16 14:32:34 +00:00
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:
parent
dabae386ef
commit
0b2907f586
2 changed files with 137 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue