Repository navigation
Anthropic content_filter terminal is reported as 502 and retried by Codex #4312
Description
Client or integration
Codex App
Area
Streaming
Summary
An Anthropic filtered/refused stream is logged as HTTP 502 with Upstream incomplete: content_filter. Codex treats the response.incomplete terminal as a disconnected stream and retries the same sampling request up to five times. The provider decision should remain explicit, but the failure should not look like a transient transport outage or invite automatic repeated refusal requests.
Reproduction
- Stream an Anthropic-compatible response whose stop_reason is refusal/content_filter through /v1/responses.
- OpenCodex normalizes the stop reason to content_filter and emits response.incomplete.
- The usage record reports terminalStatus=incomplete, terminalSource=upstream, transportPhase=terminal_sse, status=502, and upstreamError=Upstream incomplete: content_filter.
- Codex logs
stream disconnected before completion: Incomplete response returned, reason: content_filter, followed by retries 1/5 through 5/5.
Observed in two separate local conversations. No provider raw payload is retained for attachment, so this report does not assert whether the original vendor field was refusal or content_filter. The adapter and bridge mapping explain both forms. Preserve partial output and the actual refusal; do not replace it with false success, remove safety controls, or silently reroute to evade the decision.
Version
OpenCodex 2.51.0; Codex App executable 0.153.4.
Operating system
Windows 11 x64
Provider and model
anthropic / claude-opus-5, adapter=anthropic
Logs or error output
status=502 terminalStatus=incomplete terminalSource=upstream transportPhase=terminal_sse
Upstream incomplete: content_filter
stream disconnected - retrying sampling request (5/5 ...)
sampling_error=stream disconnected before completion: Incomplete response returned, reason: content_filter
Screenshots and supporting files
Source pointers: src/server/request-log.ts incompleteReasonLabel; src/adapters/anthropic.ts stop-reason mapping; src/bridge.ts incomplete terminal handling. A fixture-driven integration regression should assert one explicit non-retryable refusal terminal while preserving partial output and tool-call integrity. No billable live refusal replay is needed.
Redacted configuration
{"provider":"anthropic","model":"claude-opus-5","stream":true}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
Ingwannu commentedon Sep 12, 2026
The server-side path is present in dev 7a0513c. The Anthropic adapter normalizes refusal/content_filter, the bridge emits response.incomplete with reason content_filter, and request-log.ts falls through to the generic incomplete status 502 (with the label shown in the report). This is not evidence of a network outage.
One important distinction: for a stream whose HTTP headers have already been sent, that 502 is terminal usage/log accounting, not necessarily a new HTTP 502 received by Codex. Your client log points to the response.incomplete event as the retry trigger. Changing only the log status would not fix that client behavior.
The fix needs a deterministic terminal-contract regression, not a billable replay: preserve the refusal and partial output, avoid treating it as retryable transport failure, and keep incomplete tool arguments and compaction/history from being promoted to valid completed output. Check streaming and non-streaming paths separately, plus max-output/EOF controls. The original provider payload was not retained, so I am not treating its exact stop_reason or the five client retries as independently reproduced.
@lidge-jun please keep this open as a protocol-compatibility bug. I have not changed the wire terminal or marked this fixed; choosing a non-retryable representation needs client-contract evidence, not a synthetic successful completion.
luvs01 commentedon Sep 12, 2026
Author follow-up: reproduced the client behavior with the actual Codex 0.153.4 executable against a loopback-only deterministic fixture, using isolated CODEX_HOME and ephemeral sessions. No provider credentials, live model calls, private prompts, or billable requests were used.
All cases completed without the 40-second timeout. Both filtered-stream variants produced the five Reconnecting messages. The successful/streamed cases contained the same harmless partial output, Hello. Thus changing the accounting status or simply renaming response.incomplete to response.failed is insufficient for this client. The 400 control is only applicable before headers are committed; it is not a proposed replacement for an already-streaming response.
This independently establishes the client half of the original report. It does not establish the original vendor stop field, an adapter end-to-end refusal fix, incomplete tool-argument safety, compaction correctness, or the non-streaming projection. Those remain required before calling a fix complete. I have left the issue open and have not changed production wire semantics.
Reproducer (Bun; set CODEX_BIN to the executable to test). Run normally for completed/incomplete/failed; set PROBE_CONTROLS=1 for the 400/max-output/EOF controls. It writes only a local synthetic fixture directory beside the script.
Deterministic client fixture
Additional author validation on be08fe0: a 12-case bridge matrix covered streaming and buffered JSON independently (text, unfinished tool arguments, and compaction; refusal/content_filter/max_tokens/EOF controls). All cases emitted exactly one incomplete terminal with the expected reason, and no truncated compaction item was installed.
A separate current defect is now reproduced: after tool_call_start + an unfinished tool_call_delta + done(stopReason=refusal/content_filter/max_tokens), both projections label the function_call item completed even though its arguments are unfinished and the response is incomplete. The EOF control correctly leaves the tool incomplete. This is output-shape evidence, not evidence of actual tool execution. It reinforces the requirement not to synthesize successful completion as the retry fix.
Minimal buffered reproduction (use the same events with bridgeToResponsesSSE for streaming parity):
No production protocol was changed and no real tool was executed. This issue remains open; the new evidence should be included in the terminal-contract regression, not hidden by accounting-only changes.
lidge-jun commentedon Sep 12, 2026
리뷰 · 우선순위 67 / 80
설명
이 이슈는 Anthropic 쪽에서 refusal/content_filter로 끝난 스트림을 OpenCodex가
response.incomplete+incomplete_details.reason=content_filter로 잘 정규화한 뒤에도, 요청 로그·표면 HTTP 의미가 502처럼 보이고 Codex App이 stream disconnected로 받아 같은 샘플링을 최대 5번까지 다시 치는 문제다. 제공자 결정은 이미 난 상태인데 일시 전송 장애처럼 보이니 거절을 여러 번 반복한다. 부분 출력과 실제 거절 이유는 남겨야 하고, 우회 라우팅·안전장치 제거·가짜 성공으로 바꾸면 안 된다. 보고 버전은 OpenCodex 2.51.0 + Codex App 0.153.4, provider=anthropic / claude-opus-5다.지금
devHEAD7a0513c2f기준으로 경로는 이렇게 이어진다.src/adapters/anthropic.ts는 pending stop_reasonrefusal/content_filter를 adaptercontent_filter로 모은다.src/bridge.ts는 truncation/stop을response.incomplete로 내고 reason을content_filter(또는truncationReasonFor결과)로 붙인다.src/server/request-log.ts의incompleteReasonLabel은 content_filter를 특별 취급하지 않아 기본Upstream incomplete: content_filter라벨이 되고,httpStatusForRequestLogTerminal은 incomplete 중max_output_tokens만 200으로 두며(terminalIncompleteReason기준) 나머지는httpStatusForTerminalStatus→ 502로 떨어진다.captureTerminalHttpStatus는 content_filter를 ordinary incomplete reason 목록에 넣어 두지만, 그 경우 incomplete에 별도terminalHttpStatus를 심지 않는다(429/402 quota 분기만 예외). 그래서 usage/ledger에는 terminalStatus=incomplete인데 status=502·transportPhase=terminal_sse가 같이 찍힌다. Codex 쪽 재시도는 클라이언트 동작이지만, 우리 쪽이 거절을 전송 단절로 보이게 만드는 한 재시도 유도는 계속된다.#4292풀 열차와는 겹치지 않는 스트리밍/터미널 분류 버그다. cyber_policy는 이미 400·non-retryable 쪽으로 정리된 전례가 있어, content_filter도 명시적 정책/거절 터미널로 같은 축에 두는 설계가 자연스럽다. types.ts/config.ts 분리와 무관하다. 이슈에 적힌 픽스처로 incomplete content_filter / failed content_filter / completed를 나눠 Codex CLI 재시도 횟수를 재는 방향은 타당하다. 라이브 billable refusal 재현은 필요 없다.라인 - 이게 무슨 문제다
src/server/request-log.tshttpStatusForRequestLogTerminal- incomplete + content_filter가 502로 남는다.max_output_tokens만 200 예외라 거절이 업스트림 장애처럼 집계·표시된다.incompleteReasonLabel- content_filter 전용 문구가 없어 기본 Upstream incomplete 라벨이 된다. 로그 독자가 전송 실패로 읽기 쉽다.src/bridge.tsincomplete 방출부 - content_filter 경로에retryable: false(또는 동등 신호)를 안 심는다. cyber_policy failed 쪽은 retryable false를 넣는 패턴이 이미 있다.src/adapters/anthropic.tsstop_reason 매핑 - refusal과 content_filter를 하나로 모으는 것은 맞고, 문제는 그 다음 HTTP/재시도 의미 부여다. 매핑 자체를 되돌릴 필요는 없다.메인테이너의 판단이 필요한 지점
max_output_tokens처럼 200+incomplete로 둘지retryable: false만으로 충분한지, 클라이언트 버전별로 추가 신호가 필요한지너의 추천
content_filter(및 Anthropic refusal 정규화분) incomplete를 non-retryable 명시적 거절로 분류하고, 로그 status를 502에서 빼는 좁은 패치를 받는 걸 추천한다. bridge incomplete에
retryable: false를 맞추고, request-log에max_output_tokens와 대칭인 분기를 추가한 뒤 이슈에 적힌 픽스처 회귀를 붙이면 된다. 안전장치 우회·가짜 completed 변환은 받지 말 것. help wanted로 두되 재현·포인터가 충분하니 needs-design까지는 필요 없다.이 댓글은 grok-bot이 작성했습니다
luvs01 commentedon Sep 12, 2026
Superseded by the follow-up after #4341 merged (comment 5644607500). The current #4381 has been reduced to failing an in-flight web search at a truncated terminal. It does not remove the incomplete
response.output_item.doneframe now pinned by the upstream tests.The earlier claim that this PR withholds all executable completion frames no longer describes its scope. Client handling of incomplete items still needs direct version-specific verification. The original retry/accounting issue remains open.
luvs01 commentedon Sep 12, 2026
Author follow-up after #4341 merged: the tool-finalization half of this issue is now upstream, and I reduced my own follow-up to the one case it did not cover. Two things remain open here, and the second is a decision rather than a defect report.
Carried forward in #4381. On the
done+ truncated path an in-flight provider web search was still finalized ascompleted, while the error and explicit-incomplete terminals already finalize it asfailed. It never returned results, so the client showed a finished search for a truncated turn. Reproduced onec065aa0cbefore the change and covered for all four truncated stop reasons.Open contract question, deliberately not changed.
failCurrentToolCallstill emitsresponse.output_item.donefor the call that was open, andtests/adapters/bridge-nonstreaming-terminal.test.tsnow pins that frame to exist withstatus: "incomplete". Withholding the argument and input completion frames prevents an issued call only if the client also reads the item status when it builds the call from the item frame. If it does not, a truncated turn still hands over an executable call whose arguments are half-written JSON, which is the shape the surrounding comment insrc/bridge.tsdescribes as the failure being avoided.I am not asserting that as fact here. I previously inspected a Codex 0.153.4 build and read it that way, but I cannot re-derive that from evidence I can cite in this thread, and it contradicts a contract you just merged, so changing it unilaterally in a PR would be the wrong move. What would settle it is a single observation of whether a client issues a call from an item frame carrying
status: "incomplete".If it does issue the call, the fix is to retain the partial item in the terminal snapshot through
retainFinishedItemwithout emitting the item frame, and to invert those two assertions. I have that change working locally with regression coverage for function, custom and tool-search calls and can open it as a separate PR whenever you want it. If the client honors the status, the current contract is already correct and this can be closed out.The original repeated-request behavior in this issue is unaffected either way: Codex maps
response.incompleteto a stream error on its own, so no terminal-shape change here stops the retry loop.fix(bridge): fail an in-flight search at a truncated terminal [skip ci]
lidge-jun commentedon Sep 13, 2026
Status after the 60-plus contributor backlog pass, which landed 12 lanes on dev. This issue is not closed by it, and the reason is specific rather than a blanket deferral.
#4356 gave a clean empty forced search answer one further attempt and #4341 keeps open function, custom and tool-search calls incomplete when an adapter ends with a filtered or truncated stop reason. #4356 states directly that it does not resolve this issue's client retry contract.
What remains is the reporting shape you described: an Anthropic content_filter terminal surfacing as a 502 that Codex then retries. Classifying that terminal so the client stops retrying is a separate change from preserving the truncated terminal internally.
fix(responses): terminal refusals, caller-mismatched reasoning blobs,…
lidge-jun commentedon Sep 14, 2026
Fixed on
devby 44027ae.src/adapters/anthropic.tsnow emits a non-retryable incomplete result with acontent_filterreason on all three terminal paths, so a content-filter stop is no longer reported as a 502 that Codex will retry. The retry loop is closed at the adapter and bridge contract; the HTTP request-log mapping itself is unchanged.Hosted CI was green at the exact merged head.
fix(bridge): fail an in-flight search at a truncated terminal [skip ci]
fix(responses): terminal refusals, caller-mismatched reasoning blobs,…