openai / openai/openai-java

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

Open
#710 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug spec
Dominant language
Kotlin
Stars
1.5k
Forks
264
Avg merge
9h 46m
Merged PRs (30d)
96

Description

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 Java, JS, Python, Go, .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: previousResponseId(). For manual history, always pass reasoning+message pairs together.

Related:

  • openai/openai-openapi#536 (spec root cause)
  • openai/openai-node#1791 (same bug, Node SDK)
  • openai/openai-python#TBD (same bug, Python SDK)
  • openai/openai-dotnet#TBD (same bug, .NET SDK)
  • openai/openai-go#TBD (same bug, Go SDK)
  • openclaw/openclaw#49167 (64.9k forks, production breakage)
To Reproduce
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.*;

import java.util.ArrayList;
import java.util.List;

public class PairingConstraintRepro {
    public static void main(String[] args) {
        OpenAIClient client = OpenAIOkHttpClient.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .build();

        String[] prompts = {"Write a Python prime checker.", "Add type hints.", "Add docstrings."};
        List<ResponseInputUnionParam> conversation = new ArrayList<>();

        for (String msg : prompts) {
            System.out.println("\n> " + msg);
            conversation.add(ResponseInputUnionParam.ofEasyInputMessage(
                EasyInputMessageParam.builder()
                    .role(EasyInputMessageParam.Role.USER)
                    .content(msg)
                    .build()));

            try {
                Response response = client.responses().create(
                    ResponseCreateParams.builder()
                        .model("gpt-5.3-codex")
                        .input(ResponseCreateParams.Input.ofResponseInputs(conversation))
                        .maxOutputTokens(300)
                        .reasoning(Reasoning.builder().effort(Reasoning.Effort.HIGH).build())
                        .build());

                // Common pattern: keep only messages, discard reasoning
                for (ResponseOutputItem item : response.output()) {
                    if (item.isMessage()) {
                        // Convert output message to input — orphan message → 400
                        conversation.add(ResponseInputUnionParam.ofResponseInputItem(
                            ResponseInputItemParam.builder()
                                .id(item.asMessage().id())
                                .type(ResponseInputItemParam.Type.MESSAGE)
                                .build()));
                    }
                }
            } catch (Exception e) {
                System.out.println("  ERROR: " + e.getMessage().substring(0, Math.min(120, e.getMessage().length())));
                break;
            }
        }
        // Turn 2 → 400: Item 'msg_...' was provided without its required preceding item
    }
}

Note: the Java SDK's distinct input/output wrapper types make the conversion verbose, but the pairing constraint failure is the same regardless.

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

OS

Windows 11, also reproduced on Linux

Java version

Java 21

Library version

openai-java v4.29.0

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Responses API flow shown in PairingConstraintRepro, especially ResponseInputUnionParam, ResponseCreateParams, response.output(), and previousResponseId(). Reproduce the second-turn failure with the listed models and determine whether the remedy belongs in this SDK or the API; done requires a clearly identified, testable ownership and behavior change.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.