Skip to content

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

@achandmsft

Description

@achandmsft

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

Related:

To Reproduce

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/openai/openai-go"
	"github.com/openai/openai-go/responses"
)

func main() {
	client := openai.NewClient(openai.WithAPIKey(os.Getenv("OPENAI_API_KEY")))
	ctx := context.Background()

	prompts := []string{"Write a Python prime checker.", "Add type hints.", "Add docstrings."}
	var conversation []responses.ResponseInputUnionParam

	for _, msg := range prompts {
		fmt.Printf("\n> %s\n", msg)
		conversation = append(conversation, responses.ResponseInputUnionParam{
			OfEasyInputMessage: &responses.EasyInputMessageParam{
				Role:    "user",
				Content: responses.EasyInputMessageContentUnionParam{OfString: &msg},
			},
		})

		resp, err := client.Responses.New(ctx, responses.ResponseNewParams{
			Model:           "gpt-5.3-codex",
			Input:           responses.ResponseNewParamsInputUnion{OfResponseInputs: conversation},
			MaxOutputTokens: openai.Int(300),
			Reasoning: &responses.ReasoningParam{
				Effort: "high",
			},
		})
		if err != nil {
			fmt.Printf("  ERROR: %v\n", err)
			break
		}

		// Common pattern: keep only messages, discard reasoning
		for _, item := range resp.Output {
			if item.Type == "message" {
				conversation = append(conversation, responses.ResponseInputUnionParam{
					OfMessage: &responses.ResponseInputMessageParam{
						ID:      item.ID,
						Type:    "message",
						Role:    "assistant",
						Content: item.Content, // needs conversion in practice
					},
				})
			}
		}
	}
	// Turn 2 → 400: Item 'msg_...' was provided without its required preceding item
}

Note: the Go 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

Go version

Go 1.24

Library version

openai-go v3.29.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions