Repository navigation
Claude Code → Codex forward leaks user; optional-control 400 stops combo failover #4527
Description
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
src/claude/inbound.tsmapsmetadata.user_idto top-level Responsesuserand separately derivesprompt_cache_key.src/adapters/openai-responses.tsremoves other unsupported native-forward fields, but leavesuseron the canonical ChatGPT wire.- 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. - Separate requests to that native target also returned
unsupported_valueforreasoning.effort: none, with supported values listed as low/medium/high/xhigh. The combo's genericinvalid_request_errorstop similarly prevented fallback. A configured default oflowdoes not replace an explicit inboundnone.
No credentials, account identifiers, conversation bodies, or private project files are included here.
Expected behaviour
- Remove top-level
useronly 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 reportingunsupported_valuefor 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
noneto 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
lidge-jun commentedon Sep 13, 2026
리뷰 · 우선순위 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(HEADdf7dc1be5, package 2.54.0) 코드도 그 경로를 그대로 보여 준다.src/claude/inbound.ts413–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(paramreasoning.effort)를 그 타입으로 주면, 요청 모양 문제인데도 콤보가 다음 미시도 타깃으로 넘어가지 않는다. 본문이 말하는 “좁은 구조화 엔벨로프만 hop, 쿨다운 없음, 출력/툴 커밋 이후 재시도 금지, 모든 400을 hop으로 만들지 말 것”은 기존 fail-closed 설계와 같은 방향이다, 지금 코드에는 그 예외가 없다. 관련 목적-엔벨로프 선례로 본문이 가리킨 #2765·#2668 축과도 이어진다.보고 버전은 설치본 2.52.0이고, 같은 결함을 현재
devdf7dc1be5에서도 합성 회귀로 재현했다고 한다. 기여자 쪽에 격리 패치와 회귀(포커스 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.tsstripUnsupportedForwardParams-max_output_tokens/metadata만 제거한다.user는 그대로 남는다.라인 2256 - 같은 파일
isCanonicalOpenAiForwardProvider블록 - retention·envelope 정규화는 캐노니컬만 적용하지만user삭제는 없다. 목적지 스코프 가드를 여기 붙이는 편이 자연스럽다.라인 468 -
src/combos/failover.tscomboFailureDecision-invalid_request_error는 무조건stop이다. 선택적user/reasoning.effort거절도 여기서 콤보가 끊긴다.경로/심볼 - 본문 제안 classifier - 엔벨로프·길이·래퍼 인식 경계를 좁게 잡았다고 한다. 리뷰 때 “모든 400 hop”이 되지 않는지, 쿨다운 스코프가
none인지 확인이 필요하다.경로/심볼 - #2765 / #2668 - 목적지 엔벨로프 선례. 중복 이슈로 닫지 말고, 캐노니컬 forward sanitation 후속으로 링크한다.
메인테이너의 판단이 필요한 지점
user삭제를 canonical forward에만 둘지, 테스트용 noncanonical mock에도 대칭 픽스처를 둘지reasoning.effort: none을 hop만 할지, 인바운드에서 모델 허용 집합으로 낮출지(본문은 값 바꾸지 말라고 함)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이 작성했습니다
RHODIZSECURITY commentedon Sep 13, 2026
@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.
RHODIZSECURITY commentedon Sep 13, 2026
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:
combo/rhodiz-coding-primaryopenai/gpt-5.3-codex-sparkinvalid_request_errorparam: inputModel 'gpt-5.3-codex-spark' does not support image inputs. Try again with a vision model.Current
devalready classifiesgpt-5.3-codex-sparkas 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 exactModel '<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:
429 -> image 400 -> healthy backup: PASS;umask 0077; with normalumask 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.
lidge-jun commentedon Sep 14, 2026
Fixed on
devatae3cb2311, but in two pieces rather than one, and the distinction matters for anyone reading this later.The
userleak itself was already gone before the carry landed. #4535 addedstripCanonicalForwardSamplingParams, which removestemperature,top_p,stopanduserat the canonical ChatGPT backend only — self-hosted and third-party forward gateways still receive them. A translated Claude turn therefore no longer sendsmetadata.user_idonward as a top-leveluser, 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 head2530f4cd0fe3278996796290036a582ada7d5af7with 21 jobs and zero failures.#4563 carries the original work by @RHODIZSECURITY from #4528, with a
Co-authored-bytrailer preserved in the squash commit.RHODIZSECURITY commentedon Sep 14, 2026
Follow-up from a 2.54.0 HomeLab revalidation (2026-09-14): the previously observed canonical-forward 400s for
Unsupported parameter: userand unsupportedreasoning.effort: nonewere 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.