Skip to content

[Provider compatibility]: zero-output bare SSE error commits a failover combo target #4808

Closed

Description

@87003697

Client or integration

Codex App

Provider or upstream service

Xhub (api15.xhub.chat), through generic openai-responses providers

OpenCodex version

2.56.0

Endpoint or capability

/v1/responses, streaming, strategy: "failover" combos

Current behaviour

A provider can return HTTP 200 with text/event-stream and then emit a bare SSE error before any text, reasoning, tool call, or other meaningful output. A representative redacted frame is:

event: error
data: {"type":"error","error":{"type":"server_error","code":"upstream_server_error","message":"temporarily unavailable"}}

The combo stream preflight does not classify this default bare type: "error" payload as a retryable zero-output terminal. Because an unknown event commits the child conservatively, the combo returns the failed stream instead of trying the next eligible target. In VS Code 1.137.0 / Copilot Chat 0.65.0 this surfaced as The model provider reported a failed response without any error details (event: error).

By contrast, ordinary pre-stream HTTP 503 responses in the same setup do traverse the remaining combo targets.

Expected behaviour

When a bare SSE error arrives before the combo commit boundary and contains concrete retryable provider evidence, such as a structured 429 or 5xx status/code, the combo should classify that attempt through the existing bounded failure policy and may advance to the next eligible target.

Unknown bare errors, request/policy errors, and every error after text, reasoning, tool/action, or other output begins should remain fail-closed and must not be replayed. This matches the replay-safety contract established for zero-output response.failed in #2431/#2449.

Minimal redacted request or reproduction

# Configure combo/repro with two openai-responses targets and strategy: failover.
# Target A returns HTTP 200, Content-Type: text/event-stream, then only:
#
#   event: error
#   data: {"type":"error","error":{"type":"server_error","code":"upstream_server_error","message":"temporarily unavailable"}}
#
# Target B returns a normal Responses SSE completion.

curl -N http://127.0.0.1:10100/v1/responses \
  -H "content-type: application/json" \
  -d "{\"model\":\"combo/repro\",\"input\":\"reply with ok\",\"stream\":true}"

# Actual: target A is committed and its error reaches the client; B is not attempted.
# Expected: A is recorded as a retryable zero-output failure and B is attempted.

Actual response or error

The model provider reported a failed response without any error details (event: error).

OpenCodex service logs do not retain the raw upstream SSE frame. The frame above is a redacted representative of the observed event: error / type: "error" shape; no credentials, affected request bodies, or private provider identifiers are included.

Upstream documentation

No public Xhub specification for this SSE error envelope was found. The compatibility requirement is concrete client behaviour plus the existing OpenCodex combo contract: retryable failures before meaningful output may fail over, while post-output failures must not replay.

Related contract and prior fix: #2431 and #2449.

Suggested mapping or implementation notes

src/server/responses/combo-stream-preflight.ts already supports an explicit caller predicate opting a known bare type: "error" payload into replay, but the default retryableZeroOutputTerminal() accepts only response.failed and the allowlisted transport response.incomplete reasons.

A conservative implementation could:

  1. recognize only bare error payloads with structured retryable status/code evidence;
  2. normalize them through failedTerminalResponse() and the existing combo failure classifier;
  3. preserve the existing outputCommitted and bounded-buffer barriers;
  4. leave unstructured or ambiguous bare errors committed.

This should be provider-neutral rather than keyed to Xhub.

Additional context and attachments

Environment: macOS 15.2 arm64, OpenCodex 2.56.0, VS Code 1.137.0, Copilot Chat 0.65.0.

Existing related work:

No matching open or closed issue was found for default combo handling of bare type: "error" SSE events.

Checks

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

Activity

  1. added
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    streamingSSE, WebSocket, terminal stream frames
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 16, 2026
  2. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 73 / 80

    이 이슈는 failover combo가 HTTP 200 + text/event-stream 으로 연 뒤, 출력·reasoning·tool 이전에 bare SSE event: error (예: type:"error" / server_error / upstream_server_error) 만 받으면 다음 타깃으로 넘어가지 않고 그 자식을 커밋해 버리는 provider-compatibility 구멍입니다. 같은 설정의 pre-stream HTTP 503은 나머지 타깃을 도는데, zero-output bare error만 빠집니다. VS Code/Copilot에서는 상세 없는 failed response로 보입니다.

    지금 dev(HEAD d210c46da) 코드가 원인을 정확히 보여 줍니다. src/server/responses/combo-stream-preflight.ts 의 기본 retryableZeroOutputTerminal 은 response.failed 와 특정 response.incomplete reason만 true입니다. bare type === "error" 는 false입니다. 동시에 comboStreamPayloadCommitsOutput 은 알 수 없는 이벤트 타입을 보수적으로 커밋하는데, "error" 는 PRE_OUTPUT_CONTROL / TERMINAL 집합에 없어 커밋됩니다. 주석도 “Only an explicit caller predicate may opt a known bare error into replay. Default … commits unknown/error events.” 라고 적어 두었습니다. #2431/#2449 zero-output response.failed 재생 안전 계약과 같은 정신으로, 구조화된 5xx/429 증거가 있는 bare error만 커밋 전에 실패로 분류하면 됩니다.

    최근 tip(#4807 replay refusal, #4813 fence, #4814 refresh classify)과도 잘 맞습니다. “재시도해도 안전한 zero-output 실패”와 “출력이 시작된 뒤의 fail-closed”를 가리는 문제입니다. types/config 분리와 무관합니다.

    라인 / 경로 수준의 메모입니다.

    src/server/responses/combo-stream-preflight.ts retryableZeroOutputTerminal - bare error를 보지 않음. 여기(또는 호출부 predicate)에 구조화 code/status 기반 5xx·429 허용을 넣는 것이 최소 수정입니다.

    comboStreamPayloadCommitsOutput - type:"error" 기본 커밋. matchedBareError 분기는 retryable일 때만 커밋을 건너뛰므로, predicate가 false면 결국 accepted/commit 경로로 갑니다.

    정책 경계 - 알 수 없는 bare error·요청/정책 오류·출력 시작 후 오류는 재생 금지(이슈 Expected와 동일). 메시지 substring만으로 열면 #4814가 막으려던 false quarantine과 비슷한 함정이 됩니다. structured code/status를 우선하세요.

    재현 - 이슈의 curl combo repro(A: bare error stream, B: 정상 완료)가 충분합니다. 단위 테스트로 preflight에 동일 프레임을 넣으면 CI로 고정하기 쉽습니다.

    관련 - #2431/#2449 계약 확장으로 문서화하면 리뷰 비용이 줄어듭니다. 열린 중복으로 보이는 동일 bare-error combo 이슈는 이 목록에 없습니다.

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

    • 기본 retryableZeroOutputTerminal 을 넓힐지, openai-responses/passthrough 호출부에만 predicate를 줄지
    • 허용 증거를 structured error.code / nested status만으로 할지, 메시지 화이트리스트를 허용할지
    • bare error를 TERMINAL_EVENTS에 넣을지(프로토콜 의미 변경) vs retryable 경로만 확장할지
    • Xhub뿐 아니라 일반 openai-responses failover 전체에 적용할지

    너의 추천
    높은 신호로 받아 주세요. retryableZeroOutputTerminal(또는 명시 predicate)에 zero-output bare error + structured 5xx/429 증거만 true로 두고, 출력 커밋 이후·모호한 error는 fail-closed를 유지하세요. 이슈 repro를 회귀 테스트로 고정. close-don't-rebase 대상 아님. #4807 재생 거절 계약과 충돌하지 않게 “재시도 가능 실패”로만 분류하면 됩니다.

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

  3. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Tracking: draft PR #4817 addresses this.

    Confirmed still present on dev (2b19983bfd): src/server/responses/combo-stream-preflight.ts:33-42 recognizes only response.failed and selected response.incomplete frames, so an unknown bare SSE error falls through to the commit path at lines 50-54.

  4. lidge-jun commented on Sep 17, 2026

    @lidge-jun
    Owner

    Fixed on dev by #4817 (41f1832f6a), released in 2.58.0.

    A zero-output bare SSE error is now judged by the existing combo failure classifier. The frames this report showed — server_error and upstream_server_error — resolve to 5xx through httpStatusFromTerminalError and hop to the next target. Unknown bare errors, client errors, and errors after output began stay fail-closed, which is deliberate: those are not safe to replay.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportsstreamingSSE, WebSocket, terminal stream framestoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions