feat(responses): add an opt-in bounded JSON fallback for custom providers - #1367
feat(responses): add an opt-in bounded JSON fallback for custom providers#1367novelKR wants to merge 1 commit into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
⏳ DRAFT
What to do
Review readiness checklist
0/4 boxes ticked. This PR stays in draft until every box above is ticked. |
Summary
This PR adds an opt-in per-model fallback for custom
openai-responsesproviders that return a correct non-streaming Responses object but do not reliably deliver a streamed response to Native Codex through OpenCodex.The new setting exposes the existing registry-only
modelResponsesUpstreamStreamingpolicy to validated custom-provider configuration. Setting a model tofalsekeeps the Responses API on both sides of the proxy, but asks the upstream forstream:false. OpenCodex then reads the completed JSON through its existing bounds and reframes it into the canonical Responses event sequence expected by a streaming Codex client.The bounded fallback policy is opt-in and prioritizes correctness. It does not route traffic through Chat Completions, hard-code OpenCode Go or GPT-5.6 Luna, or claim to solve the underlying Bun/
tee()transport defect. The submitted draft also tightens validation in the shared WebSocket JSON bridge beyond the opt-in path; the review addendum below records that broader behavior as an unresolved scope item rather than presenting it as an intentional default change.What we observed
The same client-delivery split was reproduced in the two POSIX-family environments we tested:
response.completed, but Native Codex does not complete the turnWe reproduced the same failure on macOS arm64 and Linux x86_64. That does not mean every POSIX implementation is affected. Linux arm64, native Windows, WSL, and other POSIX systems were not part of the live reproductions and remain unverified.
Discovery context and relay-independent reproduction
The symptom was first noticed in a personal multi-layer deployment that included an independently implemented OCI relay. That environment prompted the structural investigation, but the relay was not treated as the cause or retained as a reproduction prerequisite.
We then reproduced the same behavior in a separate minimal environment whose relevant AI request stack had only Native Codex and OpenCodex installed and configured—no personal OCI relay, sidecar, or additional gateway/proxy was present. With Native Codex connected directly to OpenCodex, OpenCodex reached its internal terminal state while Native Codex did not commit the turn and entered the idle-timeout path. In the opposite control, Native Codex connected directly to the same OpenCode Go Responses upstream—without OpenCodex or any intermediate relay—and completed four consecutive turns. The personal relay is therefore neither required for reproduction nor the failing component identified by this comparison.
This A/B evidence places the observed failure boundary inside OpenCodex's Responses relay/client-delivery path, after upstream completion and before the Codex client commits the terminal event. It does not by itself prove that one specific Bun primitive is the sole low-level cause.
Current built-in routing distinction
The current OpenCode Go endpoint matrix explicitly assigns GPT 5.6 Luna (
gpt-5.6-luna) tohttps://opencode.ai/zen/go/v1/responsesusing@ai-sdk/openai. The same matrix assigns models such as DeepSeek V4 Flash to/v1/chat/completionsusing@ai-sdk/openai-compatible. OpenCode Go therefore exposes a model-specific protocol distinction rather than one provider-wide Chat Completions contract.At this PR's exact
devbase, however, OpenCodex's built-inopencode-goregistry entry declares the provider-wide adapter asopenai-chatand has nomodelWireDefaultsentry forgpt-5.6-luna. Without an explicitmodelAdaptersoverride, the wire resolver therefore keeps the built-in route onopenai-chat, whose request builder posts to${baseUrl}/chat/completionsinstead of Luna's documented Responses endpoint.The motivating reproduction worked around that separate built-in mapping gap by registering a custom provider with
adapter: "openai-responses", the OpenCode Go base URL, andgpt-5.6-luna. Sanitized direct probes observed completed Responses in both non-streaming JSON (stream:false) and SSE (stream:true) modes; the public Go documentation identifies the endpoint but does not separately guarantee both delivery modes, so those results are reported as reproduction evidence rather than as a documented service guarantee.These are two distinct gaps. The built-in preset does not currently select Responses for Luna, while the failure reproduced here occurs after the custom provider has correctly selected the Responses endpoint: OpenCodex reaches
response.completed, but Native Codex does not receive a terminal event it can commit. This PR addresses only the latter with an opt-in bounded fallback; it does not change the built-inopencode-goregistry mapping.The controls narrow the failure boundary:
stream:falseandstream:truecalls to the affected Responses upstream complete;response.completedin OpenCodex inspection/state, while Native Codex receives no completion event it can commit and later retries.This narrows the fault to the relay path, but it does not prove a specific Bun bug. Source inspection points most strongly to the interaction among
ReadableStream.tee(), independently paced inspection and client consumers, the JavaScript relay, SSE chunk boundaries, and backpressure.flowchart LR C[Native Codex] -->|stream=true| O[OpenCodex] O --> U[Custom Responses upstream] U -->|valid SSE| T[ReadableStream.tee] T --> I[Inspection branch] I -->|response.completed| L[Internal outcome: completed] T --> R[Client relay branch] R -. no semantic completion .-> C C -->|idle timeout| X[retry or failed turn] U -. direct control: 4/4 turns complete .-> CWhy use bounded JSON instead of enabling Linux eager relay
OpenCodex currently keeps its bounded single-reader eager relay behind a conservative runtime and platform gate. The bundled Bun is still 1.3.14,
MIN_FIXED_BUN_VERSIONis stillnull, and OpenCodex has not yet verified a stable Bun release for the relevant async-stream cancellation/backpressure path.Simply allowing Linux to enter
eager-relaywould bypass that safety decision. It would also overlap the runtime-qualified, protocol-safe one-reader work already planned in issue #820.The narrower option proposed here reuses behavior OpenCodex already has:
The client and upstream both continue to use the Responses API. Only the upstream delivery mode changes.
Configuration and precedence
{ "providers": { "<custom-responses-provider>": { "adapter": "openai-responses", "baseUrl": "<redacted-https-origin>", "authMode": "key", "modelResponsesUpstreamStreaming": { "gpt-5.6-luna": false } } } }The example intentionally contains no credential.
Policy lookup is case-insensitive but exact; it does not inherit colon-family entries. It runs after provider namespace/combo resolution and after the effective per-model wire is known, but before a client-facing response-model rewrite. Virtual aliases are resolved at both their public and wire-model identities.
The proposed precedence is:
The field should be rejected when the effective wire is not
openai-responsesand on effective forward-auth providers, so this option cannot silently alter the canonical OpenAI transport contract.Response handling
For an opted-in model and a client request with
stream:true, OpenCodex should:streamvalue tofalse;response.created, oneresponse.output_item.doneper output item, the originalcompleted/failed/incompleteterminal, and one[DONE]; for WebSocket, emit the equivalent JSON lifecycle events without an SSE sentinel;The shared validator should require:
nullor an array;objecteither omitted under the documented compatibility policy or equal toresponse;statusequal tocompleted,failed, orincomplete;outputarray whose entries are objects with non-empty stringtypefields;usageeither absent,null, or an object with non-negative integer input/output token counts; optional total/detail token fields are checked when present.The fallback itself should not invent output items or usage. Existing explicitly configured client-facing normalizations—image-call restoration, response-model rewrite, snapshot repair, and item-id repair—should still run exactly once, in the same order as the streaming path. Function-call, repair-enabled, and parallel-call tests must verify that item ids, call ids, names, and argument strings remain usable on the next turn.
Malformed JSON, an unknown terminal status, an invalid usage object, an oversized or stalled body, or an unexpected 2xx content type must fail closed. If an upstream ignores
stream:falseand returns SSE, OpenCodex must not fall back to the suspect tee path and must not replay the model request. Non-2xx behavior and safe retry metadata should keep their existing semantics. Client cancellation must abort the in-flight upstream read.Implementation
src/types.tssrc/config.tssrc/server/auth-cors.tssrc/providers/registry.tsand the Responses route setupcompletedevent.src/server/responses/core.tsand the Responses WebSocket bridgeNo visual-interface change is required for this PR.
Compatibility, tradeoffs, and rollback
The bounded fallback policy is intended to be additive and inactive unless explicitly configured. At the submitted head, however, the shared WebSocket JSON validator also reaches successful snapshots from unconfigured routes. The review addendum records this as an unresolved scope mismatch; the claim that unconfigured behavior remains unchanged is contingent on resolving it before review readiness.
The benefit is a way to avoid the problematic streaming relay without adding a provider-specific branch or weakening the Responses contract and Bun runtime gate. The tradeoff is straightforward: the client receives no incremental text, reasoning, or tool deltas; its first event arrives only after the upstream completes; and the response is retained within the existing bounded JSON envelope. The upstream must genuinely support non-streaming Responses.
Setting the model entry to
truedisables this forced bounded-JSON fallback after the normal configuration reload or service restart. That restores the client-requested/default streaming policy but does not guarantee that the upstream will actually stream. Removing the entry restores the inherited registry/default policy, which may itself befalse. No data migration is required.This PR should not close #820. The long-term fix remains a runtime-qualified one-reader relay that preserves true streaming across supported platforms.
Related work
#820 — broader runtime-qualified, protocol-safe one-reader architecture; this proposal is intentionally narrower.
#1127 — similar macOS symptom: upstream/internal completion with zero client SSE events.
#1142 — merged explicit Darwin eager relay for client-rewrite traffic; it deliberately left Darwin
autoand Linux unchanged.#947 — closed, unmerged predecessor whose transport predicate was attributed in fix(streaming): relay Darwin rewrites eagerly (#1127) #1142.
#1133 — bounded translated SSE inspection while preserving downstream bytes.
#1241 — bounded client-facing SSE frame retention without removing the tee/client-pull boundary.
#1217 — complementary content-free transport observability.
#1176 — separate bounded-JSON timeout tradeoff that belongs in regression and operational risk coverage.
#1026 — the bounded JSON and canonical event-reframing foundation reused here.
#1155 — an open, model-specific proposal touching registry streaming policy for web-search handling; it does not expose a validated custom-provider policy.
No currently open issue or PR found in the repository search implements this custom-provider setting.
Scope
In scope:
openai-responsesproviders;Out of scope:
opencode-gowire mapping forgpt-5.6-luna;streamMode;tee()or upgrading bundled Bun;If implementation adds diagnostics, they must remain content-free: status, content-type category, byte counts, relative timing, selected mode, terminal type, cancellation, and bounded-read result only. Do not record credentials, provider origins, query strings, prompts, output text, raw SSE/JSON, account ids, or unredacted request/thread/response ids.
Review addendum — confirmed blockers and follow-up risks
A second static review of the submitted head identified two concrete code issues and one outstanding acceptance gate. This appendix records the current draft state; it does not claim that the findings are already fixed, and it is not a maintainer approval or a formal GitHub review decision.
Must be resolved before review readiness
modelResponsesUpstreamStreaming=falseselected the bounded fallback. Before this change, a sparse unconfigured JSON snapshot could be reframed with compatibility defaults; the submitted head now returns a 502 protocol error. The safest resolution is to carry an internal bounded-fallback discriminator into the WebSocket bridge and apply the new strict contract only there. If global fail-closed validation is intentionally retained, it must instead be documented and tested as a broader behavioral change, preferably in a separate PR.Non-blocking follow-up risks
Disposition
The architectural direction remains unchanged: keep the workaround provider-agnostic, keep Responses on both sides, and avoid replaying an ambiguous model request. This draft should remain unready until the two code blockers are fixed, their focused tests are added, and the live fallback canary and repository review gates are complete. The timeout, duplicate-validation, and WebSocket backpressure items may be tracked separately unless new acceptance evidence raises their severity.
Sanitized retained runtime evidence
The following excerpts use selected fields from actual retained canary/tool output, OpenCodex request history, and Native Codex rollout records. They are not reconstructed model output. Secrets, prompts, responses, tool arguments, opaque identifiers, exact timestamps, host details, local paths, and private endpoints were removed. Relative
t+values are derived only from retained timestamps; protocol outcomes and durations remain the recorded values. No new model call was made to prepare this appendix.Direct control — Native Codex to OpenCode Go Responses
Selected fields from the retained four-turn canary output:
This control called the same OpenCode Go Responses upstream directly from one persistent Native Codex thread, without an OpenCodex endpoint, wrapper, or existing AppServer. It establishes that the direct client/upstream path can complete consecutive turns. It did not exercise this PR's new fallback or a tool-call round trip.
Failure reproduction — OpenCodex server-side history
The following normalized rows are from one retained Linux/x86_64 OpenCodex conversation. The private correlation identifier was removed;
t+is relative to the first request.OpenCodex recorded upstream terminal completion in 2.155–4.690 seconds on every attempt, while the same conversation was requested again at intervals of 302.694, 301.943, 302.629, 302.933, and 304.393 seconds. That cadence is consistent with the Native Codex 300-second stream-idle boundary, but the raw client retry diagnostic line was not retained and is not claimed here.
Failure reproduction — Native Codex client rollout
A separate macOS/arm64 reproduction used a minimal request stack containing only Native Codex and OpenCodex. The retained client rollout normalizes to:
The Linux server-side rows and macOS client rollout are separate reproductions and are not presented as one cross-log correlation. The failed request's raw response headers and body were not retained. Therefore,
status=200andtransport_phase=terminal_sseabove are OpenCodex request-history fields, not an independently capturedContent-Typeheader or a complete client-facing SSE body. Together with the relay-free topology, these records support an OpenCodex client-delivery boundary failure without making the personal relay part of the reproduction or acceptance contract.Verification
Implementation and repository-level verification are complete on this draft branch. The production service was not changed, and the new fallback has not yet been exercised against a live provider.
Diagnostic basis:
stream:false, directstream:true, and four consecutive Native Codex turns complete without OpenCodex.dev, and related upstream work were reviewed; no released general fix for the default POSIX tee path was found.Implementation verification:
stream, and completed/failed/incomplete JSON produces the correct HTTP and WebSocket events.nullusage is accepted; malformed JSON, invalid usage, unknown status, oversize/stall, unexpected content types, and unexpected SSE fail without replay.Required live validation before requesting review:
Checklist
maintainer-sponsored; external contributors cannot satisfy this repository gate themselves.한국어 번역 — 접근성 제공
요약
이 PR은 사용자 정의
openai-responsesprovider가 정상적인 non-streaming Responses 객체는 반환하지만, streaming 응답을 OpenCodex를 거쳐 Native Codex까지 안정적으로 전달하지 못하는 경우에 사용할 모델별 호환성 설정을 추가합니다.기존에 내장 provider registry에서만 사용하던
modelResponsesUpstreamStreaming정책을 검증된 사용자 정의 provider 설정으로 노출했습니다. 특정 모델을false로 설정하면 client와 upstream 모두 Responses API를 계속 사용하지만, OpenCodex는 upstream에stream:false를 요청합니다. 이후 기존 제한 안에서 완료된 JSON을 읽고 streaming Codex client가 기대하는 canonical Responses event sequence로 다시 구성합니다.Bounded fallback 정책은 명시적으로 선택해야 동작하며 정확성을 우선합니다. Chat Completions로 우회하거나 OpenCode Go 또는 GPT-5.6 Luna를 하드코딩하지 않고, 근본적인 Bun/
tee()transport 결함까지 해결했다고 주장하지도 않습니다. 다만 제출된 Draft는 공용 WebSocket JSON bridge의 validation도 opt-in 범위 밖까지 강화합니다. 아래 검토 별첨에서는 이를 의도된 기본 동작 변경으로 포장하지 않고 아직 해결되지 않은 범위 문제로 기록합니다.관측 결과
검증한 두 POSIX 계열 환경에서 OpenCodex의 내부 완료 상태와 Native Codex가 실제로 받은 결과가 일치하지 않는 현상이 재현되었습니다.
response.completed를 확인하지만 Native Codex는 turn을 완료하지 못함같은 현상을 macOS arm64와 Linux x86_64에서 확인했지만, 모든 POSIX 구현이 영향을 받는다는 뜻은 아닙니다. Linux arm64, Native Windows, WSL 및 다른 POSIX 시스템은 실제 장애 재현에 포함되지 않았으며 계속 미검증 상태로 둡니다.
최초 발견 배경과 relay 독립 재현
증상은 개인적으로 사용하기 위해 독립 구현한 OCI relay가 포함된 다층 배포 환경에서 처음 발견되었습니다. 이 환경은 구조적 검토를 시작한 계기였지만, 해당 relay를 원인으로 전제하거나 재현의 필수 요소로 유지하지 않았습니다.
이후 관련 AI request stack에 Native Codex와 OpenCodex만 설치·구성된 별도의 최소 환경에서도 같은 동작을 재현했습니다. 이 환경에는 개인 OCI relay, sidecar 또는 추가 gateway/proxy가 존재하지 않았습니다. Native Codex를 OpenCodex에 직접 연결했을 때 OpenCodex는 내부 terminal state에 도달했지만 Native Codex는 turn을 commit하지 못하고 idle-timeout 경로에 진입했습니다. 반대 대조군에서는 OpenCodex와 모든 중간 relay를 제외하고 Native Codex를 같은 OpenCode Go Responses upstream에 직접 연결했으며, 하나의 대화에서 4회 연속 turn이 완료되었습니다. 따라서 이 비교에서 개인 relay는 재현에 필요하지 않았고 확인된 장애 구성 요소도 아닙니다.
이 A/B 증거는 관측된 실패 경계를 upstream 완료 이후부터 Codex client가 terminal event를 commit하기 전까지의 OpenCodex Responses relay/client-delivery 경로 내부로 좁힙니다. 다만 특정 Bun primitive 하나가 유일한 저수준 원인이라고 확정하는 증거는 아닙니다.
현행 내장 경로와 재현 경로의 차이
현행 OpenCode Go endpoint 표는 GPT 5.6 Luna(
gpt-5.6-luna)를@ai-sdk/openai기반의https://opencode.ai/zen/go/v1/responses에 명시적으로 배정합니다. 같은 표에서 DeepSeek V4 Flash 등의 모델은@ai-sdk/openai-compatible기반/v1/chat/completions로 구분합니다. 따라서 OpenCode Go는 provider 전체를 Chat Completions 하나로 취급하는 것이 아니라 모델별 protocol 차이를 공개하고 있습니다.그러나 이 PR의 정확한
devbase에서 OpenCodex의 내장opencode-goregistry entry는 provider-wide adapter를openai-chat으로 선언하고,gpt-5.6-luna용modelWireDefaultsentry를 두지 않습니다. 명시적인modelAdaptersoverride가 없으면 wire resolver는 내장 경로를openai-chat으로 유지하고, request builder는 Luna에 문서화된 Responses endpoint가 아니라${baseUrl}/chat/completions로 전송합니다.최초 재현에서는 이 별도의 내장 mapping 누락을 우회하기 위해 OpenCode Go base URL,
gpt-5.6-luna및adapter: "openai-responses"를 사용하는 Custom Provider를 등록했습니다. 민감정보를 제거한 direct probe에서는 non-streaming JSON(stream:false)과 SSE(stream:true) 모두 completed Response가 관측되었습니다. 공개 Go 문서는 endpoint를 명시하지만 두 delivery mode를 별도로 보장하지는 않으므로, 이 결과는 공식 service guarantee가 아니라 재현 근거로 기록합니다.따라서 두 문제는 구분해야 합니다. 내장 preset이 현재 Luna에 Responses를 선택하지 않는 문제와, Custom Provider가 Responses endpoint를 올바르게 선택한 뒤 OpenCodex는
response.completed에 도달하지만 Native Codex에는 확정 가능한 terminal event가 전달되지 않는 문제입니다. 이 PR은 opt-in bounded fallback으로 후자만 다루며, 내장opencode-goregistry mapping은 변경하지 않습니다.대조 결과는 실패 경계를 다음처럼 좁힙니다.
stream:false및stream:trueResponses 요청은 완료됩니다.response.completed에 도달하지만, Native Codex에는 완료로 확정할 수 있는 event가 도달하지 않고 이후 재시도합니다.이 증거만으로 특정 Bun bug 하나를 원인으로 확정할 수는 없습니다. 다만 장애 구간은 OpenCodex가 upstream stream을 받은 뒤 Native Codex가 종료 event를 받아 turn을 완료로 확정하기 전까지로 좁혀집니다. 소스 분석상 가장 유력한 가설은
ReadableStream.tee(), 서로 독립적인 inspection/client 소비 속도, JavaScript relay, SSE chunk 경계 및 backpressure의 상호작용입니다.flowchart LR C[Native Codex] -->|stream=true| O[OpenCodex] O --> U[Custom Responses upstream] U -->|유효한 SSE| T[ReadableStream.tee] T --> I[Inspection branch] I -->|response.completed| L[내부 결과: completed] T --> R[Client relay branch] R -. 완료 event가 전달되지 않음 .-> C C -->|idle timeout| X[retry 또는 failed turn] U -. 직접 대조군: 4/4 turn 완료 .-> CLinux eager relay 대신 bounded JSON을 사용하는 이유
OpenCodex는 bounded single-reader eager relay를 보수적인 runtime/platform gate 뒤에 두고 있습니다. Bundled Bun은 여전히 1.3.14이고
MIN_FIXED_BUN_VERSION도null입니다. OpenCodex가 관련 async-stream cancellation/backpressure 수정의 포함 여부를 확인한 Bun 안정 버전도 아직 없습니다.Linux를 단순히
eager-relay대상으로 추가하면 이 안전 결정을 해결하는 것이 아니라 우회하게 됩니다. 또한 Issue #820에서 이미 계획한 runtime-qualified, protocol-safe one-reader 작업과 범위가 겹칩니다.이번 제안은 OpenCodex에 이미 존재하는, 범위가 더 좁은 경로를 재사용합니다.
Client와 upstream 모두 Responses API를 유지하며 upstream의 응답 전달 방식만 달라집니다.
설정과 우선순위
{ "providers": { "<custom-responses-provider>": { "adapter": "openai-responses", "baseUrl": "<redacted-https-origin>", "authMode": "key", "modelResponsesUpstreamStreaming": { "gpt-5.6-luna": false } } } }예시에는 credential을 의도적으로 포함하지 않았습니다.
정책 조회는 대소문자를 구분하지 않지만 정확한 모델 id만 일치시키며 colon-family entry를 상속하지 않습니다. Provider namespace/combo 해석과 effective 모델별 wire 결정 이후, client-facing response-model rewrite 이전에 적용합니다. Virtual alias는 public id와 wire-model id를 모두 해석합니다.
우선순위는 다음과 같습니다.
Effective wire가
openai-responses가 아니거나 effective forward-auth provider이면 이 field를 거부하여 canonical OpenAI transport contract가 바뀌지 않게 합니다.응답 처리
Opt-in model에서 client가
stream:true를 요청하면 OpenCodex는 다음처럼 처리합니다.stream값만false로 변경합니다.response.created, output item별response.output_item.done, 원래의completed/failed/incompleteterminal 및 하나의[DONE]을 보냅니다. WebSocket에서는 SSE sentinel 없이 동등한 JSON lifecycle event를 보냅니다.Shared validator는 다음을 확인해야 합니다.
null이나 array가 아닌 top-level objectobjectfield가 문서화된 compatibility 정책에 따라 없거나response와 같음status가completed,failed,incomplete중 하나output이 array이고 각 entry가 비어 있지 않은 stringtype을 가진 objectusage가 없거나null이거나 non-negative integer input/output token count를 가진 object. 선택형 total/detail token field도 존재하면 형식을 검증함Fallback 자체는 output item이나 usage를 새로 만들지 않습니다. 기존에 명시적으로 설정한 client-facing normalization(image-call restore, response-model rewrite, snapshot repair, item-id repair)은 streaming 경로와 같은 순서로 정확히 한 번만 적용합니다. Function call, repair-enabled, parallel call 테스트로 다음 turn에서도 item id, call id, name, argument string을 사용할 수 있는지 확인해야 합니다.
Malformed JSON, unknown terminal status, invalid usage object, oversize/stall body 또는 예상하지 않은 2xx content type은 fail closed합니다. Upstream이
stream:false를 무시하고 SSE를 반환하면 의심되는 tee 경로로 fallback하거나 model 요청을 재실행하지 않습니다. Non-2xx와 안전한 retry metadata는 기존 의미를 유지하고 client cancellation은 진행 중인 upstream read를 abort해야 합니다.구현
src/types.tssrc/config.tssrc/server/auth-cors.tssrc/providers/registry.ts및 Responses route setupcompletedevent로 바뀌지 않게 함src/server/responses/core.ts와 Responses WebSocket bridge이 PR에는 시각적 인터페이스 변경이 필요하지 않습니다.
호환성, 제약 및 롤백
Bounded fallback 정책 자체는 기존 설정에 선택적으로 추가되며 명시하지 않으면 비활성 상태를 유지하는 것이 의도입니다. 그러나 제출된 head의 공용 WebSocket JSON validator는 설정하지 않은 route의 성공 JSON snapshot에도 적용됩니다. 아래 검토 별첨에서는 이를 아직 해결되지 않은 범위 불일치로 기록하며, 설정하지 않은 동작이 유지된다는 주장은 review-ready 전 이 문제를 해결하는 것을 전제로 합니다.
장점은 Responses contract와 Bun runtime gate를 약화하지 않으면서 특정 provider에만 적용되는 분기 없이 문제가 있는 streaming relay를 우회할 수 있다는 점입니다. 제약도 명확합니다. Opt-in model에서는 incremental text/reasoning/tool delta가 없고, upstream이 완료된 뒤 첫 client event가 도착하며, 완료된 응답을 기존 bounded JSON 범위 안에 보유합니다. Upstream이 non-streaming Responses를 실제로 지원해야 합니다.
정상적인 configuration reload 또는 service restart 후 model entry를
true로 설정하면 forced bounded-JSON fallback이 비활성화됩니다. 이는 client-requested/default streaming policy를 복원하지만 upstream이 실제로 stream할 것까지 보장하지는 않습니다. Entry를 제거하면 inherited registry/default policy로 복귀하며 그 값도false일 수 있습니다. Data migration은 필요하지 않습니다.이 PR은 #820을 닫지 않아야 합니다. 장기 해결책은 지원 플랫폼에서 true streaming을 보존하는 runtime-qualified one-reader relay입니다.
관련 작업
#820 — 더 넓은 runtime-qualified, protocol-safe one-reader architecture. 이번 제안은 의도적으로 범위가 더 좁습니다.
#1127 — upstream/internal completion 이후 client SSE event가 0개였던 유사한 macOS 증상
#1142 — client-rewrite traffic에 대한 Darwin explicit eager relay 수정. Darwin
auto와 Linux는 의도적으로 변경하지 않았습니다.#947 — transport predicate가 #1142에 attribution된 닫힌 미병합 선행 PR
#1133 — downstream byte를 유지하면서 translated SSE inspection을 bound
#1241 — tee/client-pull 경계를 제거하지 않고 client-facing SSE frame retention을 bound
#1217 — 요청·응답 본문을 남기지 않는 transport 관측성에 관한 상호 보완 작업
#1176 — regression 및 운영 위험에 포함해야 하는 별도의 bounded-JSON timeout tradeoff
#1026 — 이번 변경이 재사용하는 bounded JSON 및 canonical event reframe 기반
#1155 — web-search 처리를 위해 registry streaming policy를 다루는 열린 모델별 제안. 검증된 custom-provider 정책을 노출하지는 않습니다.
현재 열린 Issue/PR 검색에서는 이 사용자 정의 provider 설정을 구현하는 작업을 찾지 못했습니다.
범위
포함:
openai-responsesprovider제외:
opencode-go의gpt-5.6-lunawire mapping 변경streamMode변경tee()제거 또는 bundled Bun upgrade구현 중 진단 정보를 추가하더라도 요청·응답 본문을 포함하지 않아야 합니다. Status, content-type 범주, byte count, 상대 timing, 선택 mode, terminal type, cancellation, bounded-read 결과만 허용합니다. Credential, provider origin, query string, prompt, output text, raw SSE/JSON, account id 및 원문 request/thread/response id는 기록하지 않습니다.
검토 별첨 — 확인된 blocker와 후속 위험
제출된 head에 대한 2차 정적 검토에서 구체적인 코드 문제 두 가지와 아직 완료되지 않은 acceptance gate 한 가지가 확인되었습니다. 이 별첨은 현재 Draft 상태를 기록하며, 해당 finding이 이미 수정됐다고 주장하지 않습니다. 또한 maintainer 승인이나 GitHub의 공식 review 판정도 아닙니다.
Review-ready 전 반드시 해결할 항목
modelResponsesUpstreamStreaming=false가 bounded fallback을 선택했는지 알지 못한 채 성공한 모든 Responses JSON snapshot을 엄격하게 검증합니다. 변경 전에는 설정하지 않은 sparse JSON snapshot이 compatibility default를 사용해 event로 재구성될 수 있었지만, 제출된 head에서는 502 protocol error가 됩니다. 가장 안전한 해결책은 bounded-fallback 내부 discriminator를 WebSocket bridge까지 전달해 새 strict contract를 해당 경로에만 적용하는 것입니다. 전역 fail-closed validation을 의도적으로 유지한다면 더 넓은 behavioral change로 문서화하고 회귀 테스트를 추가해야 하며, 가능하면 별도 PR로 분리하는 편이 적절합니다.비차단 후속 위험
처리 방침
Architecture 방향은 유지합니다. Workaround는 provider-independent하게 유지하고, client와 upstream 모두 Responses를 사용하며, 처리 여부가 불명확한 model request를 replay하지 않습니다. 두 코드 blocker를 수정하고 집중 테스트를 추가하며, live fallback canary와 repository review gate를 완료하기 전까지 이 Draft는 review-ready로 전환하지 않습니다. Timeout, duplicate validation 및 WebSocket backpressure는 acceptance evidence가 심각도를 높이지 않는 한 별도 후속 작업으로 관리할 수 있습니다.
검열된 보존 런타임 증거
아래 excerpt는 실제로 보존된 canary/tool output, OpenCodex request history 및 Native Codex rollout record에서 안전한 field만 선별한 것입니다. Model output을 재구성한 것이 아닙니다. Secret, prompt, response, tool argument, opaque identifier, 정확한 timestamp, host 정보, 로컬 경로 및 private endpoint는 제거했습니다. 상대 시간
t+만 보존 timestamp에서 파생했으며 protocol outcome과 duration은 기록값을 유지했습니다. 이 별첨을 만들기 위한 새 model call은 수행하지 않았습니다.직접 연결 대조군 — Native Codex에서 OpenCode Go Responses로
보존된 4-turn canary output에서 선별한 field입니다.
이 대조군은 하나의 persistent Native Codex thread에서 같은 OpenCode Go Responses upstream을 직접 호출했으며 OpenCodex endpoint, wrapper 또는 기존 AppServer를 사용하지 않았습니다. 따라서 direct client/upstream 경로가 연속 turn을 완료할 수 있음을 보여줍니다. 이번 PR의 새 fallback이나 tool-call round trip을 검증한 것은 아닙니다.
실패 재현 — OpenCodex server-side history
아래 normalized row는 Linux/x86_64의 동일한 OpenCodex conversation에서 보존된 기록입니다. Private correlation identifier는 제거했고
t+는 첫 요청 기준 상대 시간입니다.OpenCodex는 매 attempt에서 2.155~4.690초 안에 upstream terminal completion을 기록했지만 같은 conversation의 요청은 302.694, 301.943, 302.629, 302.933 및 304.393초 간격으로 반복됐습니다. 이 cadence는 Native Codex의 300초 stream-idle 경계와 일치하지만, 원본 client retry 진단 문자열은 보존되지 않았으므로 직접 retry log라고 주장하지 않습니다.
실패 재현 — Native Codex client rollout
별도의 macOS/arm64 재현은 Native Codex와 OpenCodex만 있는 최소 request stack에서 수행했습니다. 보존된 client rollout을 정규화하면 다음과 같습니다.
Linux server-side row와 macOS client rollout은 서로 다른 재현이며 하나의 cross-log correlation처럼 제시하지 않습니다. 실패 요청의 원본 response header와 body는 보존되지 않았습니다. 따라서 위의
status=200및transport_phase=terminal_sse는 OpenCodex request-history field이며,Content-Typeheader 또는 client-facing SSE 전체 body를 독립적으로 캡처했다는 의미가 아닙니다. Relay가 없는 topology와 함께 보면, 이 기록은 개인 relay를 재현 또는 acceptance contract에 포함하지 않으면서 OpenCodex client-delivery 경계의 실패를 뒷받침합니다.검증
이 Draft branch의 구현 및 repository 수준 검증은 완료했습니다. 운영 서비스는 변경하지 않았고 새 fallback을 실제 provider에 적용하는 live canary는 아직 수행하지 않았습니다.
진단 근거:
stream:false, directstream:true, 하나의 대화에서 4회 연속 Native Codex turn이 모두 완료됨dev및 관련 upstream 작업을 검토했지만 정식 릴리스에서 기본 POSIX tee 경로를 일반적으로 해결한 수정은 찾지 못함구현 검증:
streamfield만 바뀌고 completed/failed/incomplete JSON이 HTTP와 WebSocket에서 올바른 event로 변환됨nullusage는 허용하고 malformed JSON, invalid usage, unknown status, oversize/stall, 예상 밖 content type 및 SSE는 replay 없이 실패함리뷰 요청 전 필수 실환경 검증:
체크리스트
maintainer-sponsoredlabel이 필요하며 외부 기여자가 직접 충족할 수 없습니다.Review readiness checklist
This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:
All CI tests are green on my local testing.
I pushed my PR to the latest dev commit.
I resolved all correct Codex and CodeRabbit findings.
My PR is ready for review.