Empty or Whitespace-Only Completions
8 min read · updated August 4, 2026
An empty or whitespace-only completion is never unexplained. The response carries a finish_reason (or the provider’s equivalent stop-reason field), and its value identifies the cause precisely. Read it before you read anything else, because the six causes have nothing in common except the symptom.
finish_reason is the diagnosis
The values are standard across OpenAI-shaped APIs and have close analogues elsewhere — other providers use names like end_turn, max_tokens and tool_use for the same states.
| finish_reason | Description |
|---|---|
| tool_calls | The model called a tool. Empty content is correct and expected; the payload is in tool_calls. By far the commonest cause of a reported empty response. |
| length | Generation hit max_tokens. Empty content here means the budget was consumed by something other than visible output, or was too small to produce any. |
| content_filter | A safety classifier stopped generation. Content may be partial or absent. |
| stop | The model emitted an end-of-turn token, or one of your stop sequences matched. Empty content with this value is the case that confuses people, and it has three sub-causes. |
| null or absent | In a streaming chunk, this is normal for every chunk except the last. If it is null on a complete non-streaming response, the request was cut off in transit. |
Log this field on every request permanently. It costs one column and it converts an entire class of vague bug reports into a query.
Empty with tool_calls
Nothing is wrong. The assistant turn is the tool call; content is null by design and reading it as the answer produces the empty string. Your loop should branch on finish_reason rather than on the truthiness of the content, execute the call, append the result as a tool message, and call again. If that loop is not there yet, the tool-calling failure page covers the parsing shape in more detail, and how tool calling works covers the round trip.
Empty with length
Three ways to get here, in order of frequency.
max_tokensis very small. A value of 1 to 16 with a model that opens with a preamble produces truncation before any substantive text. Check the effective value, including whatever default your framework applies when you set nothing.- A reasoning model spent the budget thinking. On reasoning models the internal tokens are drawn from the same output allowance, and they are usually not returned to you. A low limit with a high reasoning effort gives you a complete, expensive response with no visible content. The tell is in the usage object: a non-zero reasoning-token count with zero or near-zero visible output. Raise the limit substantially or lower the effort — reasoning tokens explains the accounting.
- Structured output with a large schema. Constrained decoding can spend many tokens on required scaffolding before emitting anything you would call content, and a truncated JSON object often parses to nothing useful. Budget output for the whole object, not for the values in it.
Empty with stop, which is the confusing one
The model chose to stop immediately. Three sub-causes, and they are distinguishable.
- A stop sequence matched at position zero. If
stopincludes"\n"and the model begins with a newline, generation ends before the first character of content. Any short or common stop sequence — a newline, a colon, a quotation mark,"User:"in a chat-formatted prompt — can do this. Test: remove the stop sequences and re-run. If content appears, you have found it. Then choose a sequence that cannot occur at the start. - The prompt already ended the turn. With raw completion endpoints, or with a hand-built chat template, a prompt that already contains the assistant’s closing token asks the model to continue after the end — and it correctly does nothing. This is the standard failure when someone assembles a template by string concatenation. Test: print the fully rendered prompt string, including special tokens, and look at the last twenty characters.
- The prompt genuinely calls for no output. A classification prompt whose instructions permit an empty answer, a request to output nothing when a condition is unmet, or a conversation where the last message is already an assistant turn. Read the assembled messages in order; this is more common than it sounds in agent loops that append messages programmatically.
Empty with content_filter
A classifier stopped it. The important distinction is between input filtering, which rejects the request before generation with an HTTP 400, and output filtering, which returns 200 with this finish_reason and whatever text was produced before the filter fired. Neither is the model refusing — a refusal is a normal completion with finish_reason of stop and text explaining the refusal. Content filters on innocent input separates all three.
Empty only when streaming
If the non-streaming call returns text and the streaming call returns nothing, the model is fine and the accumulator is wrong. The usual faults:
- Ignoring null deltas incorrectly. The first chunk typically carries only
delta.rolewith no content, and the final chunk carries onlyfinish_reason. Code that stops on the first empty delta stops immediately. - Reading
messageinstead ofdelta. In a streaming chunk the field isdelta;messageis absent or empty, so the concatenation of many empties is an empty. - Discarding the tool-call deltas. Same as the tool-calls case, but harder to see because there is no single object to print.
- Stopping at the
[DONE]sentinel too eagerly. The terminator is a literal line, not JSON; attempting to parse it raises, and a bareexceptaround the loop turns that into a silent empty result.
Whitespace-only, which is its own case
Content that is present but contains only spaces, newlines or a zero-width character is a narrower diagnosis than a genuinely empty string, and it points at the prompt rather than at the plumbing.
- The prompt ends mid-structure. A prompt finishing with a colon, an opening brace or a bullet marker invites the model to continue a layout, and the first thing that layout needs is a newline. If a stop sequence then matches, you are left with the whitespace and nothing else.
- Structured output where the schema is already satisfied. A schema whose properties are all optional permits an empty object, and a model with nothing to say will emit one. Make at least one field required, or add an explicit “no result” variant to the schema so that “nothing” is a value rather than an absence.
- Leading whitespace stripped downstream. If your code trims and then checks truthiness, a response consisting of a newline plus a short answer that was itself truncated becomes empty after trimming. Log the raw string with
repr()before trimming; the two cases look identical afterwards. - An invisible character. A zero-width space or a byte-order mark makes a string non-empty and visually blank.
repr()shows it immediately, and broken characters in output covers where they come from.
raw = choice.message.content print(repr(raw)) # '\n\n' and '' are different bugs print(len(raw or ""), len((raw or "").strip()))
Handling it in production
Whatever the cause, an empty completion should not reach a user as an empty screen. Branch explicitly:
fr = choice.finish_reason
text = (choice.message.content or "").strip()
if fr == "tool_calls":
return handle_tools(choice.message)
if fr == "length" and not text:
log.warning("output budget exhausted with no content", extra=usage)
return retry_with(max_tokens=max_tokens * 3, max_attempts=1)
if fr == "content_filter":
return user_message("That request could not be completed.")
if not text:
log.error("empty completion", extra={"finish_reason": fr, **usage})
return retry_once_then_fail()The retry deserves a caveat: retrying an empty stop at temperature 0 gets you the same empty answer and a doubled bill, so either raise the temperature slightly for the retry or do not retry at all. Blind retries on this error are a real contributor to the overnight bill in bill triage.