Response.StopReason tells you why generation ended. Getting this wrong is how
a truncated answer gets presented as a complete one, so it is worth branching on
rather than ignoring.
You will learn
- What each stop reason means, and which need handling
- Which provider values map onto each
- Why
StopStopSequenceonly ever comes from Anthropic - How to handle truncation and refusal properly
The values#
| Constant | Value | Meaning | Provider values |
|---|---|---|---|
StopEndTurn | end_turn | The model finished naturally. | end_turn, stop, STOP |
StopMaxTokens | max_tokens | The output hit Request.MaxTokens. The response is truncated — treat it as incomplete. | max_tokens, length, MAX_TOKENS, model_context_window_exceeded |
StopToolUse | tool_use | The model wants a tool run. Execute the calls and send the results back. | tool_use, tool_calls, function_call |
StopStopSequence | stop_sequence | A sequence from Request.Stop was produced. Only ever comes from Anthropic — OpenAI reports a stop-sequence hit as plain "stop", so it arrives as StopEndTurn. | stop_sequence (Anthropic only) |
StopRefusal | refusal | The model or its safety classifiers declined. Content may be empty or partial; do not retry the same request. | refusal, content_filter, SAFETY, RECITATION, BLOCKLIST, PROHIBITED_CONTENT, SPII |
StopUnknown | unknown | The provider reported something skyl does not model. Read Response.Raw. | anything else, including pause_turn, OTHER, MALFORMED_FUNCTION_CALL |
Adapters map provider-specific values onto these and fall back to StopUnknown
rather than inventing a new one — so a value you have never seen is always
readable in Response.Raw.
The two that need handling#
StopMaxTokens means the response is truncated. The text you have is a
prefix of what the model was going to say.
if resp.StopReason == skyl.StopMaxTokens {
// Do not present this as an answer. Either raise MaxTokens and retry,
// or tell the caller it was cut short.
return fmt.Errorf("truncated at %d tokens", req.MaxTokens)
}if resp.StopReason == skyl.StopMaxTokens {
// Do not present this as an answer. Either raise MaxTokens and retry,
// or tell the caller it was cut short.
return fmt.Errorf("truncated at %d tokens", req.MaxTokens)
}StopToolUse means the model wants a tool run and is waiting for you. It is
not an error, and Text() will often be empty — see
The Tool Loop.
Refusals#
StopRefusal means the model or its safety classifiers declined.
if resp.StopReason == skyl.StopRefusal {
// Content may be empty or partial. Do NOT retry the same request —
// the same prompt gets the same answer.
}if resp.StopReason == skyl.StopRefusal {
// Content may be empty or partial. Do NOT retry the same request —
// the same prompt gets the same answer.
}resp, err := client.Complete(ctx, req)
switch {
case errors.Is(err, skyl.ErrRefusal):
// Declined, with nothing to show.
case err != nil:
return err
case resp.StopReason == skyl.StopRefusal:
// Declined, but said something about why.
fmt.Println(resp.Text())
}resp, err := client.Complete(ctx, req)
switch {
case errors.Is(err, skyl.ErrRefusal):
// Declined, with nothing to show.
case err != nil:
return err
case resp.StopReason == skyl.StopRefusal:
// Declined, but said something about why.
fmt.Println(resp.Text())
}The provider-specific ones#
And on Gemini, a response containing any function call reports
StopToolUse regardless of its actual finishReason — so a Gemini response can
be both truncated and reported as tool_use.
Deep diveWhy StopUnknown rather than a growing enum
Providers add finish reasons regularly — pause_turn, model_context_window_exceeded,
MALFORMED_FUNCTION_CALL. If skyl minted a constant for each, every new
provider value would require a skyl release before you could branch on it, which
is the same failure mode as a curated model list.
StopUnknown plus Raw means an unmapped value is readable immediately, and
your switch has a default branch that is honest about not knowing rather
than one that misclassifies.
Recap
StopMaxTokensmeans truncated — treat it as incomplete, never as an answer.StopToolUsemeans the model is waiting for you;Text()may be empty.- A refusal with text is a
StopRefusalresponse; one without text is anErrRefusalerror. StopStopSequenceis Anthropic-only — the other providers reportstop/STOP.- On Gemini, any function call forces
StopToolUseregardless offinishReason. - Unmapped values become
StopUnknownrather than a wrong guess; readRaw.
Try out some challenges
Each of these is solvable with what this page covered. Run them against the sandbox — no API key needed.
Write an exhaustive stop-reason switch
Handle every stop reason in a way that never presents incomplete output as complete.
Show hint
The default branch matters as much as the named ones — StopUnknown is real
and you will hit it.
Show solution
switch resp.StopReason {
case skyl.StopEndTurn, skyl.StopStopSequence:
return resp.Text(), nil
case skyl.StopMaxTokens:
return "", fmt.Errorf("truncated at %d tokens; raise MaxTokens", req.MaxTokens)
case skyl.StopToolUse:
return "", errToolCallsPending
case skyl.StopRefusal:
return "", fmt.Errorf("the model declined: %s", resp.Text())
default:
// StopUnknown, or something newer than this build. Be honest.
log.Printf("unmapped stop reason; raw: %s", resp.Raw)
return resp.Text(), nil
}switch resp.StopReason {
case skyl.StopEndTurn, skyl.StopStopSequence:
return resp.Text(), nil
case skyl.StopMaxTokens:
return "", fmt.Errorf("truncated at %d tokens; raise MaxTokens", req.MaxTokens)
case skyl.StopToolUse:
return "", errToolCallsPending
case skyl.StopRefusal:
return "", fmt.Errorf("the model declined: %s", resp.Text())
default:
// StopUnknown, or something newer than this build. Be honest.
log.Printf("unmapped stop reason; raw: %s", resp.Raw)
return resp.Text(), nil
}Grouping StopStopSequence with StopEndTurn is right for most callers: both
mean "the model finished deliberately". Splitting them only matters if you are
detecting the sequence itself, which is not portable anyway.