Repository navigation
[Provider compatibility] Codex 0.155 access_programs is forwarded to strict third-party Responses upstreams and 400s every OpenCode Go muse-spark turn #4853
Description
Client or integration
Codex App. Codex runtime codex-cli 0.155.0-alpha.2.6 (shipped with ChatGPT.app, installed 2026-09-17 10:03, previously 0.154.0-alpha.6.2). Also reproduced with minimal direct HTTP requests against the proxy and against the upstream.
Provider or upstream service
OpenCode Go (OpenCode-Go, https://opencode.ai/zen/go/v1) — the gateway that identifies itself as "Console Go" in the error message. Affects muse-spark-1.3-contributor and muse-spark-1.2-contributor; the same request shape succeeds on other models behind the same base URL.
OpenCodex version
2.57.0 (installed CLI, latest on npm at the time of writing). macOS, Apple Silicon. Observed 2026-09-17.
Endpoint or capability
Inbound POST /v1/responses on the proxy → outbound Responses passthrough to /zen/go/v1/responses (adapter: openai-responses, OpenCode-Go provider). Request body shape / unknown-parameter handling.
Current behaviour
Every routed turn to muse-spark-1.3-contributor fails with HTTP 400:
Error from provider (Console Go): Upstream request failed: [invalid_request_error] unknown parameter `access_programs`
Root cause: Codex 0.155 attaches a new OpenAI-private top-level field, access_programs, to its Responses request body. The client gates it on ChatGPT auth, not on the destination's base URL (codex-rs/core/src/cyber_access_program.rs):
pub(crate) fn for_auth(auth: Option<&CodexAuth>, program: Option<CyberAccessProgram>) -> Option<AccessPrograms> {
program.filter(|_| auth.is_some_and(CodexAuth::is_chatgpt_auth)).map(AccessPrograms::from)
}Because loopback injection keeps Codex's built-in openai provider pointed at the proxy (openai_base_url + realtime sideband override), the client still believes it is talking to the canonical ChatGPT backend and keeps the field. There is no client-side switch to suppress it, and it is not conditional on the model being requested.
The proxy forwards it verbatim, so whether a turn fails depends only on how strict the destination is:
| Destination model (same provider, same wire) | Unknown top-level field tolerated? |
|---|---|
muse-spark-1.3-contributor |
No → 400 |
muse-spark-1.2-contributor |
No → 400 |
deepseek-v4.1-flash |
Yes → 200 |
grok-4.6 |
Yes → 200 |
glm-5.3 |
Yes → 200 |
Expected behaviour
Top-level fields that only OpenAI-operated destinations define should not be forwarded to third-party Responses upstreams. The proxy already does this for the per-item private field internal_chat_message_metadata_passthrough (stripInternalChatMessageMetadataPassthrough) at exactly the same boundary; the top-level private list needs the equivalent treatment.
Minimal redacted request or reproduction
# Fails: 400 unknown parameter `access_programs`
curl -sS http://127.0.0.1:10100/v1/responses \
-H 'Content-Type: application/json' \
-d '{"model":"OpenCode-Go/muse-spark-1.3-contributor","input":"hi","stream":false,"access_programs":{"cyber":"standard"}}'
# Succeeds: 200 (identical body, field omitted)
curl -sS http://127.0.0.1:10100/v1/responses \
-H 'Content-Type: application/json' \
-d '{"model":"OpenCode-Go/muse-spark-1.3-contributor","input":"hi","stream":false}'The same body sent straight to the upstream, bypassing OpenCodex, isolates the field as the cause:
curl -sS https://opencode.ai/zen/go/v1/responses \
-H "Authorization: Bearer $OPENCODE_GO_API_KEY" \
-H 'x-opencode-session: <any-session-id>' \
-H 'Content-Type: application/json' \
-d '{"model":"muse-spark-1.3-contributor","input":"hi","access_programs":{"cyber":"standard"}}'Actual response or error
{"model":"muse-spark-1.3-contributor","error":{"param":"access_programs","type":"invalid_request_error","message":"Error from provider (Console Go): Upstream request failed: [invalid_request_error] unknown parameter `access_programs`"}}Probe results with the same minimal body against /zen/go/v1/responses for muse-spark-1.3-contributor:
| Extra top-level field | Result |
|---|---|
access_programs: {"cyber":"standard"} |
400, names access_programs |
codex_output_schema: {"type":"object"} |
400, names codex_output_schema |
totally_made_up_param: 1 |
400, names totally_made_up_param |
client_metadata, prompt_cache_key, service_tier, safety_identifier |
200 |
| (none) | 200 |
So this upstream validates the top-level response schema strictly, and the only reason the failures started today is the new client field.
Upstream documentation
- https://opencode.ai/docs/zen/
- Codex client source that sets the field: https://github.com/openai/codex/blob/main/codex-rs/core/src/cyber_access_program.rs
- Field in the client's request struct and its equality/omission handling: https://github.com/openai/codex/blob/main/codex-rs/core/src/client.rs
No public specification defines access_programs; it appears to be OpenAI-internal (cyber access program), so a third-party gateway treating it as unknown is correct.
Suggested mapping or implementation notes
Validated locally against 2.57.0, in the same place the per-item private field is already stripped:
src/adapters/openai-responses/request-strips.ts— addstripOpenAiPrivateClientParams(body)that deletesaccess_programsandcodex_output_schema, returns the input object unchanged when neither key is present (so the common path stays allocation-free), and passes non-objects through.src/adapters/openai-responses/passthrough.ts— inside the existingif (!isCanonicalOpenAiForwardProvider(provider))block, next tostripInternalChatMessageMetadataPassthrough(outBody), call it when!isOpenAiOperatedResponsesDestination(provider)so official OpenAI / ChatGPT traffic keeps the field and only third-party destinations lose it.
Verification after patching: muse-spark turns return 200 with the field present in the incoming body; a genuinely invalid parameter still returns 400 (so the change does not mask real client errors); deepseek-v4.1-flash, grok-4.6 and glm-5.3 are unaffected; the incoming raw body object is not mutated.
Note: forcing muse-spark onto the openai-chat wire is not an alternative workaround — POST /zen/go/v1/chat/completions for muse-spark-1.3-contributor returns HTTP 500 {"type":"error","error":{"type":"error","message":"Internal server error"}} (observed the same day, both via the proxy and directly), so the Responses wire is the only working one for these models.
Additional context and attachments
- Checked for duplicates: no existing issue or PR mentions
access_programs,codex_output_schema, or Codex 0.155. - Requests and responses above are redacted: no credentials, tokens, account labels, or conversation content are included.
- The field is version-dependent (
access_programsis new in Codex 0.155; requests from Codex 0.154 and earlier did not contain it) and the same Codex build works fine on providers whose validator is lenient, which is why this only broke today and only for the strict Responses backends.
Activity
lidge-jun commentedon Sep 17, 2026
리뷰 · 우선순위 77 / 80
이 이슈는 Codex CLI 0.155가 Responses 요청 최상위에 붙이기 시작한 OpenAI 비공개 필드
access_programs가, 엄격한 서드파티 Responses 업스트림으로 그대로 전달되면서 매 턴 400이 난다는 프로바이더 호환 보고입니다. 증상은 OpenCode Go(OpenCode-Go,https://opencode.ai/zen/go/v1)의muse-spark-1.3-contributor/1.2-contributor에서unknown parameter access_programs로 나타납니다. 같은 베이스 URL의deepseek-v4.1-flash·grok-4.6·glm-5.3은 모르는 필드를 관대해서 200이 나옵니다. 즉 프록시 “라우팅”이 깨진 게 아니라, 엄격 스키마 게이트웨이만 새 클라이언트 필드에 걸린 것입니다.원인은 클라이언트와 프록시 경계가 어긋난 전형적인 패턴입니다. Codex는
cyber_access_program.rs에서 ChatGPT 인증이면access_programs를 넣고, 목적지 base URL은 보지 않습니다. 루프백 주입은 Codex의openai프로바이더 정체성을 유지한 채 프록시로 보내므로, 클라이언트는 계속 “정식 ChatGPT 백엔드”라고 믿고 필드를 붙입니다. 지금dev의src/adapters/openai-responses/passthrough.ts는 이미 비슷한 일을 합니다.!isCanonicalOpenAiForwardProvider(provider)일 때stripInternalChatMessageMetadataPassthrough로 아이템 단위 비공개 필드internal_chat_message_metadata_passthrough를 지웁니다(request-strips.ts). 다만 최상위 비공개 키(access_programs, 리포터가 같이 확인한codex_output_schema)를 지우는 헬퍼는 아직 없습니다.rg로dev전체를 봐도 두 심볼은 코드에 없습니다.리포터의 최소 재현도 깔끔합니다. 같은 muse-spark 바디에 필드만 있으면 400, 빼면 200. 업스트림 직행도 동일.
totally_made_up_param도 400인 걸 보면 Console Go는 최상위 스키마를 엄격히 검사합니다. 제안 패치 자리(stripOpenAiPrivateClientParams를request-strips.ts에 두고, 기존 non-canonical 블록에서isOpenAiOperatedResponsesDestination이 아닐 때 호출)는 현재 코드 구조와 잘 맞습니다. 공식 OpenAI/ChatGPT 트래픽은 필드를 유지하고, 서드파티만 벗겨야 합니다. Chat 와이어로 muse-spark를 돌리는 우회는 업스트림 500이라 대안이 아닙니다. types/config 대형 분리와도 무관하고, 스트립 한 함수 + passthrough 한 줄 + 회귀 테스트면 끝납니다. Codex 0.155가 퍼지면 같은 400이 다른 엄격 게이트웨이에도 퍼질 수 있어서, 지금dev(2.58.0 트레인)에 넣는 가치가 큽니다.경로
src/adapters/openai-responses/request-strips.ts- 아이템 메타 스트립만 있고 최상위access_programs/codex_output_schema삭제가 없다.경로
src/adapters/openai-responses/passthrough.ts라인 317-318 근처 - non-canonical 분기에서 아이템 스트립만 호출하고, 최상위 비공개 키는 그대로 나간다.심볼
access_programs/codex_output_schema-dev트리에 언급이 없어 Codex 0.155 이후 필드가 아직 가드 목록에 없다.경로 테스트 - muse-spark(엄격) vs 관대 모델, canonical OpenAI 유지 여부를 같이 고정하지 않으면 공식 경로까지 필드를 지울 위험이 있다.
경로 출시 - 증상은 npm latest 2.57.0 + Codex 0.155 조합에서 오늘 터졌다. 2.57.x 핫픽스 vs 2.58.0만 넣을지 정해야 한다.
메인테이너의 판단이 필요한 지점
access_programs+codex_output_schema로 최소 시작할지, “OpenAI-private top-level” 확장 목록으로 갈지isOpenAiOperatedResponsesDestination기준을 쓸지, 기존처럼!isCanonicalOpenAiForwardProvider만으로 충분할지(리포터 제안은 전자를 명시)너의 추천
재현·원인·패치 자리가 모두 명확하고, 기존
stripInternalChatMessageMetadataPassthrough와 같은 계층의 구멍이다. 제안대로request-strips.ts에 할당 없는 no-op 경로를 둔 최상위 스트립을 추가하고, non-canonical(또는 non-OpenAI-operated) passthrough에서 호출한 뒤, 필드 있음→서드파티 200 / 공식 경로 유지 / 진짜 잘못된 파라미터는 여전히 400 회귀를 넣고dev에 랜딩하는 쪽을 강하게 추천한다. 우선순위는 오늘 기준 프로바이더 호환 이슈 중 최상위권이다.이 댓글은 grok-bot이 작성했습니다
fix(responses): stop forwarding Codex's private access_programs to th…
lidge-jun commentedon Sep 17, 2026
Fixed on
devin 4b234da (#4884). Closing manually, since pull requests here targetdevrather than the default branch.Two notes on what shipped, because both differ from the suggested patch.
codex_output_schemais not stripped. In codex-rs that string is thenameof the JSON-schematext.formatobject (codex-rs/codex-api/src/common.rs), not a top-level request key, and no serde field carries it — your probe table used it the same way astotally_made_up_param, as an arbitrary unknown key. Listing it would delete a field this client never sends, and discard it for any client that does send it meaningfully.access_programsis the only row inCANONICAL_ONLY_TOP_LEVEL_FIELDS, and a regression pins that an unlisted top-level key still reaches the wire, which is what keeps the change from becoming a general unknown-parameter sanitizer.The boundary is
!isOpenAiOperatedResponsesDestination, as you suggested, and the deciding reason turned out to be one you could not have seen from outside:src/server/responses/compact.tsspreads the caller's raw body into the native/responses/compactrequest without passing through the Responses adapter, andsupportsNativeResponsesCompactEndpointoffers that endpoint only to OpenAI-operated destinations. Scoping on the canonical surface instead would have madeopenai-apikeybehave differently on its two endpoints for the same field.Your report made this quick to land: the probe table isolating the field, the before/after pair against the upstream directly, and the pointer to
stripInternalChatMessageMetadataPassthroughas the existing equivalent one level down were all correct and all load-bearing. The note that the Chat wire is not a workaround saved a dead end too.