Skip to content

finishReason Values in the Gemini API and What Each One Means

9 min read · updated August 11, 2026

Gemini returns HTTP 200 for nearly everything, including responses with no text in them. finishReason is the field that says what actually happened, and reading it is the difference between an application that handles a block and one that shows an empty box.

Where the field lives

It is a property of a candidate, not of the response: candidates[0].finishReason. In a streamed response it appears only on the final chunk. There are two distinct facts to keep apart, and the API keeps them in two fields:

  • promptFeedback.blockReason — the prompt was rejected. There are no candidates at all. Values include SAFETY, OTHER, BLOCKLIST, PROHIBITED_CONTENT and IMAGE_SAFETY.
  • candidates[i].finishReason — generation started and then ended, for the reason named. There is a candidate, though it may have no usable content.

Code that reaches for candidates[0] without checking length throws on the first case, which is why a prompt-level block so often shows up in logs as an index error rather than as a safety event.

The two ordinary values

  • STOP — the model finished. It emitted its end-of-turn token, or it hit one of your stopSequences. Note that both produce STOP: there is no separate value telling you a stop sequence fired, so if you need to know, check whether the text ends where a stop sequence would have cut it. STOP is also what you get when the model produced a function call and nothing else, which is why tool detection means inspecting parts rather than switching on this field.
  • MAX_TOKENS — the output allowance ran out. The text is a valid prefix of an answer that was going to be longer. On thinking models this can arrive with no visible text at all, because reasoning tokens consumed the allowance first. See the output ceilings.

The block values

These share a shape: generation was stopped by a filter, the candidate has little or no content, and you are billed for what was produced.

  • SAFETY — the generated content tripped a configured harm category. The candidate’s safetyRatings array names which, with a blocked: true on the offending entry. This is the one that responds to safetySettings; the others below generally do not.
  • RECITATION — the output reproduced training data too closely. It fires on long verbatim passages: song lyrics, licence texts, well-known code listings, and—most confusingly—on faithful quotation of a document you supplied yourself. No safety setting relaxes it. The practical workaround is to ask for a summary, a paraphrase, or short quoted spans rather than long reproduction.
  • PROHIBITED_CONTENT — content blocked by a non-configurable policy filter, separate from the adjustable categories.
  • BLOCKLIST — the output contained a term from a blocked-terms list.
  • SPII — sensitive personally identifiable information was detected in the output. Worth knowing about if you are asking a model to transform documents containing personal data.
  • IMAGE_SAFETY — generated image content violated safety policy. Only relevant on image-generating models.
  • LANGUAGE — generation used an unsupported language.
  • OTHER — the catch-all. Unknown reason. Retry once; if it persists, something about the request is reliably triggering it and the useful debugging move is bisecting the prompt.

A response blocked mid-generation looks like this, and note that it has usageMetadata—you paid for the tokens produced before the stop:

{
  "candidates": [
    {
      "finishReason": "RECITATION",
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 318,
    "candidatesTokenCount": 87,
    "totalTokenCount": 405
  }
}

The tool-calling values

These arrived with function calling and are the ones most often missing from handling code written against an older client library:

  • MALFORMED_FUNCTION_CALL — the model attempted a function call the API could not parse into a valid functionCall part. In practice this correlates with complex or deeply nested parameter schemas, and with declaring many tools at once. Simplifying the schema of the offending function is a more reliable fix than retrying.
  • UNEXPECTED_TOOL_CALL — a tool call was produced when no tool was available for it.
  • TOO_MANY_TOOL_CALLS — the model exceeded a limit on consecutive tool calls, which is a loop guard. Seeing this repeatedly means an agent is cycling rather than converging, and the fix is in the loop, not the request.
  • FINISH_REASON_UNSPECIFIED — the proto default. You should not see it on a completed response; on a streamed chunk that is not the last one, the field is simply absent rather than set to this.
This enum grows. Values for tool-call limits and image safety were added after the API launched, and more will be. Any exhaustive switch over finishReason needs a default branch that fails visibly rather than falling through as success—a new value silently treated as “fine” is the failure mode this list is meant to prevent.

It is per candidate, not per response

The [0] that everyone writes hides a fact worth knowing: candidates is an array, and every entry carries its own finishReason and its own index. Set generationConfig.candidateCount above one and you can get a response where the first candidate was blocked and the second was not.

{
  "candidates": [
    { "index": 0, "finishReason": "SAFETY", "safetyRatings": [ ... ] },
    { "index": 1, "finishReason": "STOP", "content": { "role": "model", "parts": [{ "text": "..." }] } }
  ]
}

Two implications. If you request multiple candidates, iterate and pick the first usable one rather than assuming index zero is the good one — and note that a mixed response like the above is not an error condition, it is exactly what a probabilistic filter over several independent generations produces. Support for candidateCount varies by model, so check before designing around it.

The billing consequence is the one that surprises people: a blocked candidate was generated before it was blocked, and usageMetadata.candidatesTokenCount covers the tokens produced across all candidates including the discarded ones. Asking for four candidates and using one costs four candidates.

There is also an ordering detail worth internalising for streamed responses: because finishReason arrives only on the final chunk, a stream gives you no advance warning that a block is coming. Every chunk up to the block looks like an ordinary chunk. This is the mechanical reason that “stream directly to the user” and “never show text that was subsequently blocked” cannot both be true, and the streaming page works through what to do about it.

Handling them without a giant switch

Fifteen values do not need fifteen branches. They need three, because only three things can be done about them:

  1. Complete. STOP, with non-empty content. Use the answer.
  2. Truncated but usable. MAX_TOKENS. You have a valid prefix. Either continue by appending the partial text as a model turn and asking for the rest, or raise the ceiling. Do not parse it as JSON.
  3. Refused. Every block value, plus STOP with empty content. There is nothing to salvage. Retrying the identical request will produce the identical outcome for the deterministic filters—BLOCKLIST, SPII—and may not for the probabilistic ones, so at most one retry, and then surface it.

The case worth calling out separately is STOP with empty content, because it belongs in the third bucket despite carrying the success value. It happens, and code that branches on the finish reason alone treats it as a successful empty answer. Check content length as well as the reason.

One more distinction, since it is the source of most wasted debugging time: a model that declines in prose—“I can’t help with that”—finishes with STOP and normal safety ratings. It was not blocked. It refused. Only the response body distinguishes them, and only if you look at the field rather than the text.