Skip to content
skyl

Stop Reasons

Six values, two of which are effectively provider-specific.

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 StopStopSequence only ever comes from Anthropic
  • How to handle truncation and refusal properly

The values#

ConstantValueMeaningProvider values
StopEndTurnend_turnThe model finished naturally.end_turn, stop, STOP
StopMaxTokensmax_tokensThe output hit Request.MaxTokens. The response is truncated — treat it as incomplete.max_tokens, length, MAX_TOKENS, model_context_window_exceeded
StopToolUsetool_useThe model wants a tool run. Execute the calls and send the results back.tool_use, tool_calls, function_call
StopStopSequencestop_sequenceA 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)
StopRefusalrefusalThe 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
StopUnknownunknownThe 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.

goCompiles
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.

goCompiles
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.
}
goCompiles
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

  • StopMaxTokens means truncated — treat it as incomplete, never as an answer.
  • StopToolUse means the model is waiting for you; Text() may be empty.
  • A refusal with text is a StopRefusal response; one without text is an ErrRefusal error.
  • StopStopSequence is Anthropic-only — the other providers report stop/STOP.
  • On Gemini, any function call forces StopToolUse regardless of finishReason.
  • Unmapped values become StopUnknown rather than a wrong guess; read Raw.

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
goCompiles
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.

Edit this page on GitHub