Skip to content

17 min read

Stop reasons and the four endings

On this page

A completion is the text a model produces for one request: a decode loop sampling one token at a time until something ends it. That loop runs inside the provider's serving layer, the infrastructure that drives it, counts tokens as they come out, watches for whatever conditions you asked it to watch for, and assembles the JSON response you receive. This topic is about what the serving layer does when the ending is not the model's own, and about the fact that nothing in the returned text distinguishes those cases from a finished answer.

This chapter establishes the vocabulary the rest of it uses. There are four distinct ways a generation can end, they are caused by four different actors, and the response carries a short string naming which one fired. That string is the stop reason, and a caller that never reads it has no other source for the information — not the status code, not the length of the text, not the text itself.

Why the ending is reported beside the text rather than inside it

Generated text keeps no record of its own extent. A file has a size and a database row has a schema, so a truncated one can be recognised as truncated by examining it. A model's answer is a run of characters that stops, and the model never declared in advance how many characters it intended to produce, so there is nothing inside the text to check the text against. The provider therefore reports the ending out of band, in a field beside the content.

The reason the text cannot carry that record is visible in how it is produced. Each step of the decode loop takes the tokens generated so far, computes a probability for every token that could come next, samples one, and appends it to the sequence. That is the entire operation, repeated. No step in it fixes a total length, because no step holds any representation of a total — at the fifth token the loop has no more idea whether the answer will run to twenty tokens or to two thousand than it had at the first. So a completion is a stream that stopped, and "did this stream reach its end" is not a question the stream can answer about itself. It can only be answered by whatever was watching the loop. The thing watching the loop is the serving layer, which is why the answer arrives as a field the serving layer wrote rather than as a property of the text.

You meet that field on every response from every major provider: as an attribute on the message object your SDK hands back, as a field on the final event of a streaming response — the delivery mode where output arrives token by token as it is produced rather than in one block at the end — and as a line in your request logs if you record it. You also meet the two request-time parameters that produce two of the endings it reports. max_tokens is a hard ceiling on how many tokens a single response may contain. Stop sequences are strings you nominate in advance, and generation ends the moment the model produces one of them. Both are ordinary fields you set at request time, both are decisions you make rather than the model, and both leave a distinct fingerprint in the stop reason.

The four endings, and who decided each one

The model emits an end-of-sequence token. This is the model's own signal that the turn is complete: during training it learned where answers end, and at inference it assigns high probability to that special token when it judges the response finished. The decode loop sees it, stops, and does not include it in the text. This is the only ending in which the content you receive is the content the model intended to produce in full.

A stop sequence you supplied matches. Before the request, you told the API to cut generation whenever a particular string appears in the output — "</answer>", "\n\nHuman:", "---". The model generated normally, produced text that happened to contain your string, and the serving layer terminated the loop at that point. The model was not consulted about whether it was done, and in most cases it was not.

The token cap is reached. You set a maximum number of output tokens, the serving layer counted down as tokens were sampled, and when the count hit zero it stopped the loop. This can happen at any position — mid-sentence, mid-word, between the opening brace of a JSON object and its first key.

The provider terminates the request for its own reasons. Safety classifiers may decline a request or cut it off partway through; a long-running turn that uses server-side tools may be paused at an iteration limit and handed back to you to resume; a streaming connection may drop and leave you holding a partial response with no terminal event. None of these are your configuration and none of them are the model finishing.

The first of those four is the only one the model takes part in, and it is worth seeing how it can take part at all. A language model performs one operation per step: given the tokens so far, it produces a probability for every token in its vocabulary as the next one. Nothing in an operation of that shape can express stop unless stopping is itself one of the tokens being scored. So the vocabulary contains one that means exactly that — the end-of-sequence token, a reserved entry with no printable text behind it, appended to the end of every finished document the model was trained on. Having met it at the close of an enormous number of completed answers, the model learns which contexts it belongs in, and assigns it a high probability when the answer in front of it looks finished. It then comes out of the sampler like any other token. What makes it special is one branch in the decode loop: on seeing that particular id, stop, and do not append it to the text. The model "deciding it is done" is therefore an ordinary next-token prediction that happened to land on one particular entry in the vocabulary, and it is the only ending that carries any information about the answer, because it is the only one produced by the thing that wrote the answer.

The other three endings are decided by machinery the model cannot see. The token cap is a counter in the serving layer. A stop sequence is a string comparison run over decoded output. A provider termination happens outside the decode loop altogether. None of the three is written into the prompt, so none of them changes a single probability the model computes. That has a consequence worth stating on its own, because a great deal of misplaced confidence rests on the opposite assumption: a response that is going to be cut off at the cap contains exactly the text the model would have produced with no cap at all, up to the point of the cut. The model does not shorten its answer to fit the budget, does not wrap up early as it approaches the ceiling, and does not know afterwards that it was interrupted. The truncation is applied to output planned as though it would run to completion, which is why the cut lands at an arbitrary position rather than at a sensible one.

EndingWho decidedAnthropic stop_reasonOpenAI finish_reasonContent is complete?
End-of-sequence tokenThe modelend_turnstopYes
Stop sequence matchedYou, at request timestop_sequencestopOnly if your boundary is the real end
Token cap reachedThe serving layermax_tokenslengthNo — cut at an arbitrary position
Provider terminatedThe providerrefusal, pause_turncontent_filterNo, or resumable

Two things in that table are worth pausing on. The first is that OpenAI's finish_reason collapses the model's own ending and your stop-sequence match into the same value, stop, so a caller on that API cannot distinguish the two from the finish reason alone and must check whether the text ends at one of their configured boundaries. The second is that a normal tool call also ends generation — Anthropic reports tool_use, OpenAI reports tool_calls — and that is neither a failure nor a completion. It is a turn handing control back to you so you can run something and continue the conversation.

The last column needs its word defined before it can be used, because "complete" is doing narrow work there. It does not mean correct, and it does not mean long enough to be useful. It means that the text you are holding is the text the model would have produced had nothing interfered — that the loop ran until the model itself signalled the end. Read that way, the middle row is genuinely conditional rather than evasive. Register a closing tag as the boundary, and if the model reaches it the content is complete in the only sense available, since the model's judgement about where the answer ends and yours coincide at that string. Register a blank line as the boundary, ask for an answer that runs to several paragraphs, and generation ends at the first paragraph break while the model is still in the middle of the response you asked for. The field reads stop_sequence in both cases and the two situations could not be further apart. What separates them is knowledge about the boundary you chose, and that knowledge lives in your code rather than in the API.

What the wire actually shows you

Here are two responses to the same prompt from the same model. The only difference between the requests was max_tokens.

json
{
  "id": "msg_01QfXk3mRt9WvZ2pLd8yHnAb",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-5",
  "content": [
    {
      "type": "text",
      "text": "Net 30 from the invoice date, with a 2% discount available if paid within 10 days."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": { "input_tokens": 1204, "output_tokens": 23 }
}
json
{
  "id": "msg_01BhY7nKpQ4sTx6cVm2rWjEu",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-5",
  "content": [
    {
      "type": "text",
      "text": "Net 30 from the invoice date, with a 2% discount"
    }
  ],
  "stop_reason": "max_tokens",
  "stop_sequence": null,
  "usage": { "input_tokens": 1204, "output_tokens": 16 }
}

Both are HTTP 200. Both have a populated content array with a text block containing a grammatical English clause. Both have the same id shape, the same role, the same model. A client that reads response.content[0].text and writes it to a column cannot tell them apart, because at the level of shape they are identical, and at the level of language the second one is a perfectly well-formed noun phrase. The single byte of difference that matters is the value of stop_reason, and nothing forces you to look at it.

The belief that keeps this class of bug alive for months at a time is that a truncated response would announce itself somehow — with a non-2xx status, or an exception out of the SDK, or a flag on the object with a name like truncated that a reasonable client would trip over on the way to the text. That expectation is not naive, because every other kind of truncation an engineer meets does announce itself. A short read from a socket returns a byte count the caller compares against the count it asked for. An interrupted download fails against the Content-Length the server declared up front. A corrupted archive fails its checksum. A half-written row violates a constraint. What those four have in common is a declaration made before the bytes arrived, against which the bytes can be checked afterwards. A completion has no such declaration. The only quantity the request ever committed to was the ceiling, and a response cut off at the ceiling satisfies it precisely; it is the response that stops early that departs from the number you sent, and stopping early is the good case. There is nothing for a generic client to catch, which is why the checking has to be written by hand and why it is so reliably not written at all.

The stop_sequence field is the companion signal. It is null on every response except one that ended because your configured sequence matched, in which case it holds the specific string that fired. If you supplied three stop sequences, this is how you learn which of the three did the cutting.

A third field is worth reading alongside them. When usage.output_tokens is exactly equal to the max_tokens you requested, the cap is almost certainly what ended the run, and the two signals agreeing gives you a useful cross-check while you are still learning to trust the first one. In the second response above, sixteen output tokens against a cap of sixteen is not a coincidence.

Splitting the response into a payload and metadata about how the payload was produced is a deliberate design choice, and it explains a lot of the confusion downstream. None of these four endings is a transport failure — the request was well formed, the server processed it, and a response was produced — so none of them warrants a non-2xx status. Returning 200 with a reason field is the honest encoding. It is also, for most client code, an invisible one, because the overwhelming majority of API-consuming code in the world is written to check the status and then read the body, and there is no third habit for reading the body's account of itself.

A stop reason is not an error, and not a quality signal

It is tempting to read the field as a status and treat anything other than end_turn as a fault. That mapping is wrong in both directions, and both errors cause real bugs.

In one direction, tool_use and pause_turn are ordinary control flow. A model that stops because it wants to call your lookup_invoice function has not failed at anything; it has produced a well-formed request for you to service. A turn that pauses because a server-side tool loop reached its iteration limit is telling you to send the conversation back so the work can continue where it left off. Code that treats every non-end_turn value as an exception will break agentic flows that were working correctly.

In the other direction, end_turn is not a claim about correctness. The model deciding it is finished says nothing about whether the answer is right, whether it invented the discount percentage, or whether it answered the question you actually asked. Stop reasons are about the mechanics of termination. They tell you whether the token stream you received is the token stream the model was in the middle of producing, and that is all.

There is also a fifth condition that is easy to confuse with the token cap and is genuinely different: running out of context window rather than out of output budget. Anthropic reports this separately as model_context_window_exceeded, which means the conversation itself grew past what the model can hold, not that your requested output ceiling was hit. The remedies differ — one wants a larger cap or a shorter answer, the other wants compaction or a trimmed history — so collapsing them into a single "too long" branch will send you to the wrong fix.

The two are worth separating at the level of what is being counted. A model has a fixed context window: the number of token positions it can attend over in one pass, fixed when the model was built, and shared by everything in the request. The prompt occupies positions in that window, and each token the model generates takes the next free one, so prompt and output draw on a single allowance rather than on two. The output cap you set is a second and smaller budget, and it constrains only the generated half. Those are different quantities, and a request can be comfortably inside one while failing the other. A conversation that has grown over many turns can fill the window with history alone, and at that point the cap you configured is beside the point, because there are no free positions for it to be spent in. Raising the cap there makes the situation worse rather than better, since a larger requested output needs more of the same room that has already run out. That is the practical reason for keeping the two apart in your error handling, and it is why the provider spends a distinct reason string on the distinction rather than folding it into the one you already handle.

Which of the four gets mistaken for success

Ask a working engineer to name the ways a completion can end and the common answer is two: it finishes, or it hits max_tokens. That answer drops exactly the two endings whose output is hardest to distinguish from a normal, finished response.

A stop-sequence match usually produces text that ends cleanly at a boundary you chose, because you chose the boundary to be a clean place. Set "\n\n" as a stop sequence and every truncated response ends at the end of a paragraph, with terminal punctuation, reading like a complete short answer. Nothing about the characters coming back suggests that the model was three paragraphs from done.

Provider-side termination is worse, because the response can arrive with an empty or partial content array and an HTTP status that says everything went fine. A safety classifier that declines before any output produces a 200 with content: [], and a client that reaches for content[0].text gets an index error at best or, if it guards with a default, silently writes an empty string. A dropped streaming connection gives you whatever tokens made it across the wire with no terminal event at all — the accumulated text just stops, and if your stream handler does not distinguish the stream ending from the message completing, you record the fragment as the answer.

That last case deserves one more turn, because it is the one where client code that looks entirely correct produces the fault. In a streaming response the stop reason does not travel with the text. It arrives at the end, on the final event, after every delta that carried content — it has to, because the serving layer does not know why generation stopped until generation has stopped. A client accumulates the deltas into a string and returns that string once the iteration finishes. Consider what the iteration finishing can mean to that client. Either the server sent its terminal event and closed the stream, or the connection went away underneath it. Both of those surface identically, as a loop that has no more items. Unless the handler records that it saw the terminal event and treats its absence as a failure, nothing distinguishes them from inside, and the fragment is returned as though it were the answer. The stop reason cannot rescue you here, because the stop reason is precisely the thing that never arrived.

A token-cap truncation is the loud one by comparison, because it tends to cut mid-word or mid-structure often enough that somebody eventually notices. The two people forget are the two that leave you holding something that reads like a finished answer.

If you have to work out which ending you are looking at from a logged response and nothing else, there is a short sequence that gets you there. Read stop_reason first, because on Anthropic's API it distinguishes all four directly. If the field says stop_sequence, read stop_sequence to learn which of your configured strings fired. If it says max_tokens, compare usage.output_tokens against the cap you sent to confirm. If the content array is empty and the reason is refusal, the provider declined before producing anything, and the accompanying detail names the category. And if you are looking at accumulated output from a stream with no recorded reason at all, the connection ended without a terminal event, which is the fourth category wearing its least obvious costume.

Four endings, one field, and only one of the four is the model deciding it has said what it meant to say. The value of holding the taxonomy in mind is that it converts a vague worry about truncation into four specific questions with four specific answers, each of which the provider has already computed for you and attached to the response. Everything else in this topic is a consequence of that field existing and of client code not reading it.

A token cap fired where nobody expected it is the ordinary version of the problem, and the chapters that follow work through progressively less ordinary ones. They start with the ending that is most surprising the first time you meet it, because it changes the bytes you receive rather than merely stopping them: what happens to the text that matched your stop sequence.

Stop sequences, max tokens, truncation, silent output cutoff — read the current version at https://daviddominguez.dev/learn/en/stop-sequences-max-tokens-truncation-silent-output-cutoff/stop-reasons-and-the-four-endings

Back to top