Skip to content

feat(responses): add an opt-in bounded JSON fallback for custom providers - #1367

Draft
novelKR wants to merge 1 commit into
lidge-jun:devfrom
novelKR:agent/responses-bounded-json-fallback
Draft

feat(responses): add an opt-in bounded JSON fallback for custom providers#1367
novelKR wants to merge 1 commit into
lidge-jun:devfrom
novelKR:agent/responses-bounded-json-fallback

Conversation

@novelKR

@novelKR novelKR commented Aug 9, 2026

Copy link
Copy Markdown

Summary

This PR adds an opt-in per-model fallback for custom openai-responses providers 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 modelResponsesUpstreamStreaming policy to validated custom-provider configuration. Setting a model to false keeps the Responses API on both sides of the proxy, but asks the upstream for stream: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:

Environment Architecture Result through OpenCodex
macOS arm64 OpenCodex sees response.completed, but Native Codex does not complete the turn
GNU/Linux over SSH x86_64 The same mismatch occurs, followed by Native Codex's stream-idle retry

We 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) to https://opencode.ai/zen/go/v1/responses using @ai-sdk/openai. The same matrix assigns models such as DeepSeek V4 Flash to /v1/chat/completions using @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 dev base, however, OpenCodex's built-in opencode-go registry entry declares the provider-wide adapter as openai-chat and has no modelWireDefaults entry for gpt-5.6-luna. Without an explicit modelAdapters override, the wire resolver therefore keeps the built-in route on openai-chat, whose request builder posts to ${baseUrl}/chat/completions instead 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, and gpt-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-in opencode-go registry mapping.

The controls narrow the failure boundary:

  • direct stream:false and stream:true calls to the affected Responses upstream complete;
  • one Native Codex conversation completes four consecutive turns against that upstream without OpenCodex;
  • minimal calls through the same OpenCodex route complete;
  • the reviewed official OpenAI and custom DeepSeek Responses paths complete through OpenCodex;
  • the affected richer OpenCode Go turn reaches response.completed in 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 .-> C
Loading

Why 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_VERSION is still null, and OpenCodex has not yet verified a stable Bun release for the relevant async-stream cancellation/backpressure path.

Simply allowing Linux to enter eager-relay would 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:

Native Codex
  POST /v1/responses, stream:true
        │
        ▼
OpenCodex
  modelResponsesUpstreamStreaming[model] = false
        │
        ▼
Responses upstream
  POST /v1/responses, stream:false
        │
        ▼
completed Responses JSON
        │
        ▼
OpenCodex bounded validation + canonical SSE reframe
        │
        ▼
Native Codex

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:

  1. an explicit configured value for the selected public model id, then its resolved wire-model id;
  2. a matching built-in registry default for those same ids when the configured transport still matches that registry entry;
  3. no override, preserving the existing client-requested behavior.

The field should be rejected when the effective wire is not openai-responses and 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:

  1. preserve the existing request semantics and change only the upstream stream value to false;
  2. read the JSON body through the existing total-size, total-time, and inactivity limits;
  3. validate the completed/failed/incomplete Responses object through one shared validator used by both HTTP synthesis and Responses WebSocket reframing;
  4. for HTTP/SSE, emit response.created, one response.output_item.done per output item, the original completed/failed/incomplete terminal, and one [DONE]; for WebSocket, emit the equivalent JSON lifecycle events without an SSE sentinel;
  5. close without waiting for an upstream SSE EOF because this path received bounded JSON, not a live stream.

The shared validator should require:

  • a 2xx JSON Responses object with a non-empty response id;
  • a top-level object rather than null or an array;
  • object either omitted under the documented compatibility policy or equal to response;
  • terminal status equal to completed, failed, or incomplete;
  • an output array whose entries are objects with non-empty string type fields;
  • usage either 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:false and 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.ts
    • add the documented optional provider field.
  • src/config.ts
    • validate non-empty model keys and boolean values;
    • enforce the effective-wire and forward-auth restrictions.
  • src/server/auth-cors.ts
    • mirror the same validation at the management write boundary so startup loading and persisted writes cannot disagree.
  • src/providers/registry.ts and the Responses route setup
    • resolve explicit config before the registry fallback;
    • use the case-insensitive model-map helper;
    • resolve the effective per-model wire before applying the policy.
  • the shared Responses JSON event boundary
    • validate terminal JSON through one shared validator for HTTP and WebSocket reframing;
    • do not let an unknown or missing status fall through to a default completed event.
  • src/server/responses/core.ts and the Responses WebSocket bridge
    • reuse the current bounded JSON and event-reframing machinery;
    • fail once on unexpected SSE rather than retrying or entering tee.
  • focused tests and provider-configuration documentation
    • cover exact-model precedence, aliases, HTTP/WebSocket parity, terminal status preservation, tool ids, limits, cancellation, and rollback.

No 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 true disables 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 be false. 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 auto and 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:

  • custom openai-responses providers;
  • explicit per-model opt-in;
  • HTTP/SSE and existing Responses WebSocket reframing;
  • strict bounded JSON validation and canonical snapshot events;
  • synthetic, credential-free tests;
  • live validation on macOS arm64 and Linux x86_64 before requesting review.

Out of scope:

  • Completion API routing;
  • changing the built-in opencode-go wire mapping for gpt-5.6-luna;
  • provider- or model-name heuristics;
  • changing the default streamMode;
  • enabling Linux eager relay on Bun 1.3.14;
  • globally removing tee() or upgrading bundled Bun;
  • logging live request or response content;
  • native Windows, WSL, Linux arm64, or untested POSIX compatibility claims;
  • upstream-provider changes or a post-OpenCodex sidecar.

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

  1. Shared WebSocket validation exceeds the opt-in scope. The current WebSocket bridge strictly validates every successful Responses JSON snapshot, without knowing whether modelResponsesUpstreamStreaming=false selected 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.
  2. Policy keys are not canonicalized. A key with surrounding whitespace passes configuration validation but is not found by runtime lookup. Keys that differ only by case may also hold conflicting values, making the result depend on exact request casing or insertion order. The policy-specific validator must reject surrounding whitespace and case-insensitive duplicates at both startup and management-write boundaries, with a runtime lookup regression test for a canonical key.
  3. The new fallback still lacks live Native Codex acceptance. Synthetic tests cover event order, terminal status, function calls, repairs, cancellation, and no replay, but they do not prove that Native Codex commits a real turn and continues the conversation through this fallback. Before review readiness, an isolated canary must complete an accumulating multi-turn workflow on macOS arm64 and Linux x86_64 with one upstream request per turn, zero client retries, exactly one terminal outcome, preserved tool-call identifiers, and content-free cleanup evidence. The personal relay was not needed to reproduce the failure and is outside this upstream acceptance criterion; deployment-specific relays or sidecars should be validated separately by their operators.

Non-blocking follow-up risks

  • Bounded-body timeout visibility. The reused limits allow 180 seconds for the first byte and total body, then 30 seconds between non-empty chunks. Long generation before the first body byte is therefore not itself a 30-second failure; the risk is an upstream that starts a JSON body and then pauses between chunks. Timeout phase, chunk count, received bytes, and relative timing should be exposed through content-free telemetry instead of collapsing every timeout into the same 502 message.
  • Repeated full validation. Raw and client-repaired snapshots should each be validated once, but the current HTTP/SSE path scans output roughly three times and the WebSocket path can scan it roughly five times. The body and item caps keep this bounded, so it is not a correctness blocker, but serializers should eventually consume a validated result or an internal unchecked iterator instead of rescanning the same repaired snapshot.
  • WebSocket backpressure. JSON reframing materializes the lifecycle event array and sends it synchronously. A queued/backpressured Bun send is accepted without pausing production, so a slow client can retain individual item frames alongside the final full snapshot. Existing Bun/body/item limits keep this from being an unbounded claim, but a drain-aware sender and slow-consumer stress tests remain appropriate follow-up work.

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:

source=retained_direct_canary_output
route=direct_https_responses
opencodex_endpoint_used=false
existing_wrapper_used=false
existing_appserver_used=false
config_wire_responses=true
config_websockets_false=true
config_retries_zero=true
config_reasoning_unset=true
turn=1 elapsed_ms=4588 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
turn=2 elapsed_ms=5499 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
turn=3 elapsed_ms=3825 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
turn=4 elapsed_ms=3789 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
persistent_thread_turns_completed=4 overall_success=true cleanup=true ssh_exit=0

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.

source=retained_opencodex_request_history
t+0.000s    status=200 duration_ms=4690 first_output_ms=2998 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+302.694s  status=200 duration_ms=2246 first_output_ms=2192 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+604.637s  status=200 duration_ms=2634 first_output_ms=2628 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+907.266s  status=200 duration_ms=2420 first_output_ms=2416 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+1210.199s status=200 duration_ms=2853 first_output_ms=2770 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+1514.592s status=200 duration_ms=2155 first_output_ms=2152 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream

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:

source=retained_native_codex_rollout
event_sequence=task_started -> token_count -> token_count -> token_count -> token_count -> turn_aborted
assistant_messages=0 task_complete=0 tool_calls=0
turn_aborted=1 turn_aborted_duration_ms=1026650 turn_aborted_reason=interrupted
personal_relay_present=false sidecar_present=false additional_gateway_present=false

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=200 and transport_phase=terminal_sse above are OpenCodex request-history fields, not an independently captured Content-Type header 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:

  • Direct stream:false, direct stream:true, and four consecutive Native Codex turns complete without OpenCodex.
  • The failure remains reproducible in a separate minimal environment whose relevant AI request stack contains only Native Codex and OpenCodex, with no personal OCI relay, sidecar, or additional gateway/proxy.
  • On macOS arm64 and Linux x86_64, OpenCodex sees the terminal event while Native Codex does not complete the affected turn.
  • Minimal requests through the same proxy route complete, while the richer request reproduces the failure.
  • Official OpenAI and custom DeepSeek Responses paths provide successful controls in the reviewed evidence.
  • OpenCodex 2.10.1 through 2.11.1, current dev, and related upstream work were reviewed; no released general fix for the default POSIX tee path was found.

Implementation verification:

  • Config-file and management-write validation agree; model/wire precedence and unconfigured behavior are covered.
  • The upstream request changes only stream, and completed/failed/incomplete JSON produces the correct HTTP and WebSocket events.
  • Function-call, parallel-call, virtual-model, snapshot-repair, item-id, size-limit, and cancellation paths are covered with synthetic fixtures.
  • Absent or null usage is accepted; malformed JSON, invalid usage, unknown status, oversize/stall, unexpected content types, and unexpected SSE fail without replay.
  • A never-settling body cancellation cannot delay the fail-closed 502 response.
  • Focused tests, type checking, documentation build, privacy scan, and the repository pre-push gate pass on the submitted head.

Required live validation before requesting review:

  • Use an isolated canary without changing the active 2.10.1 service.
  • Complete at least four turns in one accumulating conversation on macOS arm64 and repeat the same workflow on Linux x86_64 over SSH.
  • Keep retries at zero, confirm no duplicate billable request, and record exactly one client terminal and matching turn outcome.
  • Retain only content-free evidence: mode, status, event counts/types, byte counts, elapsed time, terminal outcome, and cleanup state.
  • Keep Linux arm64, native Windows, WSL, and other POSIX systems marked unverified unless separately exercised.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.
  • Fixtures and evidence contain no live secret, private endpoint, payload, prompt/output, account id, or raw identifier; the public OpenCode Go endpoint above is cited only as protocol documentation.
  • A maintainer must review the management-validation change and apply maintainer-sponsored; external contributors cannot satisfy this repository gate themselves.
  • Live fallback validation on macOS arm64 and Linux x86_64 remains a follow-up before this draft is marked ready for review.

한국어 번역 — 접근성 제공

아래 내용은 위 영문 PR 본문의 접근성용 한국어 번역입니다. 제출 기준이 되는 원문은 영문이며, 구현 범위·검증 상태·제약 조건은 두 판본에서 동일합니다.

요약

이 PR은 사용자 정의 openai-responses provider가 정상적인 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가 실제로 받은 결과가 일치하지 않는 현상이 재현되었습니다.

환경 아키텍처 OpenCodex 경유 결과
macOS arm64 OpenCodex는 response.completed를 확인하지만 Native Codex는 turn을 완료하지 못함
SSH 기반 GNU/Linux x86_64 같은 불일치가 나타난 뒤 Native Codex가 stream-idle 재시도에 진입함

같은 현상을 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의 정확한 dev base에서 OpenCodex의 내장 opencode-go registry entry는 provider-wide adapter를 openai-chat으로 선언하고, gpt-5.6-lunamodelWireDefaults entry를 두지 않습니다. 명시적인 modelAdapters override가 없으면 wire resolver는 내장 경로를 openai-chat으로 유지하고, request builder는 Luna에 문서화된 Responses endpoint가 아니라 ${baseUrl}/chat/completions로 전송합니다.

최초 재현에서는 이 별도의 내장 mapping 누락을 우회하기 위해 OpenCode Go base URL, gpt-5.6-lunaadapter: "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-go registry mapping은 변경하지 않습니다.

대조 결과는 실패 경계를 다음처럼 좁힙니다.

  • 영향받는 upstream에 직접 보낸 stream:falsestream:true Responses 요청은 완료됩니다.
  • OpenCodex 없이 동일 upstream을 사용한 하나의 Native Codex 대화에서 4회 연속 turn이 모두 완료됩니다.
  • 같은 OpenCodex route의 최소 요청은 완료됩니다.
  • 검토한 공식 OpenAI 및 custom DeepSeek Responses 경로는 OpenCodex를 통해 완료됩니다.
  • 문제가 발생한 rich OpenCode Go turn은 OpenCodex inspection/state에서 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 완료 .-> C
Loading

Linux eager relay 대신 bounded JSON을 사용하는 이유

OpenCodex는 bounded single-reader eager relay를 보수적인 runtime/platform gate 뒤에 두고 있습니다. Bundled Bun은 여전히 1.3.14이고 MIN_FIXED_BUN_VERSIONnull입니다. OpenCodex가 관련 async-stream cancellation/backpressure 수정의 포함 여부를 확인한 Bun 안정 버전도 아직 없습니다.

Linux를 단순히 eager-relay 대상으로 추가하면 이 안전 결정을 해결하는 것이 아니라 우회하게 됩니다. 또한 Issue #820에서 이미 계획한 runtime-qualified, protocol-safe one-reader 작업과 범위가 겹칩니다.

이번 제안은 OpenCodex에 이미 존재하는, 범위가 더 좁은 경로를 재사용합니다.

Native Codex
  POST /v1/responses, stream:true
        │
        ▼
OpenCodex
  modelResponsesUpstreamStreaming[model] = false
        │
        ▼
Responses upstream
  POST /v1/responses, stream:false
        │
        ▼
완료된 Responses JSON
        │
        ▼
OpenCodex bounded validation + canonical SSE 재구성
        │
        ▼
Native Codex

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를 모두 해석합니다.

우선순위는 다음과 같습니다.

  1. 선택된 public model id와 그 다음 해석된 wire-model id의 명시적 설정값
  2. 설정 transport와 일치할 때 같은 두 id에 대한 built-in registry 기본값
  3. override가 없으면 현재 client-requested 동작 유지

Effective wire가 openai-responses가 아니거나 effective forward-auth provider이면 이 field를 거부하여 canonical OpenAI transport contract가 바뀌지 않게 합니다.

응답 처리

Opt-in model에서 client가 stream:true를 요청하면 OpenCodex는 다음처럼 처리합니다.

  1. 기존 request 의미를 보존하고 upstream stream 값만 false로 변경합니다.
  2. 기존 total-size, total-time, inactivity limit을 사용해 JSON body를 읽습니다.
  3. HTTP synthesis와 Responses WebSocket reframing에서 함께 쓰는 하나의 validator로 completed/failed/incomplete Responses 객체를 검증합니다.
  4. HTTP/SSE에서는 response.created, output item별 response.output_item.done, 원래의 completed/failed/incomplete terminal 및 하나의 [DONE]을 보냅니다. WebSocket에서는 SSE sentinel 없이 동등한 JSON lifecycle event를 보냅니다.
  5. 이 경로는 live stream이 아니라 bounded JSON을 받았으므로 upstream SSE EOF를 기다리지 않습니다.

Shared validator는 다음을 확인해야 합니다.

  • 2xx JSON Responses 객체와 비어 있지 않은 response id
  • null이나 array가 아닌 top-level object
  • object field가 문서화된 compatibility 정책에 따라 없거나 response와 같음
  • terminal statuscompleted, failed, incomplete 중 하나
  • output이 array이고 각 entry가 비어 있지 않은 string type을 가진 object
  • usage가 없거나 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.ts
    • 문서화된 optional provider field 추가
  • src/config.ts
    • 비어 있지 않은 model key와 boolean value 검증
    • effective-wire 및 forward-auth 제한 적용
  • src/server/auth-cors.ts
    • management write boundary에도 같은 검증을 적용하여 startup loading과 persisted write의 불일치 방지
  • src/providers/registry.ts 및 Responses route setup
    • registry fallback보다 명시적 config를 우선
    • case-insensitive model-map helper 사용
    • policy 적용 전에 effective model별 wire 결정
  • shared Responses JSON event boundary
    • HTTP 및 WebSocket reframing에서 terminal JSON을 같은 방식으로 검증
    • unknown/missing status가 default completed event로 바뀌지 않게 함
  • src/server/responses/core.ts와 Responses WebSocket bridge
    • 기존 bounded JSON 및 event-reframing 로직 재사용
    • 예상하지 않은 SSE에서 retry나 tee 진입 없이 한 번 실패
  • 집중 테스트와 provider-configuration 문서
    • exact-model precedence, alias, HTTP/WebSocket parity, terminal status, tool id, limit, cancellation, rollback 검증

이 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 설정을 구현하는 작업을 찾지 못했습니다.

범위

포함:

  • custom openai-responses provider
  • 명시적 모델별 opt-in
  • HTTP/SSE 및 기존 Responses WebSocket reframing
  • 엄격한 bounded JSON validation과 canonical snapshot event
  • credential이 없는 synthetic 테스트
  • 리뷰 요청 전 macOS arm64 및 Linux x86_64 실환경 검증

제외:

  • Completion API route
  • 내장 opencode-gogpt-5.6-luna wire mapping 변경
  • provider/model-name heuristic
  • 기본 streamMode 변경
  • Bun 1.3.14에서 Linux eager relay 활성화
  • 전역 tee() 제거 또는 bundled Bun upgrade
  • 실제 request/response content 로깅
  • Native Windows, WSL, Linux arm64 또는 미검증 POSIX compatibility 주장
  • upstream provider 수정 또는 post-OpenCodex sidecar

구현 중 진단 정보를 추가하더라도 요청·응답 본문을 포함하지 않아야 합니다. 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 전 반드시 해결할 항목

  1. 공용 WebSocket validation이 opt-in 범위를 넘습니다. 현재 WebSocket bridge는 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로 분리하는 편이 적절합니다.
  2. 정책 key가 canonicalize되지 않습니다. 앞뒤 공백이 있는 key는 configuration validation을 통과하지만 runtime lookup에서 발견되지 않습니다. 대소문자만 다른 key가 서로 충돌하는 값을 가질 수도 있어 exact request casing 또는 insertion order에 따라 결과가 달라집니다. 정책 전용 validator는 startup과 management-write boundary 모두에서 surrounding whitespace 및 case-insensitive duplicate를 거부해야 하며, canonical key의 runtime lookup regression test도 필요합니다.
  3. 새 fallback에 대한 Native Codex 실환경 acceptance가 아직 없습니다. Synthetic test는 event 순서, terminal status, function call, repair, cancellation 및 no replay를 검증하지만, Native Codex가 이 fallback을 통해 실제 turn을 commit하고 다음 대화를 계속하는지는 증명하지 않습니다. Review-ready 전 격리 canary에서 macOS arm64와 Linux x86_64의 누적 multi-turn workflow를 완료하고, turn별 upstream request 1회, client retry 0회, terminal outcome 정확히 1개, tool-call identifier 보존 및 content-free cleanup evidence를 확인해야 합니다. 개인 relay는 장애 재현에 필요하지 않았으며 이 upstream acceptance 기준의 범위 밖입니다. 배포별 relay 또는 sidecar는 해당 운영자가 별도로 검증해야 합니다.

비차단 후속 위험

  • Bounded-body timeout 관측성. 재사용하는 제한은 first byte 및 전체 body에 180초, non-empty chunk 사이에 30초를 허용합니다. 따라서 첫 body byte 전의 긴 generation 자체가 30초 실패를 의미하지는 않으며, 위험은 upstream이 JSON body를 시작한 뒤 chunk 사이에서 멈추는 경우입니다. 모든 timeout을 같은 502 message로 합치기보다 timeout phase, chunk count, received byte 및 상대 timing을 content-free telemetry로 제공하는 편이 좋습니다.
  • 반복적인 전체 validation. Raw snapshot과 client repair 이후 snapshot은 각각 한 번 검증할 가치가 있지만, 현재 HTTP/SSE 경로는 output을 대략 세 번, WebSocket 경로는 대략 다섯 번 순회할 수 있습니다. Body/item cap으로 제한되므로 correctness blocker는 아니지만, serializer가 같은 repaired snapshot을 다시 순회하지 않도록 validated result 또는 내부 unchecked iterator를 재사용하는 후속 개선이 필요합니다.
  • WebSocket backpressure. JSON reframing은 lifecycle event array를 materialize한 뒤 동기적으로 전송합니다. Bun send가 queue/backpressure 상태여도 생성을 멈추지 않으므로 느린 client에서는 개별 item frame과 최종 전체 snapshot이 함께 유지될 수 있습니다. Bun/body/item limit이 있어 무제한 증가라고 볼 수는 없지만, drain-aware sender와 slow-consumer stress test는 적절한 후속 작업입니다.

처리 방침

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입니다.

source=retained_direct_canary_output
route=direct_https_responses
opencodex_endpoint_used=false
existing_wrapper_used=false
existing_appserver_used=false
config_wire_responses=true
config_websockets_false=true
config_retries_zero=true
config_reasoning_unset=true
turn=1 elapsed_ms=4588 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
turn=2 elapsed_ms=5499 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
turn=3 elapsed_ms=3825 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
turn=4 elapsed_ms=3789 turn_completed=1 agent_messages=1 usage_present=1 retry_markers=0 error_events=0 tool_items=0 thread_consistent=true sentinel_exact=true success=true
persistent_thread_turns_completed=4 overall_success=true cleanup=true ssh_exit=0

이 대조군은 하나의 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+는 첫 요청 기준 상대 시간입니다.

source=retained_opencodex_request_history
t+0.000s    status=200 duration_ms=4690 first_output_ms=2998 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+302.694s  status=200 duration_ms=2246 first_output_ms=2192 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+604.637s  status=200 duration_ms=2634 first_output_ms=2628 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+907.266s  status=200 duration_ms=2420 first_output_ms=2416 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+1210.199s status=200 duration_ms=2853 first_output_ms=2770 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream
t+1514.592s status=200 duration_ms=2155 first_output_ms=2152 terminal_status=completed close_reason=terminal transport_phase=terminal_sse terminal_source=upstream

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을 정규화하면 다음과 같습니다.

source=retained_native_codex_rollout
event_sequence=task_started -> token_count -> token_count -> token_count -> token_count -> turn_aborted
assistant_messages=0 task_complete=0 tool_calls=0
turn_aborted=1 turn_aborted_duration_ms=1026650 turn_aborted_reason=interrupted
personal_relay_present=false sidecar_present=false additional_gateway_present=false

Linux server-side row와 macOS client rollout은 서로 다른 재현이며 하나의 cross-log correlation처럼 제시하지 않습니다. 실패 요청의 원본 response header와 body는 보존되지 않았습니다. 따라서 위의 status=200transport_phase=terminal_sse는 OpenCodex request-history field이며, Content-Type header 또는 client-facing SSE 전체 body를 독립적으로 캡처했다는 의미가 아닙니다. Relay가 없는 topology와 함께 보면, 이 기록은 개인 relay를 재현 또는 acceptance contract에 포함하지 않으면서 OpenCodex client-delivery 경계의 실패를 뒷받침합니다.

검증

이 Draft branch의 구현 및 repository 수준 검증은 완료했습니다. 운영 서비스는 변경하지 않았고 새 fallback을 실제 provider에 적용하는 live canary는 아직 수행하지 않았습니다.

진단 근거:

  • OpenCodex 없이 direct stream:false, direct stream:true, 하나의 대화에서 4회 연속 Native Codex turn이 모두 완료됨
  • 관련 AI request stack에 Native Codex와 OpenCodex만 설치·구성되고 개인 OCI relay, sidecar 또는 추가 gateway/proxy가 없는 별도 최소 환경에서도 장애가 재현됨
  • macOS arm64 및 Linux x86_64에서 OpenCodex는 종료 event를 확인하지만 Native Codex는 해당 turn을 완료하지 못하는 현상을 재현함
  • 같은 proxy route에서 최소 요청은 완료되지만 rich 요청은 장애를 재현함
  • 검토한 증거에서 공식 OpenAI와 custom DeepSeek Responses 경로가 성공 대조군으로 동작함
  • OpenCodex 2.10.1부터 2.11.1, 현재 dev 및 관련 upstream 작업을 검토했지만 정식 릴리스에서 기본 POSIX tee 경로를 일반적으로 해결한 수정은 찾지 못함

구현 검증:

  • Config-file과 management-write validation이 일치하고 model/wire 우선순위와 미설정 동작을 검증함
  • Upstream request에서는 의도한 stream field만 바뀌고 completed/failed/incomplete JSON이 HTTP와 WebSocket에서 올바른 event로 변환됨
  • Function-call, parallel-call, virtual-model, snapshot-repair, item-id, size-limit 및 cancellation 경로를 synthetic fixture로 검증함
  • Absent 또는 null usage는 허용하고 malformed JSON, invalid usage, unknown status, oversize/stall, 예상 밖 content type 및 SSE는 replay 없이 실패함
  • Body cancellation이 영원히 끝나지 않아도 fail-closed 502 응답이 지연되지 않음
  • 집중 테스트, typecheck, 문서 build, privacy scan 및 repository pre-push gate가 제출 head에서 통과함

리뷰 요청 전 필수 실환경 검증:

  • 활성 2.10.1 service를 변경하지 않는 isolated canary를 사용함
  • macOS arm64에서 한 대화의 문맥이 누적되는 turn을 4개 이상 완료하고 SSH 기반 Linux x86_64에서 같은 workflow를 반복함
  • Retry를 0으로 유지하고 과금 가능한 중복 요청이 없으며, client terminal 하나와 대응하는 turn outcome 하나만 기록되는지 확인함
  • Mode, status, event count/type, byte count, elapsed time, terminal outcome, cleanup state만 증거로 보존함
  • Linux arm64, Native Windows, WSL 및 다른 POSIX 시스템은 별도 검증 전까지 미검증 표시를 유지함

체크리스트

  • 관련 없는 정리 없이 범위를 집중해서 유지했습니다.
  • 필요한 문서 또는 release note를 갱신했습니다.
  • Secret, auth, unsafe default 관점의 보안 검토를 완료했습니다.
  • Fixture와 증거에 실제 secret, private endpoint, payload, prompt/output, account id 또는 raw identifier가 없습니다. 위의 공개 OpenCode Go endpoint는 protocol 문서 근거로만 인용했습니다.
  • Management validation 변경은 maintainer 검토 후 maintainer-sponsored label이 필요하며 외부 기여자가 직접 충족할 수 없습니다.
  • 이 Draft를 review-ready로 전환하기 전 macOS arm64 및 Linux x86_64 live fallback 검증이 남아 있습니다.

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.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d759805f-9d54-4aca-a18d-b2878a7e50e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/server/auth-cors.ts.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ 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.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

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

Labels

enhancement New feature or request intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant