Codex was reading a Responses stream, or trying to open one, and it ended before the response.completed event arrived: the connection closed, went silent, broke at the network level, or the server reported a failure. Codex retries that on its own, five times by default. The reason printed after the colon tells you which of those happened, and that decides where to look.
The error
# Codex CLI, once its retries are spent
■ stream disconnected before completion: stream closed before response.completed
# While it retries (VS Code extension, an early-2026 build)
Reconnecting... 1/5
stream disconnected before completion: error sending request for url (https://…/responses)
# The reason after the colon varies, and it is the diagnosis:
# stream closed before response.completed
# idle timeout waiting for SSE
# error sending request (Codex before 0.156 adds: for url (…))
# An error occurred while processing your request. You can retry your request, …
# Incomplete response returned, reason: max_output_tokensCauses and fixes at a glance
| Cause | Fix |
|---|---|
| A VPN, proxy, firewall or TLS-inspecting middlebox closed the connection | Retry from another network; if the error stops, exempt the API host from that proxy or inspection. |
| The server failed mid-response — the reason is the server's own message | Nothing to change locally: let Codex retry, check the provider's status, try again later. |
| Nothing arrived for stream_idle_timeout_ms (300,000 ms by default) | A stalled upstream or a buffering proxy; raise the timeout only for silences that are legitimate. |
| A custom provider that ends streams without response.completed | Run curl -N against it: every successful response has to end with that event. |
| The response was ended on purpose — "Incomplete response returned" | A token cap, a content filter or another stop the reason names. A retry usually repeats it, so change the request. |
Read the reason after the colon
Codex uses this one error for a stream that ends early without a more specific diagnosis, and appends the reason. In its source, a stream that simply ends gives "stream closed before response.completed"; one that stays silent for longer than stream_idle_timeout_ms gives "idle timeout waiting for SSE" ("idle timeout waiting for websocket" on the WebSocket transport the built-in OpenAI provider uses); a request that breaks on the wire keeps the HTTP client's wording, "error sending request" (builds before 0.156 add the URL); and a generic response.failed event puts the server's message after the colon. Since Codex 0.148, a connection that cannot be opened at all — DNS, TLS, a refused port — is reported as a separate error, "Connection failed", which current builds keep retrying while they wait for the network.
stream closed before response.completed the connection ended with no terminal event:
network path, server, or a provider that
never sends response.completed
idle timeout waiting for SSE no event for stream_idle_timeout_ms
error sending request the request broke on the wire: a reset, a
proxy or a middlebox (before 0.148, also
a connection that never opened)
…error decoding response body the body broke mid-read: network or middlebox
An error occurred while processing… the server's own response.failed message
Incomplete response returned, reason: … the server stopped it: a token cap, a content
filter, or whatever the reason namesKnow what Codex already retries, and tune it per provider
Two retry budgets apply. request_max_retries (default 4) covers the HTTP request before any stream exists; Codex retries 5xx responses and transport errors there, but not 429s. stream_max_retries (default 5) covers everything on this page: each retry re-sends the turn, which is what "Reconnecting... 1/5" counts, and when the budget runs out the error stays on screen and the turn stops. Both are capped at 100, and both live in a [model_providers.<id>] block. The built-in openai provider ID is reserved and cannot be redefined, so these are knobs for custom providers.
model = "gpt-5-6-sol"
model_provider = "kunavo"
[model_providers.kunavo]
name = "Kunavo"
base_url = "https://api.kunavo.com/v1"
env_key = "KUNAVO_API_KEY"
# wire_api defaults to "responses", the only supported value
stream_max_retries = 10 # dropped or failed streams (default 5, max 100)
request_max_retries = 4 # 5xx and network errors before streaming (default 4, max 100)
stream_idle_timeout_ms = 300000 # silence before giving up (default 300000)Reproduce the stream without Codex
A Responses stream ends with one terminal event, and Codex needs it to be response.completed. Send the same kind of request with curl from the same machine. If it completes every time while Codex keeps disconnecting, look at the Codex build and its open issues; if curl breaks too, repeat it from another network before blaming the provider. Note when failures happen: a drop at the same elapsed time on every run points to a timer somewhere on the path, not to a flaky network.
curl -sN https://api.kunavo.com/v1/responses \
-H "Authorization: Bearer $KUNAVO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5-6-sol","stream":true,
"input":"Write a 600-word story about a lighthouse keeper."}' \
| grep -oE '"type": ?"response\.(completed|failed|incomplete)"'
# a healthy run prints exactly one line: "type":"response.completed"Take the network path out, then check the provider
Test from a second network before concluding anything. In OpenAI's forum, one user's errors on a work machine stopped the moment they disabled the company's Zscaler; in a GPT-5.6 thread, one user's cleared up on a VPN or a phone hotspot, while another's persisted after switching networks. If another network fixes it, exempt the API host from the proxy or TLS inspection rather than raising timeouts. For a custom provider, codex doctor reports whether the config loaded and whether the provider's key variable is present; the provider has to serve the Responses API, since wire_api has no other value, and supports_websockets should stay unset unless it runs the Responses WebSocket transport. If curl completes cleanly and Codex still fails, add your reproduction to openai/codex issues #41340 or #41989, which report this error against custom Responses providers that stream correctly outside Codex and were open on September 23, 2026.
If you’re calling through Kunavo
Through Kunavo, a GPT model's /v1/responses stream is the upstream's own event stream relayed frame by frame: only the model id is rewritten, and Kunavo adds no keepalive events. Until the first output event arrives, for at most 30 seconds, the stream is held back, so an upstream that errors, times out or hangs up at that stage can still be retried on the model's next channel where one is configured. If none serves it, Codex gets an HTTP error instead of a stream — a 502 when the upstream errored or never answered, with the cause in the JSON code, such as upstream_524 (an upstream 4xx keeps its own status) — which Codex prints as unexpected status 502 Bad Gateway: Upstream provider error, not as this message, and retries as well. After the first output event nothing can be retried without duplicating output: an upstream response.failed passes through as sent, so Codex handles it exactly as it would from OpenAI directly (a generic failure prints its message after the colon), and an upstream connection that drops mid-answer ends Kunavo's stream with no terminal event, which Codex reports as stream closed before response.completed. Either way, Codex's own retries take over. Nothing on Kunavo's side caps a stream that keeps flowing — the 240-second limit covers only the wait for upstream headers — but silence ends one: Kunavo stops reading an upstream that sends nothing for 300 seconds, the same as Codex's idle default, and the edge closes a connection that carries no bytes for 600 seconds. Each of those failures is recorded at zero cost. The provider block tuned above is the one set up on the Codex CLI integration page.
FAQ
What does “stream closed before response.completed” mean?
The HTTP response started and the connection then ended without the response.completed event Codex waits for. Something closed it early: a proxy or firewall on the way, the server, or a custom endpoint that never sends that event.
Does Codex retry “stream disconnected before completion” automatically?
Yes. Codex re-sends the turn up to stream_max_retries times (5 by default, 100 at most) and shows Reconnecting... 1/5 while it does. When the budget runs out, the error stays on screen and the turn stops; sending another message starts a new request.
Should I increase stream_idle_timeout_ms?
Only when the reason is "idle timeout waiting for SSE" and the silence is legitimate. It does nothing for "stream closed before response.completed", where the connection ended instead of going quiet. Through Kunavo, values above 300000 buy nothing once the stream has started: Kunavo stops reading an upstream that sends nothing for 300 seconds and ends your stream.
Why does Codex say “Incomplete response returned, reason: max_output_tokens”?
The server stopped the response at its token cap and said so with a response.incomplete event. Codex reports that under the same error and retries it, and a retry of the same turn usually runs into the same cap, so split the task or use a model with a larger output cap. Through Kunavo, a Claude model's max-token stop reaches Codex the same way.
Am I charged for the retries?
Each retry is a new request. Through Kunavo, a GPT call that fails upstream — an error before streaming, a response.failed, a dropped connection — is recorded at zero cost. An attempt that Codex gave up on while the upstream kept working is billed for what the upstream reports producing, because that work was done.
Related guides
- Codex CLI 401 error: fix the right authentication route
- LLM streaming errors — SSE cutoffs, hanging streams and missing usage
- Codex CLI API key — the working custom-provider setup
More error semantics live in the error reference; getting a key takes a minute via sign up and the authentication docs.