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.”/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:
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.
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. - Do not blindly retry a stream that dropped after you already received part of the body. The server has generated and billed it; a retry runs it again and pays again.
- Log the request id from the response headers for reconciliation and troubleshooting.
Scenario cheat sheet
Always stream. Use
api.apiyi.com (recommended in mainland China) or vip.apiyi.com (recommended overseas), 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