In one line: when you ask a model to produce tens of thousands of characters in a single call (episode outlines, long fiction, large translations, big code), stream the response, do not use non-streaming; set the client read timeout to the gap between data events (tens of seconds — 90–120s is a safe value), not to the total generation time; give
max_tokens room; and check stop_reason before using the text. Do these four things and long-output calls stop “returning nothing.” Beyond 20k–30k characters, add one more: split the work into several calls instead of holding one connection open for 20 minutes./v1/messages; differences for the OpenAI-compatible format are called out separately.
Three things to know first
- For a 10k-word output, 10–20 minutes of real generation is normal. The model emits tens of thousands of characters token by token, plus a reasoning/thinking phase — end-to-end latency is genuinely long. This is not the gateway being slow; generation itself is slow.
-
Non-streaming buffers the whole thing before sending. With non-streaming (
streamomitted orfalse), the server must wait for the model to finish the entire generation, then send the whole body back at once. During those minutes your client read timeout is racing against it, and the longer the generation the more likely you disconnect before the result arrives — and the exception is often empty (httpx.ReadError’sstr(e)is blank), so you cannot see the cause. - A dropped connection is still billed, so blind retries double-charge. Once the server has generated the output, the call is billed even if the result never reached you. Retrying after you have already received part of the body means the model runs again and you pay again.
Stream, do not use non-streaming
With streaming (stream: true), the first byte arrives within seconds, and after that a data event arrives every few tens of seconds. Your read timeout only has to cover the gap between events, not a generation that runs for many minutes — that is the whole reason streaming reliably delivers long output.
The two protocols have different terminators — do not mix them up:
With adaptive thinking on, Claude native emits a
type: "thinking" block first (its increments are thinking_delta), then the text block. When rendering, route thinking_delta and text_delta separately and do not concatenate thinking into the body text.
Minimal Claude native /v1/messages streaming example (plain httpx, line-by-line SSE parsing):
Size the read timeout to the inter-event gap, not the total time
Many people set the read timeout to one huge value meant to cover the whole generation (say 1800 seconds) and still time out — because with non-streaming that value has to race the entire generation and any hiccup breaks it. The right approach is streaming plus a read timeout sized to the inter-event gap. Measured reference (claude-opus-5 producing a ~20k-character episode outline from a ~15k-character input):
So a read timeout of 90–120 seconds covers the largest inter-event gap with headroom — no need for a value of many minutes. A three-part timeout splits the phases and sizes each one:
Setting it in each client
Many client libraries have both a per-read timeout and a total timeout, often with similar names. For long output you want the former; leave the latter unset or unlimited:Streams can stall too: the connection stops receiving mid-way
Streaming fixes the “buffer everything before sending” problem, but there is another failure mode to guard against: a connection that stalls mid-stream.- Symptom: text arrives normally at first, then nothing more comes — yet the connection neither errors nor closes. The client hangs until its own timeout: if that is one hour, it waits a full hour, and ends up with only the first small piece.
- Cause: on cross-region and cross-border network paths, a connection held open for a long time can get stuck at an intermediate network device. The server finished generating and closed the connection long ago, but the close never reaches you. The longer a connection lasts, the more likely this is — a 20-minute connection is hit more often than a 3-minute one.
- Billing: the server finishes generating and bills as usual (see point 3 of “Three things to know first”).
- Size the read timeout to the inter-event gap (previous section). With a 120 s read timeout a stall costs you two minutes before you can retry; with a one-hour read timeout it costs a full hour. This is the most important one.
- Shorten each connection: split very long outputs into several calls — see “Split very long outputs into several calls” below.
- Resume a dropped stream from where it stopped — see “Retry strategy” below.
Give max_tokens room, and check stop_reason
Long output easily hits themax_tokens ceiling and gets truncated. This is especially true for models like Claude with thinking on — thinking itself consumes the max_tokens budget, and a long piece can exhaust it.
- Start
max_tokensat 64000 (give even more at high effort / deep thinking;claude-opus-5supports 128K output). - Check
stop_reasonbefore using the response:end_turn— finished normally, the text is complete; this is the only success.max_tokens— truncated, the text may be incomplete or even empty. This is truncation, not an “empty result” — raisemax_tokensand retry.refusal— declined by a safety policy; handle separately.
str(e) alone or by “the text is empty” is misleading — an empty body is usually max_tokens truncation.
Split very long outputs into several calls
Asking for 50k characters in one call that runs for 20 minutes is the most fragile pattern, even with streaming: any network hiccup along the way voids the whole piece, and the whole piece is billed. Once the output exceeds 20k–30k characters, split it:- Generate the outline / episode list / chapter list first — a short call.
- Then generate one chapter or episode per call, keeping each under 10k–20k characters. Include the outline and the context each part needs (character settings, how the previous part ended) so the pieces stay consistent.
- Once the outline is fixed, the parts are independent and can run in parallel.
Retry strategy
Be conservative with retries on long output, so “retry on failure” does not become “double billing plus a second long run”:- Retry only on failures before the response headers arrive, and on
5xx/429(with backoff, at most twice). Those are connection or transient issues where a retry makes sense. - If a stream drops after you already received part of the body, resume from where it stopped instead of starting over. The server has generated and billed it; restarting runs it again and pays again. Keep the text you received, include it in a new call as what has been written so far, and let the model continue — you pay only for the remainder.
- Treat a read timeout (a stalled connection) the same way: if you already have a good amount of text, resume; if you received almost nothing, retry the whole call.
- Log the request id from the response headers for reconciliation and troubleshooting.
Scenario cheat sheet
For a single piece over 30k characters, split it by chapter or episode (see “Split very long outputs into several calls”). Always stream. Use
api.apiyi.com (or the backup b.apiyi.com), and not api-cf.apiyi.com (the CDN node returns 524 at ~100 seconds and cannot carry long requests).
Related
How to avoid API timeouts
Timeout values by scenario
Streaming vs non-streaming
The tradeoffs and how to choose
Claude thinking & effort
Adaptive thinking, effort tiers, max_tokens and truncation
Claude response handling
Native response shape, SSE events, stop_reason