Skip to content

Responses API: undocumented reasoning+message pairing constraint breaks multi-turn conversations #3009

@achandmsft

Description

@achandmsft

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

Reasoning+message items must appear as consecutive pairs in input, but nothing documents this. The most common pattern — filtering response.output to keep only messages — silently produces orphaned items → 400 on the next turn.

This broke OpenClaw (64.9k forks) on gpt-5.3-codex. They had to add downgradeOpenAIReasoningBlocks() to strip orphan reasoning items.

400: Item 'msg_...' of type 'message' was provided without its required preceding item of type 'reasoning'

Tested in Python, JS, Go, Java, .NET — identical results across all 5 SDKs. This is an API-level constraint, not SDK-specific. Confirmed via curl (see gist). The SDK types don't prevent building input arrays that violate it.

Model A (all items) B (msgs only)
gpt-5.3-codex (reasoning=high) PASS FAIL
o4-mini PASS FAIL*

* Nondeterministic — o4-mini sometimes returns reasoning-only output (no message to orphan). Codex with reasoning=high reliably returns both items.

Workaround: previous_response_id. For manual history, always pass reasoning+message pairs together.

Note: there's a separate Python-specific bug where model_dump() fabricates null fields (status: None, encrypted_content: None) → 400: Unknown parameter. That's tracked in #3008 and is independent of this pairing constraint.

Related:

To Reproduce

from openai import OpenAI, APIError

client = OpenAI()
conversation = []

for msg in ["Write a Python prime checker.", "Add type hints.", "Add docstrings."]:
    conversation.append({"role": "user", "content": msg})
    try:
        response = client.responses.create(
            model="gpt-5.3-codex",
            input=conversation,
            max_output_tokens=300,
            reasoning={"effort": "high"},
        )
        # Common pattern: keep only messages, discard reasoning
        for item in response.output:
            if item.type == "message":
                conversation.append(item.model_dump(exclude_none=True))
    except APIError as e:
        print(f"ERROR: {e.status_code} - {e.message}")
        break
# Turn 2 → 400: Item 'msg_...' was provided without its required preceding item

Reproduced on o4-mini and gpt-5.3-codex. Full cross-language repro (Python, JS, .NET, curl): https://gist.github.com/achandmsft/57886350885cec3af8ef3f456ed529cf

Code snippets

OS

Windows 11, also reproduced on Linux

Python version

Python v3.13

Library version

openai v2.29.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions