hermes - ✅(Solved) Fix Codex Responses API 400: input_text invalid on assistant message content [2 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#15687Fetched 2026-04-26 05:25:49
View on GitHub
Comments
1
Participants
1
Timeline
11
Reactions
0
Participants
Timeline (top)
labeled ×4referenced ×3cross-referenced ×2closed ×1

When using Codex models (codex_responses api_mode), the agent crashes with a 400 error when replaying conversation history that contains assistant messages with multimodal content (list-format content array). The Responses API rejects input_text as a content type for assistant messages — only output_text and refusal are valid.

Error Message

Non-retryable client error: Error code: 400 - {
  'error': {
    'message': "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.",
    'type': 'invalid_request_error',
    'param': 'input[109].content[0]',
    'code': 'invalid_value'
  }
}

Root Cause

In agent/codex_responses_adapter.py, the function _chat_content_to_responses_parts() hardcodes all text content to type "input_text" (lines 62 and 70):

converted.append({"type": "input_text", "text": part})   # line 62
converted.append({"type": "input_text", "text": text})    # line 70

This function is called from _chat_messages_to_responses_input() for both user and assistant messages (line 236):

content_parts = _chat_content_to_responses_parts(content)

For assistant messages, the content parts are then emitted at line 265-266:

if content_parts:
    items.append({"role": "assistant", "content": content_parts})

But in the Codex Responses API, assistant message content parts must use "output_text", not "input_text". The API spec only allows "output_text" and "refusal" as content types within assistant messages.

Fix Action

Fixed

PR fix notes

PR #15690: fix: use output_text for assistant content in Codex Responses API

Description (problem / solution / changelog)

Summary

Fixes #15687 — the Codex Responses API rejects input_text content type inside assistant messages. Only output_text and refusal are valid for assistant role.

Root Cause

_chat_content_to_responses_parts() in agent/codex_responses_adapter.py hardcoded all text content to type input_text regardless of the message role. When an assistant message had list-format content (multimodal or structured), the resulting input_text parts were sent to the API, which rejected them:

Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.
param: 'input[109].content[0]'

Fix

Added a role parameter to _chat_content_to_responses_parts() that selects the correct content type:

  • User messages → input_text
  • Assistant messages → output_text

Threaded this role through all three functions that handle content:

  1. _chat_content_to_responses_parts(content, role=role) — emits correct type
  2. _chat_messages_to_responses_input() — passes role, filters on correct type
  3. _preflight_codex_input_items() — preserves correct type per role during validation

Why it only triggers with list content

When assistant content is a plain string (the common case), it bypasses _chat_content_to_responses_parts() entirely and is emitted directly as a string. The bug only manifests when assistant content is a list of parts — which happens with multimodal messages or structured content.

Test plan

  • 4 new tests in test_provider_parity.py:
    • test_user_multimodal_content_uses_input_text
    • test_assistant_multimodal_content_uses_output_text
    • test_preflight_preserves_assistant_output_text
    • test_full_round_trip_with_list_content
  • 5 new unit tests for _chat_content_to_responses_parts role parameter
  • All 85 existing tests + 24 transport tests + 50 codex response tests pass
  • E2E verified with real imports

Files changed

  • agent/codex_responses_adapter.py — 3 functions updated (role-aware content types)
  • tests/run_agent/test_provider_parity.py — 9 new tests

Changed files

  • agent/codex_responses_adapter.py (modified, +23/-10)
  • tests/run_agent/test_provider_parity.py (modified, +106/-1)

PR #15691: fix(codex-responses): use output_text for assistant message content (#15687)

Description (problem / solution / changelog)

Summary

  • _chat_content_to_responses_parts() was hardcoding \"input_text\" for all roles; the Responses API only allows \"output_text\" and \"refusal\" inside assistant messages
  • Add a role keyword arg to derive the correct text type; fix the call site and the validation pass in _preflight_codex_input_items()
  • Add 7 regression tests covering both roles, the edge cases, and the root-cause repro scenario

The bug

_chat_content_to_responses_parts() in agent/codex_responses_adapter.py unconditionally emits {"type": "input_text", ...} parts. When _chat_messages_to_responses_input() processes an assistant message whose content field is a list (structured/multimodal content — occurs after ~100 turns or when the model produces tool calls alongside text), the resulting parts get type input_text. The Responses API rejects this with:

Error code: 400 — 'input_text' is invalid for input[N].content[M] on assistant messages.
Supported values are: 'output_text' and 'refusal'.

The _preflight_codex_input_items() validation pass had the same issue: it normalised all text parts to input_text regardless of role, so even correctly-typed output_text parts got coerced before the API call.

The fix

# Before
def _chat_content_to_responses_parts(content: Any) -> ...:
    ...
    converted.append({"type": "input_text", "text": part})

# After
def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> ...:
    text_type = "output_text" if role == "assistant" else "input_text"
    ...
    converted.append({"type": text_type, "text": part})

Call site passes role=role; content_text extraction is widened to match both "input_text" and "output_text". _preflight_codex_input_items() derives text_type per item role before the inner validation loop.

Test plan

  • Before: reverted fix → test_chat_content_to_responses_parts_assistant_uses_output_text, test_chat_messages_to_responses_input_assistant_multimodal_uses_output_text, and test_preflight_codex_input_preserves_output_text_for_assistant all fail with AssertionError: 'input_text' == 'output_text'
  • After: 57 passed (50 existing + 7 new) in tests/run_agent/test_run_agent_codex_responses.py
  • Regression guard: confirmed test failures match the exact bug repro before restoring fix
  • Adjacent suite tests/agent/ — 1936 passed, 16 pre-existing failures (anthropic_adapter + bedrock_adapter) reproduce identically on clean origin/main

Related

  • Fixes #15687

🤖 Generated with Claude Code

Changed files

  • agent/codex_responses_adapter.py (modified, +21/-10)
  • tests/run_agent/test_run_agent_codex_responses.py (modified, +111/-0)

Code Example

Non-retryable client error: Error code: 400 - {
  'error': {
    'message': "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.",
    'type': 'invalid_request_error',
    'param': 'input[109].content[0]',
    'code': 'invalid_value'
  }
}

---

converted.append({"type": "input_text", "text": part})   # line 62
converted.append({"type": "input_text", "text": text})    # line 70

---

content_parts = _chat_content_to_responses_parts(content)

---

if content_parts:
    items.append({"role": "assistant", "content": content_parts})

---

def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> List[Dict[str, Any]]:
    text_type = "output_text" if role == "assistant" else "input_text"
    # ... use text_type instead of hardcoded "input_text"

---

content_parts = _chat_content_to_responses_parts(content, role=role)

---

Non-retryable client error: Error code: 400 - {'error': {'message': "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.", 'type': 'invalid_request_error', 'param': 'input[109].content[0]', 'code': 'invalid_value'}}
RAW_BUFFERClick to expand / collapse

Codex Responses API 400: input_text invalid on assistant messages

Summary

When using Codex models (codex_responses api_mode), the agent crashes with a 400 error when replaying conversation history that contains assistant messages with multimodal content (list-format content array). The Responses API rejects input_text as a content type for assistant messages — only output_text and refusal are valid.

Error

Non-retryable client error: Error code: 400 - {
  'error': {
    'message': "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.",
    'type': 'invalid_request_error',
    'param': 'input[109].content[0]',
    'code': 'invalid_value'
  }
}

Root Cause

In agent/codex_responses_adapter.py, the function _chat_content_to_responses_parts() hardcodes all text content to type "input_text" (lines 62 and 70):

converted.append({"type": "input_text", "text": part})   # line 62
converted.append({"type": "input_text", "text": text})    # line 70

This function is called from _chat_messages_to_responses_input() for both user and assistant messages (line 236):

content_parts = _chat_content_to_responses_parts(content)

For assistant messages, the content parts are then emitted at line 265-266:

if content_parts:
    items.append({"role": "assistant", "content": content_parts})

But in the Codex Responses API, assistant message content parts must use "output_text", not "input_text". The API spec only allows "output_text" and "refusal" as content types within assistant messages.

When it triggers

This error occurs when:

  1. The agent is using codex_responses api_mode (e.g. openai-codex provider, OpenAI gpt-5.4 via Codex)
  2. A conversation has been running for enough turns to accumulate significant history (the error shows input[109] — position 109 in the input array)
  3. At least one assistant message in the history has content stored in list format (multimodal content or structured content), rather than as a plain string

When assistant content is a plain string (the common case), it bypasses _chat_content_to_responses_parts() and is emitted directly as a string, which the API accepts. The bug only manifests when assistant content is a list of parts.

Steps to reproduce

  1. Start a Hermes session with openai-codex provider or any codex_responses api_mode backend
  2. Have a multi-turn conversation where the model produces structured content (tool calls with text content alongside, or multimodal content)
  3. Continue the conversation long enough for the history to accumulate ~100+ input items
  4. The next API call will fail with the 400 error on the first assistant message that has list-format content

Suggested fix

Add a role parameter to _chat_content_to_responses_parts():

def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> List[Dict[str, Any]]:
    text_type = "output_text" if role == "assistant" else "input_text"
    # ... use text_type instead of hardcoded "input_text"

Then in _chat_messages_to_responses_input(), pass the role:

content_parts = _chat_content_to_responses_parts(content, role=role)

Logs

From ~/.hermes/logs/errors.log (2026-04-25 20:25:01):

Non-retryable client error: Error code: 400 - {'error': {'message': "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.", 'type': 'invalid_request_error', 'param': 'input[109].content[0]', 'code': 'invalid_value'}}

The agent fell back to gpt-5.4 (openai-codex) after the primary model (anthropic/claude-opus-4.6) triggered a fallback. The session ID was 20260425_201607_69a91a.

extent analysis

TL;DR

Modify the _chat_content_to_responses_parts() function to use "output_text" for assistant messages and "input_text" for user messages.

Guidance

  • Identify the role of the message (assistant or user) before calling _chat_content_to_responses_parts() to determine the correct content type.
  • Update the _chat_content_to_responses_parts() function to accept a role parameter and use it to set the text_type variable.
  • Pass the role parameter when calling _chat_content_to_responses_parts() from _chat_messages_to_responses_input().
  • Verify the fix by testing the conversation history with assistant messages containing multimodal content.

Example

def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> List[Dict[str, Any]]:
    text_type = "output_text" if role == "assistant" else "input_text"
    converted = []
    for part in content:
        converted.append({"type": text_type, "text": part})
    return converted

Notes

This fix assumes that the role parameter is correctly passed from _chat_messages_to_responses_input(). Additional testing may be necessary to ensure the fix works for all scenarios.

Recommendation

Apply the suggested fix to update the _chat_content_to_responses_parts() function to handle assistant messages correctly. This should resolve the 400 error issue with the Codex Responses API.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING