Repository navigation
[Compatibility] Add an opt-in Responses stream/terminal repair escape hatch for custom providers #1809
Description
Summary
Custom openai-responses providers can hit compatibility failures where the upstream turn completes but Native Codex does not receive a terminal lifecycle it can commit. PR #1367 reproduced this class of failure and explored a per-model bounded-JSON fallback, but that PR cannot land as written and should be treated as evidence/design exploration rather than an implementation to salvage.
The remaining requirement is still valid: custom Responses providers need a narrow, explicit compatibility escape hatch that uses the current Responses repair architecture instead of adding another broad transport/configuration path.
Why this issue exists
PR #1367 reproduced the following boundary on both macOS arm64 and GNU/Linux x86_64:
- the same Responses upstream completed when Native Codex connected directly;
- through OpenCodex, the upstream/internal path reached
response.completed; - Native Codex did not commit the turn and later entered its stream-idle retry path;
- the reproduction did not require the reporter's separate OCI relay;
- minimal calls could succeed, while richer turns reproduced the delivery failure.
That is useful evidence that there is a real Responses compatibility class after upstream completion and before the client commits the terminal event. It does not prove one specific Bun primitive is the sole cause.
The maintainer triage on #1367 also makes the current direction explicit:
The failure class is real and still uncovered. Current terminal repair is a narrower registry-controlled case, and no custom-provider escape hatch exists on current
dev. A redesign around the current registry-only compatibility hint + terminal-repair architecture would be welcome.
PR #1367 itself is conflicting, far behind dev, and has unresolved blockers, so this issue intentionally does not copy its implementation surface.
Current architecture to build on
Current dev already has a registry-only Responses terminal-repair path in:
src/providers/registry.tssrc/server/responses-terminal-repair.tssrc/server/responses/core.ts
That path was designed for the narrow DeepSeek case where output items are structurally complete but a real terminal is missing or indefinitely delayed. It keeps upstream streaming, relays healthy streams unchanged, and can synthesize one terminal only after strict structural checks and a grace period.
This issue should extend that architecture rather than create a second broad Responses validation/reframing subsystem.
Proposed direction
Add a custom-provider, per-model compatibility hint for effective openai-responses routes. The hint should resolve through the same model/wire policy machinery used by registry compatibility policies and should be inactive unless explicitly configured.
The implementation should prefer the smallest repair that matches the observed failure:
- Terminal repair first. Allow an eligible custom Responses model to opt into the existing bounded terminal-repair state machine, with the same fail-closed structural requirements used by registry-controlled repair.
- Preserve healthy streams byte-for-byte where possible. A real upstream terminal remains authoritative and must never be duplicated or replaced.
- Do not silently apply repair to unrelated providers/models. Resolve after the effective per-model wire is known and reject the setting when that wire is not
openai-responses. - Keep forward-auth/canonical OpenAI routes protected. Do not allow a custom compatibility setting to mutate the canonical forward-auth contract unless a separate design explicitly approves that.
- If the feat(responses): add an opt-in bounded JSON fallback for custom providers #1367 reproduction remains broken because the upstream terminal is present but is lost after the relay/inspection split, evaluate a bounded non-streaming fallback as a second, explicit compatibility mode. That fallback should live behind the same narrow compatibility-policy boundary. It should not reintroduce feat(responses): add an opt-in bounded JSON fallback for custom providers #1367's broad shared WebSocket validation changes or make non-streaming the default.
A shape such as the following is illustrative, not prescribed:
{
"providers": {
"<custom-provider>": {
"adapter": "openai-responses",
"modelResponsesCompatibility": {
"<model>": "terminal-repair"
}
}
}
}If more than one repair mode is eventually justified, prefer an explicit enum/policy object over multiple overlapping booleans.
Acceptance criteria
- A custom provider model whose effective wire is
openai-responsescan explicitly opt into the supported repair mode. - The default behavior of unconfigured providers/models is unchanged.
- Registry-controlled repair continues to work unchanged.
- Healthy SSE with a real
response.completed,response.failed, orresponse.incompleteremains authoritative and is not duplicated. - Partial, malformed, tainted, cancelled, aborted, unknown-item, or over-budget output never gets fabricated as successful completion.
- Function calls preserve item ids, call ids, names, arguments, ordering, and continuation usability.
- HTTP/SSE and Responses WebSocket behavior have focused regression coverage where the selected repair mode applies.
- Client cancellation aborts/releases the repair state and any upstream read without retained timers or buffers.
- Case-insensitive per-model configuration cannot contain conflicting duplicate keys.
- The policy is validated against the effective per-model wire, including
modelAdapters/virtual-model resolution where applicable. - Diagnostics, if added, remain content-free and do not log prompts, response text, credentials, provider secrets, or raw payloads.
- Live regression coverage includes the failure shape from feat(responses): add an opt-in bounded JSON fallback for custom providers #1367 on macOS arm64 and Linux x86_64 before the issue is considered fixed.
Design guardrails
Do not use this issue to:
- expose the old feat(responses): add an opt-in bounded JSON fallback for custom providers #1367 implementation wholesale;
- globally tighten validation for every successful Responses JSON/WebSocket snapshot;
- force all custom Responses providers to
stream:false; - add provider/model name heuristics;
- change the default
streamMode; - bypass the Bun/runtime safety gate for eager relay;
- replace the broader protocol-safe one-reader work tracked by [architecture][memory] Make 32 concurrent tool-recall sessions protocol-safe and memory-bounded #820;
- claim terminal repair alone fixes the feat(responses): add an opt-in bounded JSON fallback for custom providers #1367 relay-loss reproduction without reproducing it on current
dev.
The important distinction is that terminal repair and downstream relay loss are related compatibility symptoms, but they are not automatically the same bug. The implementation should prove which repair mode covers the current reproduction instead of assuming it.
Related
- feat(responses): add an opt-in bounded JSON fallback for custom providers #1367 - original draft and reproduction evidence; do not use as the implementation template
- [architecture][memory] Make 32 concurrent tool-recall sessions protocol-safe and memory-bounded #820 - long-term runtime-qualified, protocol-safe one-reader/stream architecture
- #893 is not fixed on macOS: #1025 repairs snapshots but the transport gate from closed #928 is still missing — Codex hangs on third-party Responses gateways #1127 - prior macOS Responses delivery failure with upstream success and missing client SSE
src/server/responses-terminal-repair.ts- current bounded terminal-repair state machinedocs/superpowers/plans/2026-08-06-deepseek-responses-streaming-terminal-repair.md- design rationale for the current registry-only repair path
Activity
coderabbitai commentedon Aug 16, 2026
🔗 Related PRs
#749 - fix(responses): reject expired forward continuations [merged]
#928 - fix(streaming): repair sparse Responses snapshots [open]
#1095 - fix(deepseek): restore progressive Responses streaming [open]
#1361 - fix(responses): restore DeepSeek streaming and routed code-mode tools [merged]
#1788 - fix(responses): fail closed when a routed provider calls an undeclared tool [open]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
trungtaottn commentedon Aug 16, 2026
Observation: on the uncovered custom-Responses class, a clean
[DONE]completion is also mis-accounted as a routing-healthfailureThis is a follow-on in support of #1809. The gap #1809 describes (a custom
openai-responsesprovider that completes output items but ends a turn without a Responses terminal —
response.completed/response.failed/response.incomplete— i.e. closes on a baredata: [DONE]sentinel) affects not just client commit behavior but routing-health accounting.On current
devthat class is also recorded as anincompletefailure even though the streamcontents were a clean success.
Concrete, traced end-to-end against
main@ v2.22.0:src/server/relay.ts:1121-1126—consumeForInspection’sonCleanEoffiresonTerminal("incomplete")whenever the SSE inspector hasn’t reported a terminal ([DONE]is not aterminal —
terminalStatusFromParsed(null)for[DONE]atrelay.ts:280). So a[DONE]-onlystream is accounted
incomplete, nevercompleted.src/server/responses/core.ts:2797-2816— that synthetic terminal callsterminalRecorder("incomplete")options.onNativePassthroughTerminal("incomplete").src/server/index.ts:1220-1224(HTTP namespace) —onNativePassthroughTerminal→addFinalRequestLog(..., { terminalStatus: "incomplete", closeReason: "terminal" }).src/server/request-log.ts:319-332— persisted tousage.jsonlwithterminalStatus: "incomplete"(kept because it is not
"completed").src/routing/history/indexer.ts:176—requests.terminal_status = "incomplete"in the routingindex.
src/routing/health.ts:210—classifySample:terminalStatus === "incomplete"⇒"failure"(and
:304incrementsincompleteStreams, and:212already treats any terminal ≠completedasfailure). The health score is reduced both by the failure count and by the
INCOMPLETE_WEIGHT * (1 - incompleteRate)term (health.ts:392-395).incomplete-stream rate(src/routing/analytics.ts:356;docs-site/.../routing.md:229).Reachable class (and why it’s not a standalone issue)
The step above only runs when the upstream has no terminal repair, and current repair is strictly
registry-controlled:
providerModelResponsesTerminalRepair(src/providers/registry.ts:2635) resolves against theregistry entry (
:2641); a policy exists only fordeepseek-v4-flash/deepseek-v4-pro(
registry.ts:1519).core.ts:2642-2650wraps the upstream body inrelayResponsesSseWithTerminalRepaironly when thatpolicy is non-empty; a custom provider passes its body through unrepaired.
turns also don’t health-penalize:
codexForwardTerminalOutcomeRecorderrecords anincompleteas a200 success (
core.ts:654-667) — though the request-log/requests.terminal_statuswould still readincomplete.So the mis-accounting is reachable only for a custom, unrepaired
openai-responsesprovider thatcloses a structurally-complete turn on a bare
[DONE]— exactly the class this issue says is stilluncovered. I’m not filing it separately; it belongs here as implementation context, because the repair
escape hatch #1809 proposes is the natural place to also normalize the terminal-status accounting.
Ask for #1809
When adding the custom escape hatch, please require the repaired terminal to be recorded as
completed(notincomplete/failure) when the repaired stream is structurally complete, so a healthycustom provider is not penalized in routing health / analytics on what is, from the client’s
perspective, a successful turn. (For contrast, the repair relay already emits the terminal into the
client body —
responses-terminal-repair.ts:272-273— whereas the passthrough relays emit nothing tothe client, only to accounting.)
Not a client-visible bug
To be explicit so nobody mis-files this: no
response.incompleteSSE frame is injected into theclient body on the passthrough / eager (
relay-eager.ts) / default tee paths. The raw[DONE]stillreaches and closes the client stream. The only contamination is the routing-health / request-log /
analytics status. (This corrects the earlier standalone draft’s headline.)
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 48 / 80
커스텀
openai-responses프로바이더에서 업스트림은response.completed까지 갔는데 Native Codex가 터미널 라이프사이클을 커밋하지 못하는 호환 실패를 다룹니다. PR #1367은 재현 증거로만 보고, 그 구현을 그대로 살리지 말라는 이슈입니다. 기본 네이티브 경로가 설치 직후 깨지는 문제는 아니고, 커스텀 Responses를 쓰는 일일 호환 이슈입니다.현재
dev에는 레지스트리 전용 터미널 리페어가 이미 있습니다.src/providers/registry.ts의providerModelResponsesTerminalRepair()는getProviderRegistryEntry(id)의modelResponsesTerminalRepair만 보고, 전송이 레지스트리와 맞을 때만graceMs를 돌려줍니다.src/server/responses/core.ts가 그 정책을 받아src/server/responses-terminal-repair.ts상태머신에 넘깁니다. 커스텀 프로바이더가 설정으로 이 경로에 들어가는 탈출구는 없습니다.원하는 것은 유효 와이어가
openai-responses인 모델만, 명시적으로 기존 리페어에 옵트인하는 좁은 힌트입니다. 건강한 SSE의 실제 터미널은 권위가 있어야 하고, 불완전·오염·취소된 출력을 성공으로 만들면 안 됩니다. #1367의 광범위한 WebSocket 검증이나 기본stream:false는 이 이슈 범위가 아닙니다. 터미널 리페어와 릴레이 손실은 증상이 비슷해도 같은 버그라고 단정하면 안 됩니다.해결방안: effective per-model wire를 확인한 뒤, 예시처럼
modelResponsesCompatibility같은 설정을 레지스트리 정책과 같은 해석기에 연결하면 됩니다. 기본값은 그대로 두고, 정방향 인증/캐논 OpenAI 경로는 보호하며, 구조 검사와 grace를 통과할 때만 터미널 하나를 합성하면 됩니다. #1367 재현이 릴레이 손실이면 두 번째 모드를 같은 정책 경계 뒤에만 두고, 재현 없이 패치하지 않는 편이 맞습니다.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 48 / 80 · 유지
커스텀
openai-responses는 업스트림이response.completed까지 갔는데 Codex가 턴을 못 닫고 idle 재시도로 감. DeepSeek용 수리기(src/server/responses-terminal-repair.ts)는 레지스트리만. 커스텀 옵트인이 없음. #1367은 증거일 뿐, 살리지 말 것.유지함. 기본은 그대로. 설정한 모델만 기존 수리 상태기계에 태움. 건강한 스트림은 그대로. canonical OpenAI 포워드는 손대지 말 것.
해결방안:
modelResponsesCompatibility: { "<model>": "terminal-repair" }. 실단말이 있으면 합성 금지. #1367 재현을 현재dev에서 다시 보기.이 댓글은 grok-bot이 작성했습니다
fix(providers): normalize configured model keys in terminal repair lo…
fix(providers): fail closed on invalid explicit grace values in compa…
feat(providers): wire terminal repair config validation and safeConfi…
feat(providers): add Responses terminal repair escape hatch for custo…
fix(providers): normalize configured model keys in terminal repair lo…
fix(providers): fail closed on invalid explicit grace values in compa…
feat(providers): wire terminal repair config validation and safeConfi…
fix(providers): harden custom terminal repair resolver against forwar…
feat(providers): add Responses terminal repair escape hatch for custo…
fix(providers): normalize configured model keys in terminal repair lo…
38 remaining items
fix(providers): harden custom terminal repair resolver against forwar…
feat(providers): add Responses terminal repair escape hatch for custo…
fix(providers): normalize configured model keys in terminal repair lo…
fix(providers): fail closed on invalid explicit grace values in compa…
feat(providers): wire terminal repair config validation and safeConfi…
fix(providers): harden custom terminal repair resolver against forwar…
test(providers): add WebSocket and real terminal pass-through transpo…
test(providers): assert real terminal passthrough payload (lidge-jun#…
feat(providers): add Responses terminal repair escape hatch for custo…
fix(providers): normalize configured model keys in terminal repair lo…
fix(providers): fail closed on invalid explicit grace values in compa…
feat(providers): wire terminal repair config validation and safeConfi…
fix(providers): harden custom terminal repair resolver against forwar…
test(providers): add WebSocket and real terminal pass-through transpo…
test(providers): assert real terminal passthrough payload (lidge-jun#…