Repository navigation
OpenRouter Ox Alpha returns HTTP 200 but Codex App receives an empty completion without retry #2423
Description
Client or integration
Codex App
Provider or upstream service
OpenRouter
OpenCodex version
2.31.0 (also reproduced after a controlled rollback to 2.29.0)
Endpoint or capability
POST /v1/responses, streaming, through the openai-chat adapter
Current behaviour
When Codex App sends a turn to openrouter/stealth-ox-alpha, OpenCodex routes it correctly and records HTTP 200, but Codex receives no assistant text, no tool call, no visible error, and no usage. The turn silently ends after roughly 4–8 seconds.
The request log records only one upstream send (sendCount: 1) and no recovery (recoveryKinds: []). Therefore the empty-completion guard introduced in #1655 does not retry this response shape and does not surface empty_completion_retry_failed.
The same symptom occurs with OpenCodex 2.29.0 and 2.31.0.
A direct request to the same OpenRouter model, with the same reasoning effort, succeeds outside OpenCodex:
- non-streaming: HTTP 200, visible text and reasoning, usage returned;
- streaming: HTTP 200, content deltas, reasoning deltas, usage and
[DONE]returned.
This indicates that authentication, model availability, and the OpenRouter key are working.
Expected behaviour
OpenCodex should forward the model's visible output to Codex App. If the adapter actually produces a completed turn containing neither text nor a tool call, the guard from #1655 should retry the identical turn once and then either return content or surface empty_completion_retry_failed.
Minimal redacted request or reproduction
# Configure an OpenRouter provider with a valid key (not shown), then expose:
# provider: openrouter
# model: stealth/ox-alpha
# contextWindow: 1048576
# input modalities: text, image
# reasoning efforts: low, high, max
# default effort: max
# emptyCompletionRetry: true
ocx start --port 10100
# In Codex App, select openrouter/stealth-ox-alpha and send a simple prompt,
# for example: "Reply with exactly: test ok"
# Observed: the Codex turn ends without any assistant response.Actual response or error
requestId: ocx-mt529pwy-j
provider: openrouter
model: stealth/ox-alpha
adapter: openai-chat
status: 200
durationMs: 7406
usageStatus: unreported
requestedEffort: max
effectiveEffort: max
reasoningWireField: reasoning_effort
reasoningWireValue: max
attempts[0].status: 200
attempts[0].durationMs: 4616
attempts[0].sendCount: 1
attempts[0].recoveryKinds: []
routeKind: explicit-providerCodex App displays no assistant message and no error.
Upstream documentation
https://openrouter.ai/stealth/ox-alpha
OpenRouter uses the OpenAI-compatible chat completions API:
https://openrouter.ai/docs/api-reference/chat-completion
Suggested mapping or implementation notes
This looks like a response-shape coverage gap in the empty-completion guard from #1655. Possible areas to inspect:
- Whether the OpenRouter/Ox Alpha stream ends in a shape that the
openai-chatadapter treats as a clean terminal/EOF butguardEmptyCompletionEventStreamdoes not classify as an empty completed turn. - Whether an Ox Alpha delta is classified as content by the guard but later discarded before reaching Codex App.
- The truncated-source branch: if the source ends without the exact terminal event expected by the guard, it may release held events and return without performing the configured retry.
The request log is especially relevant: a genuine empty-completion recovery should show a second send and an empty-completion recovery kind, but neither is present.
Additional context and attachments
Related work/issues:
- feat(responses): guard empty completions with one identical-turn retry #1655 — empty-completion guard and identical-turn retry
- fix(bridge,adapters): a turn that did not finish is not completed #1995 — unfinished turns must not be marked completed
- fix(openai-chat): heartbeat while buffering tool-call deltas #2180 — OpenAI-chat stream/heartbeat handling
- Cursor/Grok 4.6 via OpenCodex can terminate with upstream_stall_timeout after a silent stream #2210 — silent upstream stream symptom on another provider
Checks performed:
- Existing provider and compatibility issues were searched; no issue specific to Ox Alpha was found.
- The request and response details above are redacted and contain no credentials.
- Direct OpenRouter tests confirmed that the upstream model returns visible output and usage.
Activity
lidge-jun commentedon Aug 23, 2026
리뷰 · 우선순위 58 / 80
설명: 이 이슈는 오픈라우터 옥스 알파로 말을 보내면 서버는 200 을 기록하는데, 코덱스 앱에는 글도 도구도 에러도 안 보인다고 한다. 지금 CURRENT
devHEAD 는4f41a8e93이다. 이번 시간에 origin/dev 는 그대로다. 새 머지는 없다. 착지는 여전히 2396 사용량 CLI 오늘 비용이다. package.json 은 2.27.0 이다. src/config.ts 는 3975줄이다. src/runtime 폴더는 지금 HEAD 에 없다. 작성자는 메인 2.31.0 과 2.29.0 에서 재현했다고 적었다. 지금 origin/dev 에도 같은 오픈에이아이 채팅 어댑터와 빈 완료 가드가 있다.작성자 로그는 제공자 오픈라우터, 모델 stealth/ox-alpha, 어댑터 openai-chat, 상태 200, 사용량 없음, 보냄 횟수 1, 복구 종류 빈 목록이다. 빈 완료 가드가 한 번 더 보냈다면 보냄 횟수가 2 가 되고, 그래도 비면 empty_completion_retry_failed 가 보여야 한다. 둘 다 없다. 같은 키로 오픈라우터에 직접 보내면 글과 생각과 사용량이 나온다. 인증과 모델 자체는 산다. src/providers/registry.ts 1457줄 오픈라우터 칸은 어댑터가 openai-chat 이고 1466줄 모델 목록에 stealth/ox-alpha 가 있다. 컨텍스트는 1048576, 입력은 글과 그림이다. 카탈로그 팁의 x-preview-f-free 는 젠 무료 슬러그다. 이 이슈의 stealth/ox-alpha 와 같은 모델의 다른 이름이다.
지금 HEAD 의 src/adapters/openai-chat.ts 305줄 reasoningTextFrom 은 생각 칸이 글자일 때만 읽는다. 1666줄은 내용이 글자이고 길이가 있을 때만 텍스트로 보낸다. 오픈라우터가 생각을 객체로 주거나 내용을 조각 배열로 주면, 둘 다 조용히 버린다. 1614줄은 끝난 표식을 보면 done 을 낸다. 1853줄은 끝난 표식도 사용자 글도 없으면 에러를 낸다. 이 이슈는 에러도 없다고 하니, 어댑터는 done 을 낸 쪽으로 보는 편이 맞다. src/server/responses/empty-completion-guard.ts 72줄 isContentEvent 는 빈 글자를 내용으로 안 본다. 206줄은 내용 없이 done 이면 같은 요청을 한 번 더 보낸다. 269줄은 끝난 사건이 없이 소스만 끝나면 들고 있던 것을 풀어 주고 재시도하지 않는다. 작성자가 가리킨 세 갈래와 같다.
가드는 설정이 켜져 있어야 한다. src/config.ts 1264줄과 3390줄 기본값은 꺼짐이다. 작성자 재현은 켜 두었다고 적었다. 켜져 있는데도 보냄 횟수가 1 이면, 가드가 내용을 보았거나, 끝난 사건이 없어서 재시도를 건너뛴 것이다. src/server/responses/core.ts 4285줄은 콤보와 압축이 아니면 가드를 씌운다. 관련 1655 가 이 가드를 넣었다. 1995 는 안 끝난 턴을 완료로 표시하지 말라는 것이고, 2180 은 하트비트, 2210 은 다른 제공자의 조용한 스트림이다. 2210 은 아직 연다. 이 이슈만으로 2210 을 닫지 않는다. 고치는 PR 은 아직 없다. 사용자 길이로는 앱이 아무 말 없이 끝나는 구멍이라서 58. 카탈로그 팁은 Ox Alpha x-preview-f-free + deepseek-v4-flash-vision-exp. Cursor 정적 카탈로그는 opus-4-8-fast / opus-5-fast. 2334 CursorCredentialRouter 는 여전히 src/providers/cursor-pool.ts 모듈+테스트만 있고 어댑터에 연결되지 않았다. 2332 H2 는 discovery 전용. 2320 overflow + 2342 는 이미 dev. 2188 사이드카는 이미 dev. 2382 데스크톱 앱 재시작은 이미 dev. 2292 는 아직 연다.
src/adapters/openai-chat.ts 라인 305 - reasoningTextFrom 은 생각 칸이 글자일 때만 읽는다. 객체면 버린다
src/adapters/openai-chat.ts 라인 1666 - 내용은 글자이고 길이가 있을 때만 텍스트로 보낸다. 조각 배열은 버린다
src/adapters/openai-chat.ts 라인 1614 - 끝난 표식을 보면 done 을 낸다. 에러 없이 끝나면 가드가 재시도해야 한다
src/server/responses/empty-completion-guard.ts 라인 269 - 끝난 사건이 없이 소스만 끝나면 들고 있던 것을 풀고 재시도하지 않는다
src/config.ts 라인 3390 - 빈 완료 재시도 기본값은 꺼짐이다. 작성자는 켜 두었다고 적었다
src/providers/registry.ts 라인 1457 - 오픈라우터는 openai-chat 이고 stealth/ox-alpha 가 목록에 있다
이슈 2210 - 다른 제공자의 조용한 스트림. 이 이슈만으로 닫지 않는다
메인테이너의 판단이 필요한 지점
너의 추천
이슈는 열어 둔다. 고치는 PR 이 오면 그 PR 로 닫는다. 2210 과 2279 는 닫지 않는다. 호출 길을 넓히지 말 것. 먼저 오픈라우터 한 프레임이 글자인지 객체인지 잠근다. 라벨은 그대로 둔다. 프리뷰 배포가 아니다.
이 댓글은 grok-bot이 작성했습니다
optimikelabs commentedon Aug 23, 2026
I captured the upstream frame shapes for one minimal proxy request and one actual Codex App turn. No payload text, credentials, headers, or reasoning content were logged.
Control request through the same OpenCodex proxy
response.completed, usage returnedFailing Codex App turn
latestAssistantMessage: null, no tool marker, no errorThe OpenRouter stream was not an array-content or terminal-less EOF case. The instrumented
openai-chatadapter observed:The two visible content fragments reconstruct the exact expected 19-character reply. Therefore OpenRouter returns visible text, the
openai-chatadapter sees string content and a normalstop, and usage is present. Yet Codex App records no assistant item.This also explains
sendCount: 1andrecoveryKinds: []: the empty-completion guard correctly seestext_deltacontent, so it does not retry. The loss appears to happen after adapter decoding / empty-completion classification, while converting or emitting the Responses stream consumed by Codex App.The request-size/tool difference may be relevant: the small no-tool request succeeds through the same proxy, while the full Codex turn fails despite valid upstream output.
Temporary instrumentation was removed and OpenCodex was restarted after the capture.
Ingwannu commentedon Aug 23, 2026
Thanks — this capture materially changes the diagnosis. It rules out the two first hypotheses: the OpenAI Chat adapter did decode two non-empty
text_deltafragments, and the empty-completion guard correctly committed the first attempt after seeing them. I will not change the retry guard or broaden retries based on this evidence.The remaining boundary is after adapter decoding, but there are two distinct runtime paths there. A full Codex tool catalog normally contains hosted
web_search; when a sidecar credential is available, currentdevroutes that request throughrunWithWebSearch, while the 182-byte no-tool control bypasses that loop. Both eventually usebridgeToResponsesSSE, so we need to distinguish the loop from the bridge/client contract before changing code.Please add one more redacted capture with metadata only:
webSearchSidecar.enabled,webSearchSidecar.backend, andwebSearchSidecar.streamRoutedModelOutputvalues (no credentials);[web-search-loop]line for the failing turn;response.output_item.doneonly the itemtype, optionalphase, and output-text length;response.completed, onlyresponse.idlength,output.length, each output itemtype/optionalphase, and message text lengths.Do not include text, reasoning content, headers, tokens, or tool arguments. With those fields we can write the correct regression: either
runWithWebSearchis dropping/replaying the wrong boundary for a tool-bearing no-search turn, or the bridge emits a message that Codex App does not accept. Until that boundary is known, changing the empty-completion guard would add paid duplicate calls without fixing the loss.jkir4n commentedon Aug 23, 2026
Reproduced same root cause via
https://opencode.ai/zen/go/v1ox-alpha-free(Hermes Agent0.20.5) — not just OpenRouter, and not specific tomcp__sequential_thinking.Proof (2026-08-23 19:12 IST, direct curl bypassing Hermes):
0 tools→ streamsreasoning_contentcorrectly on all UAs1 dummy tool{"name":"t","properties":{"x":{"type":"string"}}}→finish_reason: network_errorempty on all UAs:HermesAgent/0.20.5/OpenAI/Python 1.99.0/curldefault → not a Hermes User-Agent block1tool onmuse-spark-1.2-contributor→ streams correctly, so schema validonly sequential_thinking,5,20,53tools → samenetwork_errorHTTP 503: Error from provider (Console Go): Upstream request failed: Endpoint is unavailable.Hermes logs: was working
02:55-06:11 ISTwith32+53tools (390 turns), then11:52:43firstEmpty response (no content or reasoning) — retry 1/3→3/3→exhausted, 100% fail since.Filed upstream with full curl + UA tests: anomalyco/opencode#44382
This is the
anomalyco/opencode#36889Go outage family — thetools:adapter forstealth/ox-alphabroke at ~11:52 IST. Withouttoolsit still streams.lidge-jun commentedon Aug 26, 2026
Closing as already implemented on
dev.A terminal-less pre-output EOF now raises a retryable empty-completion error at empty-completion-guard.ts:309, pinned by
tests/empty-completion-guard.test.ts:326. That is precisely the HTTP 200 with no completion and no retry described here: the stream ends without a terminal event, and the guard converts it into a retry instead of surfacing an empty turn.Verified against dev at
37909c670. Reopen with a request id if an Ox Alpha turn still ends empty without retrying — the remaining possibility is a terminal event arriving that the guard treats as legitimate.