openclaw - ✅(Solved) Fix [Bug]: 2026.4.26 injects unsupported metadata field into openai-codex Responses requests; ChatGPT backend returns HTTP 400, surfaced as "400 status code (no body)" [1 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
openclaw/openclaw#73963Fetched 2026-04-30 06:30:16
View on GitHub
Comments
1
Participants
1
Timeline
7
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×4commented ×1referenced ×1subscribed ×1

After upgrading to 2026.4.26, every request the openai-codex provider sends to https://chatgpt.com/backend-api/codex/responses is rejected by the ChatGPT codex backend with HTTP 400 {"detail":"Unsupported parameter: metadata"}. The error is surfaced to the user as the misleading string 400 status code (no body) because the SDK error parser expects an {"error":{"message":...}} shape and falls through. All openai-codex/* models on the ChatGPT subscription path are unusable until the provider stops attaching metadata to codex requests.

Error Message

{"detail":"Unsupported parameter: metadata"}. The error is surfaced to SDK error parser expects an {"error":{"message":...}} shape and falls error=400 status code (no body) rawError=400 status code (no body) {"error":{"message":...}} form the SDK error parser expects. error=400 status code (no body) rawError=400 status code (no body) misleading "400 (no body)" error. The misleading message has caused Possible secondary improvement. The pi-ai error parser falling detail string when no error.message is present would have made this

Root Cause

After upgrading to 2026.4.26, every request the openai-codex provider sends to https://chatgpt.com/backend-api/codex/responses is rejected by the ChatGPT codex backend with HTTP 400 {"detail":"Unsupported parameter: metadata"}. The error is surfaced to the user as the misleading string 400 status code (no body) because the SDK error parser expects an {"error":{"message":...}} shape and falls through. All openai-codex/* models on the ChatGPT subscription path are unusable until the provider stops attaching metadata to codex requests.

Fix Action

Fix / Workaround

The codex /backend-api/codex/responses endpoint validates a stricter parameter allowlist than api.openai.com/v1/responses and does not accept metadata. buildOpenAIResponsesParams then injects that metadata into the request body for both endpoints (...metadata ? { metadata } : {}), and mergeTransportMetadata merges it again after onPayload. There is no payload-policy gate for metadata (only store, service_tier, prompt_cache_*), so users have no config-side workaround; #10140 had previously asked for exactly such a configurable strip layer.

Suggested fix (smallest surface). In resolveOpenAITransportTurnState, omit metadata when the provider is openai-codex. Tracking via x-openclaw-* headers is preserved; the codex backend ignores arbitrary x- headers. Drop-in patch verified locally:

Verified: same openclaw agent --local --message "say hi only" --model openai-codex/gpt-5.4-mini returns the assistant reply after applying this patch in dist/transport-policy-*.js. Also confirmed the manual curl with the unpatched body and metadata: {…} reproduces the exact 400 with Unsupported parameter: metadata, while removing the field returns a normal SSE stream.

PR fix notes

PR #74029: fix(agents): strip metadata for openai-codex Responses to avoid ChatGPT 400 (#73963)

Description (problem / solution / changelog)

Fixes #73963.

Problem

The ChatGPT codex backend at `https://chatgpt.com/backend-api/codex/responses\` rejects every request that carries a top-level `metadata` field with HTTP 400:

``` {"detail":"Unsupported parameter: metadata"} ```

OpenClaw 2026.4.26 attaches `metadata` (turn/session ids) unconditionally in `buildOpenAIResponsesParams`. The SDK error parser expects an `{"error":{"message":...}}` envelope and falls through, surfacing the backend rejection to the user as the misleading string `400 status code (no body)`. All `openai-codex/*` models on the ChatGPT subscription path are unusable until the provider stops attaching `metadata`.

Fix

Strip `metadata` for `isCodexResponses` requests; keep it for `openai-responses` and `azure-openai-responses` where the field is honored. One-line conditional:

```ts ...(metadata && !isCodexResponses ? { metadata } : {}), ```

Confirmed via curl in #73963: the same payload without `metadata` returns the assistant message normally on the same OAuth token.

What changed

FileChangeLOC
`openai-transport-stream.ts`Strip metadata for isCodexResponses+9
`openai-transport-stream.test.ts`Update existing Codex test + 2 new regression tests+60
`CHANGELOG.md`Unreleased Fixes line+1

The pre-existing test `"uses top-level instructions for Codex responses without dropping parity fields"` was asserting `params.metadata` IS attached for Codex Responses. That test was actually capturing the bug (codified the broken behavior). Updated it to assert metadata is now `undefined` for Codex Responses, with a comment citing #73963.

Tests

``` pnpm vitest run src/agents/openai-transport-stream.test.ts → 92 passed (90 existing + 2 new regression tests, +1 existing test updated to assert new contract) ```

Coordination with #73930

This PR touches the same file as my open #73930 (codex empty-input fail-fast). The two fixes are at different lines — #73930 adds a guard before `convertResponsesMessages`, this PR conditionally spreads `metadata` later in the params object. Both can land independently or together; if Pete prefers one squashed PR I can rebase #73930 into this one.

🦞 lobster-biscuit


Sign-Off: hclsys

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/openai-transport-stream.test.ts (modified, +66/-4)
  • src/agents/openai-transport-stream.ts (modified, +8/-1)

Code Example

openclaw agent --local --session-id repro-$$ \
     --message "say hi only" \
     --model openai-codex/gpt-5.4-mini --thinking low

---

=== REQ POST https://chatgpt.com/backend-api/codex/responses ===
BODY (parsed top-level keys):
  ['model', 'input', 'stream', 'prompt_cache_key', 'instructions',
   'metadata', 'tools', 'reasoning', 'include', 'store', 'text',
   'parallel_tool_calls']
  metadata: {
    "openclaw_session_id": "...",
    "openclaw_turn_id": "...",
    "openclaw_turn_attempt": "1",
    "openclaw_transport": "stream"
  }

RESP status=400
  content-length: 44
  content-type: application/json
  x-oai-request-id: a9cbd076-0b53-4b88-ba9c-ffa98cc2b98e
  body: {"detail":"Unsupported parameter: metadata"}

---

[agent/embedded] embedded run agent end: runId=... isError=true
  model=gpt-5.4-mini provider=openai-codex
  error=400 status code (no body) rawError=400 status code (no body)

---

return {
  headers: { ...sessionHeaders, "x-openclaw-turn-id": turnId, ... },
  metadata: {
    openclaw_session_id: sessionHeaders["x-openclaw-session-id"] ?? "",
    openclaw_turn_id: turnId,
    openclaw_turn_attempt: attempt,
    openclaw_transport: ctx.transport
  }
};

---

const isCodexProvider =
  normalizeProviderId(ctx.provider) === OPENAI_CODEX_PROVIDER_ID;
return {
  headers: {
    ...sessionHeaders,
    "x-openclaw-turn-id": turnId,
    "x-openclaw-turn-attempt": attempt,
  },
  ...isCodexProvider ? {} : {
    metadata: {
      openclaw_session_id: sessionHeaders["x-openclaw-session-id"] ?? "",
      openclaw_turn_id: turnId,
      openclaw_turn_attempt: attempt,
      openclaw_transport: ctx.transport,
    },
  },
};
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

After upgrading to 2026.4.26, every request the openai-codex provider sends to https://chatgpt.com/backend-api/codex/responses is rejected by the ChatGPT codex backend with HTTP 400 {"detail":"Unsupported parameter: metadata"}. The error is surfaced to the user as the misleading string 400 status code (no body) because the SDK error parser expects an {"error":{"message":...}} shape and falls through. All openai-codex/* models on the ChatGPT subscription path are unusable until the provider stops attaching metadata to codex requests.

Steps to reproduce

  1. OpenClaw 2026.4.26, ChatGPT Plus OAuth profile for the openai-codex provider, baseUrl https://chatgpt.com/backend-api (default).

  2. Run any agent call against a codex model, e.g.

    openclaw agent --local --session-id repro-$$ \
      --message "say hi only" \
      --model openai-codex/gpt-5.4-mini --thinking low
  3. Observe the agent fail immediately with 400 status code (no body).

Expected behavior

The request succeeds. Sending the same payload manually (without the metadata field) against the same endpoint with the same OAuth token returns the assistant message normally — confirmed via curl. Earlier OpenClaw releases (pre-resolveOpenAITransportTurnState) did not attach this body field for codex and worked.

Actual behavior

The request body contains a top-level metadata object the codex backend does not accept; backend responds with HTTP 400 and a 44-byte JSON body {"detail":"Unsupported parameter: metadata"}. OpenClaw logs only show error=400 status code (no body) rawError=400 status code (no body) because the body is shaped {"detail":...}, not the {"error":{"message":...}} form the SDK error parser expects.

OpenClaw version

2026.4.26 (be8c246)

Operating system

macOS (Darwin 25.4.0)

Install method

npm global

Model

openai-codex/gpt-5.4-mini (also reproduces on gpt-5.4, gpt-5.5)

Provider / routing chain

openclaw → openai-codex (ChatGPT subscription path, https://chatgpt.com/backend-api/codex/responses) via ChatGPT Plus OAuth

Additional provider/model setup details

Default routing. models.providers.openai-codex.api is openai-codex-responses and auth is oauth. The transport that fires is createOpenAIResponsesTransportStreamFn in provider-stream-*.js (uses the OpenAI SDK with client.responses.create, not the pi-ai streamOpenAICodexResponses SSE path).

Logs, screenshots, and evidence

Captured via a temporary appendFile in fetch-guard-*.js around the defaultFetch call, on the failing run:

=== REQ POST https://chatgpt.com/backend-api/codex/responses ===
BODY (parsed top-level keys):
  ['model', 'input', 'stream', 'prompt_cache_key', 'instructions',
   'metadata', 'tools', 'reasoning', 'include', 'store', 'text',
   'parallel_tool_calls']
  metadata: {
    "openclaw_session_id": "...",
    "openclaw_turn_id": "...",
    "openclaw_turn_attempt": "1",
    "openclaw_transport": "stream"
  }

RESP status=400
  content-length: 44
  content-type: application/json
  x-oai-request-id: a9cbd076-0b53-4b88-ba9c-ffa98cc2b98e
  body: {"detail":"Unsupported parameter: metadata"}

Sibling gateway-log evidence (~/.openclaw/logs/gateway.err.log) for multiple runIDs:

[agent/embedded] embedded run agent end: runId=... isError=true
  model=gpt-5.4-mini provider=openai-codex
  error=400 status code (no body) rawError=400 status code (no body)

Each line is preceded by [compaction-safeguard] Compaction safeguard: no real conversation messages to summarize… — secondary symptom from the failover loop, not the root cause.

Impact and severity

  • Affected: every OpenClaw user on 2026.4.26 routing through openai-codex with a ChatGPT subscription auth profile (the most common setup for codex models).
  • Severity: High — codex models are completely unusable on this path. Fallback chains that include openai-codex/* also break, dragging down primary-model recovery.
  • Frequency: 100% reproducible on every codex request. Verified across gpt-5.4-mini, gpt-5.4, gpt-5.5 and across multiple OAuth profiles.
  • Consequence: agents fall through to other providers or surface the misleading "400 (no body)" error. The misleading message has caused follow-on debugging of OAuth, originator headers, proxy, and session state — none of which are the actual cause.

Additional information

Root cause. resolveOpenAITransportTurnState in extensions/openai/transport-policy.ts returns a metadata block for all OpenAI-family routes that pass usesKnownNativeOpenAIRoute, which includes openai-codex against chatgpt.com/backend-api:

return {
  headers: { ...sessionHeaders, "x-openclaw-turn-id": turnId, ... },
  metadata: {
    openclaw_session_id: sessionHeaders["x-openclaw-session-id"] ?? "",
    openclaw_turn_id: turnId,
    openclaw_turn_attempt: attempt,
    openclaw_transport: ctx.transport
  }
};

The codex /backend-api/codex/responses endpoint validates a stricter parameter allowlist than api.openai.com/v1/responses and does not accept metadata. buildOpenAIResponsesParams then injects that metadata into the request body for both endpoints (...metadata ? { metadata } : {}), and mergeTransportMetadata merges it again after onPayload. There is no payload-policy gate for metadata (only store, service_tier, prompt_cache_*), so users have no config-side workaround; #10140 had previously asked for exactly such a configurable strip layer.

Suggested fix (smallest surface). In resolveOpenAITransportTurnState, omit metadata when the provider is openai-codex. Tracking via x-openclaw-* headers is preserved; the codex backend ignores arbitrary x- headers. Drop-in patch verified locally:

const isCodexProvider =
  normalizeProviderId(ctx.provider) === OPENAI_CODEX_PROVIDER_ID;
return {
  headers: {
    ...sessionHeaders,
    "x-openclaw-turn-id": turnId,
    "x-openclaw-turn-attempt": attempt,
  },
  ...isCodexProvider ? {} : {
    metadata: {
      openclaw_session_id: sessionHeaders["x-openclaw-session-id"] ?? "",
      openclaw_turn_id: turnId,
      openclaw_turn_attempt: attempt,
      openclaw_transport: ctx.transport,
    },
  },
};

Verified: same openclaw agent --local --message "say hi only" --model openai-codex/gpt-5.4-mini returns the assistant reply after applying this patch in dist/transport-policy-*.js. Also confirmed the manual curl with the unpatched body and metadata: {…} reproduces the exact 400 with Unsupported parameter: metadata, while removing the field returns a normal SSE stream.

Possible secondary improvement. The pi-ai error parser falling through {"detail":...} bodies into "400 status code (no body)" hides the actual upstream message and meaningfully delayed root-cause analysis. A small fallback in parseErrorResponse that surfaces a top-level detail string when no error.message is present would have made this issue self-diagnosing.

Last known good / first known bad. Last known good: 2026.4.24 (transport-policy turn-state was added between 4.24 and 4.26 along with the OpenAI-SDK transport switch for codex). First known bad: 2026.4.26.

Related issues

  • #10140 — old enhancement asking for a configurable drop-unsupported-fields layer; would have prevented this if landed.
  • #61376 — same family of bug for google-generative-ai OpenAI-compat endpoint (rejected metadata / store / service_tier); fix was scoped to google-generative-ai only and did not extend to codex.
  • #73344, #73820 — separate 4.26 codex regressions on empty input[], not the same root cause but compounding the diagnostic difficulty.

extent analysis

TL;DR

The issue can be fixed by omitting the metadata field in the request body when the provider is openai-codex in the resolveOpenAITransportTurnState function.

Guidance

  • Identify the resolveOpenAITransportTurnState function in extensions/openai/transport-policy.ts and modify it to conditionally omit the metadata field based on the provider.
  • Verify that the openai-codex provider is correctly identified and that the metadata field is not included in the request body.
  • Test the modified function with the openclaw agent command to ensure that the issue is resolved.
  • Consider adding a configurable drop-unsupported-fields layer to prevent similar issues in the future.
  • Review related issues (#10140, #61376, #73344, #73820) to ensure that similar problems are addressed.

Example

const isCodexProvider =
  normalizeProviderId(ctx.provider) === OPENAI_CODEX_PROVIDER_ID;
return {
  headers: {
    ...sessionHeaders,
    "x-openclaw-turn-id": turnId,
    "x-openclaw-turn-attempt": attempt,
  },
  ...isCodexProvider ? {} : {
    metadata: {
      openclaw_session_id: sessionHeaders["x-openclaw-session-id"] ?? "",
      openclaw_turn_id: turnId,
      openclaw_turn_attempt: attempt,
      openclaw_transport: ctx.transport,
    },
  },
};

Notes

The provided fix is a drop-in patch that has been verified locally, but it may require additional testing and validation to ensure that it does not introduce any new issues.

Recommendation

Apply the suggested fix by modifying the resolveOpenAITransportTurnState function to omit the metadata field when the provider is openai-codex, as this is the most direct and

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…

FAQ

Expected behavior

The request succeeds. Sending the same payload manually (without the metadata field) against the same endpoint with the same OAuth token returns the assistant message normally — confirmed via curl. Earlier OpenClaw releases (pre-resolveOpenAITransportTurnState) did not attach this body field for codex and worked.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: 2026.4.26 injects unsupported metadata field into openai-codex Responses requests; ChatGPT backend returns HTTP 400, surfaced as "400 status code (no body)" [1 pull requests, 1 comments, 1 participants]