Skip to content

Claude Code → Codex forward leaks user; optional-control 400 stops combo failover #4527

Closed

Description

@RHODIZSECURITY

Client or integration

Claude Code (Anthropic Messages ingress), with a failover combo.

Provider or upstream service

Canonical ChatGPT Codex forward destination (openai-responses, authMode: forward). Observed model: gpt-5.3-codex-spark. The user sanitation defect is destination-scoped, not a request to restore any retired model-specific exception.

OpenCodex version

Observed in installed @bitkyc08/opencodex 2.52.0 on Linux on 2026-09-13. The same missing sanitation and terminal classification were reproduced against current dev base df7dc1b (package development version 2.54.0) with synthetic regression tests.

Endpoint or capability

POST /v1/messages → Responses translation → canonical Codex forward wire; pre-output combo failover after an HTTP 400 optional-control incompatibility.

Current behaviour

  1. src/claude/inbound.ts maps metadata.user_id to top-level Responses user and separately derives prompt_cache_key.
  2. src/adapters/openai-responses.ts removes other unsupported native-forward fields, but leaves user on the canonical ChatGPT wire.
  3. Actual ordered attempt telemetry showed Anthropic returning 429, followed by the native Codex target returning 400 / invalid_request_error / Unsupported parameter: user. The combo ended at that target despite subsequent configured targets.
  4. Separate requests to that native target also returned unsupported_value for reasoning.effort: none, with supported values listed as low/medium/high/xhigh. The combo's generic invalid_request_error stop similarly prevented fallback. A configured default of low does not replace an explicit inbound none.

No credentials, account identifiers, conversation bodies, or private project files are included here.

Expected behaviour

  • Remove top-level user only at the canonical Codex destination. Keep the original translated/replay body, session/cache identity, input roles, tool-schema properties and safety identifiers unchanged. Public/key-auth and noncanonical forward destinations must not inherit this deletion.
  • A bounded, intact error envelope specifically rejecting optional user, or reporting unsupported_value for the reasoning-effort field, may advance to the next eligible unattempted combo target before output commitment.
  • These are request-local incompatibilities, not evidence of an unhealthy account/provider: do not record a target/provider cooldown for them.
  • Do not turn every HTTP 400 into a retry, silently drop reasoning controls, change none to a more expensive level, retry the same attempted target, or replay after output/tool commitment. Cancellation, origin/cyber-policy refusals and ambiguous post-send errors remain terminal.

Minimal redacted request or reproduction

Synthetic Claude-shaped input, with a configured failover combo and a canonical Codex target:

{
  "model": "combo/repro",
  "max_tokens": 32,
  "metadata": { "user_id": "synthetic-claude-session" },
  "messages": [{ "role": "user", "content": "Reply OK." }]
}

For deterministic reproduction, use three synthetic upstream targets: A returns HTTP 429, B (openai-responses) returns either exact 400 envelope below, and C returns a valid completion. Exercise both streaming and non-streaming requests. Before the change, all four scenarios terminate with 400; with the proposed change, each reaches C exactly once and returns 200.

Actual response or error

{"error":{"type":"invalid_request_error","message":"Unsupported parameter: user"}}

The separately observed reasoning rejection:

{"error":{"type":"invalid_request_error","code":"unsupported_value","message":"Unsupported value: 'none' is not supported with the 'gpt-5.3-codex-spark' model. Supported values are: 'low', 'medium', 'high', and 'xhigh'.","param":"reasoning.effort"}}

Upstream documentation

The expected client requirement is concrete: Claude Code supplies metadata.user_id, and this repository's translator forwards it as user. I am not treating the public Responses API schema as a complete specification of the private Codex backend's accepted fields. The rejection above was observed in real attempt telemetry; the proposed destination guard reuses the repository's existing isCanonicalOpenAiForwardProvider authority. Related destination-envelope precedent: #2765 and #2668.

Suggested mapping or implementation notes

An isolated patch and regression tests are prepared for a dev-targeted pull request. The optional-control classifier accepts only the two narrow structured shapes, with a 16,384-character / three-envelope bound and exact OpenCodex wrapper recognition. Conflicting/unknown codes, reflected JSON, malformed/truncated envelopes and unrelated input/tool/safety fields do not acquire hop permission.

Verification so far: 214 focused tests passed, 0 failed, including the complete combo-server E2E file, the canonical-envelope suite, classification/security negatives, and existing reasoning-downgrade coverage. The final regressions were driven red against unchanged base source first: 4 canonical-envelope failures, 4 classification failures, and 4 synthetic E2E failures. Typecheck, privacy scan and structure checks passed. Full-suite/release-readiness verification is still in progress and will be reported with the PR's exact head.

Additional context and attachments

Please consider the reviewed fix for the next appropriate update. This report does not claim a deployed fix, an upstream merge, live-provider E2E of the patch, or a guaranteed release date.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Activity

  1. added
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 13, 2026
  2. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 64 / 80

    설명
    이 이슈는 Claude Code가 POST /v1/messages로 넣은 metadata.user_id가 Responses 번역 과정에서 최상위 user로 올라간 뒤, 그 값이 캐노니컬 ChatGPT Codex forward(openai-responses, authMode: forward) 와이어까지 그대로 남는 문제다. 실제 텔레메트리에서는 Anthropic 쪽 429 다음에 네이티브 Codex 타깃이 400 / invalid_request_error / Unsupported parameter: user를 돌려주었고, 콤보가 그 타깃에서 멈춰 뒤 타깃으로 넘어가지 못했다. 같은 계열로 reasoning.effort: none도 unsupported_value로 거절되는데, 설정된 기본값 low가 명시적 인바운드 none을 덮어쓰지 않아 같은 식으로 콤보가 끊긴다.

    지금 dev(HEAD df7dc1be5, package 2.54.0) 코드도 그 경로를 그대로 보여 준다. src/claude/inbound.ts 413–420행은 metadata.user_id가 있으면 body.user에 넣고, 동시에 SHA-256 앞 32자로 prompt_cache_key를 만든다. 캐시 키는 세션 안정성을 위한 것이고, 최상위 user는 그 부산물이다. forward 쪽 src/adapters/openai-responses.ts의 stripUnsupportedForwardParams(1279–1285행)는 max_output_tokens와 metadata만 지운다. 주석도 캐노니컬 ChatGPT 백엔드가 그 둘을 allowlist에서 거른다고 적혀 있고, user는 목록에 없다. isCanonicalOpenAiForwardProvider 가드는 prompt-cache retention·continuation envelope 등에는 쓰이지만 user 삭제는 없다. 그래서 공개/키 인증·비캐노니컬 forward에는 user를 남겨 두고, 캐노니컬 Codex 목적지에서만 빼야 한다는 본문 제안이 현재 아키텍처와 맞다.

    콤보 쪽은 더 직접적이다. src/combos/failover.ts의 comboFailureDecision(468–469행)는 invalid_request_error를 stop으로 본다. 업스트림이 Unsupported parameter: user나 unsupported_value(param reasoning.effort)를 그 타입으로 주면, 요청 모양 문제인데도 콤보가 다음 미시도 타깃으로 넘어가지 않는다. 본문이 말하는 “좁은 구조화 엔벨로프만 hop, 쿨다운 없음, 출력/툴 커밋 이후 재시도 금지, 모든 400을 hop으로 만들지 말 것”은 기존 fail-closed 설계와 같은 방향이다, 지금 코드에는 그 예외가 없다. 관련 목적-엔벨로프 선례로 본문이 가리킨 #2765·#2668 축과도 이어진다.

    보고 버전은 설치본 2.52.0이고, 같은 결함을 현재 dev df7dc1be5에서도 합성 회귀로 재현했다고 한다. 기여자 쪽에 격리 패치와 회귀(포커스 214 통과, 베이스 대비 red→green)가 준비되어 있고 full-suite는 PR 헤드와 함께 올린다고 한다. types/config 분할과 무관하고, 지금 dev 메인라인(웹서치 브리지·데스크톱 재시작·reasoning ladder 문서 마감)과도 충돌하지 않는 provider-compatibility 버그다. 막힌 배포 블로커는 아니지만, Claude Code → Codex forward + 콤보를 쓰는 경로에서는 실제 장애다.

    라인 413 - src/claude/inbound.ts - metadata.user_id를 body.user로 올린다. prompt_cache_key와 같이 생기지만, 캐노니컬 forward는 user를 거절한다.
    라인 1279 - src/adapters/openai-responses.ts stripUnsupportedForwardParams - max_output_tokens/metadata만 제거한다. user는 그대로 남는다.
    라인 2256 - 같은 파일 isCanonicalOpenAiForwardProvider 블록 - retention·envelope 정규화는 캐노니컬만 적용하지만 user 삭제는 없다. 목적지 스코프 가드를 여기 붙이는 편이 자연스럽다.
    라인 468 - src/combos/failover.ts comboFailureDecision - invalid_request_error는 무조건 stop이다. 선택적 user/reasoning.effort 거절도 여기서 콤보가 끊긴다.
    경로/심볼 - 본문 제안 classifier - 엔벨로프·길이·래퍼 인식 경계를 좁게 잡았다고 한다. 리뷰 때 “모든 400 hop”이 되지 않는지, 쿨다운 스코프가 none인지 확인이 필요하다.
    경로/심볼 - #2765 / #2668 - 목적지 엔벨로프 선례. 중복 이슈로 닫지 말고, 캐노니컬 forward sanitation 후속으로 링크한다.

    메인테이너의 판단이 필요한 지점

    • user 삭제를 canonical forward에만 둘지, 테스트용 noncanonical mock에도 대칭 픽스처를 둘지
    • reasoning.effort: none을 hop만 할지, 인바운드에서 모델 허용 집합으로 낮출지(본문은 값 바꾸지 말라고 함)
    • 기여자 준비 PR을 바로 dev에 받을지, 엔벨로프 classifier 보안 네거티브를 메인테이너가 한 번 더 좁힐지

    너의 추천
    받는다. provider-compatibility / account-pool 라벨 유지. 수정은 (1) 캐노니컬 Codex forward에서만 최상위 user 제거(세션/prompt_cache_key/툴 스키마는 유지), (2) 본문이 적은 두 구조화 400만 콤보 hop·쿨다운 없음으로 분류. none→low 자동 승격은 하지 말 것. 준비된 PR이 오면 exact-head와 full-suite 결과만 확인한 뒤 dev에 랜딩하면 된다.

    이 댓글은 grok-bot이 작성했습니다

  3. RHODIZSECURITY commented on Sep 13, 2026

    @RHODIZSECURITY
    ContributorAuthor

    @lidge-jun — the implementation and regression tests are now in PR #4528, targeting dev.

    Exact tested/pushed head: 2668cb6e38cb25cae8b68598d96d444e3ac332b9.

    Final complete Linux run: 24,823 passed, 45 skipped, 0 failed, exit 0, 2026-09-13 15:25:09–15:29:20 UTC. The exact HEAD and clean worktree stayed unchanged throughout. Focused coverage: 214 passed, 0 failed, with the observed failures first reproduced against unchanged base source. Typecheck, privacy scan, structure checks and documentation build also passed.

    The PR documents the initial test-environment failures and their unchanged-base reproduction; the final green run changes only the test process umask/runtime inode, not any production permission or security assertion. It preserves canonical session/cache identity and safety fields, and limits failover recovery to the two precise optional-control incompatibilities reported here.

    Codex review was requested. The PR remains draft pending independent review and repository readiness checks; this is not a claim of deployment, live-provider E2E, or hosted multi-platform CI. Please consider the reviewed fix for the next appropriate update.

  4. RHODIZSECURITY commented on Sep 13, 2026

    @RHODIZSECURITY
    ContributorAuthor

    Follow-up production evidence from the same 2.52.0 deployment (2026-09-13): a second failure class hit the same combo stop boundary.

    Observed redacted logical row:

    • requested model: combo/rhodiz-coding-primary
    • selected target: openai/gpt-5.3-codex-spark
    • upstream result: HTTP 400 / invalid_request_error
    • param: input
    • message: Model 'gpt-5.3-codex-spark' does not support image inputs. Try again with a vision model.
    • later configured combo targets existed, but the generic invalid-request stop returned this 400 to Claude Code instead of advancing.

    Current dev already classifies gpt-5.3-codex-spark as a retired native model, so production was also carrying a stale explicit combo target. I removed that retired target from the live 2.52.0 combo through the supported management API (no installed-package patch, no service restart); the config remains owner-only and a pre-change backup was retained.

    The PR branch now has a second focused commit, 8a96f86998d16350666fdb604bb4dcc2b7a1826c, adding bounded pre-output failover for the observed model-scoped image rejection. The classifier requires an intact structured 400 envelope, type: invalid_request_error, param: input, only generic/null error code, and the bounded exact Model '<id>' does not support image inputs. prefix. Conflicting codes/types, wrong params, lookalikes, malformed/reflected envelopes, cancellation, policy refusals, and committed output remain terminal. The rejection records no cooldown because it is request/target compatibility evidence rather than target health.

    Verification at this head so far:

    • focused adapter/classifier/full combo-server E2E/reasoning suite: 222 pass / 0 fail / 1,152 expectations;
    • exact reproduction 429 -> image 400 -> healthy backup: PASS;
    • typecheck / structure SSOT / privacy scan / docs build: PASS;
    • the four permission/sandbox cases from one host-default run were traced to inherited umask 0077; with normal umask 022, the three affected files pass 241/241. A repository-wide exact-head rerun under normalized umask is in progress.

    PR: #4528. No credentials, prompt contents, account identifiers, or private project data are included.

  5. lidge-jun commented on Sep 14, 2026

    @lidge-jun
    Owner

    Fixed on dev at ae3cb2311, but in two pieces rather than one, and the distinction matters for anyone reading this later.

    The user leak itself was already gone before the carry landed. #4535 added stripCanonicalForwardSamplingParams, which removes temperature, top_p, stop and user at the canonical ChatGPT backend only — self-hosted and third-party forward gateways still receive them. A translated Claude turn therefore no longer sends metadata.user_id onward as a top-level user, so the 400 this issue reported cannot be produced that way.

    The second half — the part this issue actually complained about downstream — landed as #4563: a combo that has already taken a 429 on an earlier target no longer terminates when the next target returns a request-local 400. Only a clear pre-output, target-local incompatibility falls through to the next healthy target. Cancellations, policy refusals, and anything after output has started still terminate, which is the boundary that keeps this from becoming a blanket retry of every 400.

    Landed as #4563, merge commit ae3cb2311bd6dc15b5e449eda7d69469bd577567, verified by Cross-platform CI run 34797124354 at the exact merged head 2530f4cd0fe3278996796290036a582ada7d5af7 with 21 jobs and zero failures.

    #4563 carries the original work by @RHODIZSECURITY from #4528, with a Co-authored-by trailer preserved in the squash commit.

  6. RHODIZSECURITY commented on Sep 14, 2026

    @RHODIZSECURITY
    ContributorAuthor

    Follow-up from a 2.54.0 HomeLab revalidation (2026-09-14): the previously observed canonical-forward 400s for Unsupported parameter: user and unsupported reasoning.effort: none were reproduced in historical attempt telemetry, then the bounded local branch fix was re-tested against current config. After the failover/sanitation changes, equivalent combo requests can advance to later eligible targets and complete 200 instead of surfacing the provider-specific 400 as the final Claude error. No upstream merge/release is claimed here.

    A separate image-capability defect observed on the same Spark target is now tracked independently as #4633 so this issue can remain scoped to optional-control sanitation/classification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions