Stop reasons
Every reply says why the model stopped writing. Your code reads that value to decide what happens next: show the answer, run a tool, ask for more, or handle a refusal. The list of values is fixed and documented, and it stays the same whichever model answers, so a loop written today keeps working when you switch models.
Today finish_reason is passed through from the model service and is usually one of stop, length or tool_calls. The design below fixes the full list.
Where it appears
| Endpoint | Field |
|---|---|
| Chat completions, whole reply | choices[0].finish_reason |
| Chat completions, stream | finish_reason on the last content chunk (null before that) |
| Messages | stop_reason on the message, and on the message_delta stream event |
| Batches | Inside each result line, same as the endpoint used |
The fixed list
| Value | Meaning | What to do |
|---|---|---|
stop | The model finished its answer naturally. | Use the reply. |
length | The reply reached max_tokens, or the conversation filled the context window. | Raise max_tokens, or continue: send the partial reply back as an assistant message and ask the model to go on. |
stop_sequence | One of your stop strings was produced. The matching string is in stop_sequence. | Use the reply; it ends right before the string. |
tool_calls | The model wants one or more function tools run. | Run them and send the results. See Tool calling. |
refusal | The model declined to help with the request. The reply text explains why. | Show the explanation; do not retry the same request unchanged. |
content_filter | Syntic’s safety checks stopped the output. The text so far may be cut off. | Do not show partial text as complete. Log request_id. |
pause | A long server-side tool loop (for example web search) reached its step limit for one request. | Send the conversation back unchanged to let it continue. |
No other value is ever returned. If the model service reports something unknown, the API maps it to the closest value above and logs it for the platform team.
length because of the context window, rather than max_tokens, is marked with "length_reason": "context_window" next to the stop reason, so you can tell the two apart.
Handling it in code
curl -s https://app.syntic.ai/v1/chat/completions \
-H "Authorization: Bearer $SYNTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"Amara S1.2","stream":false,"max_tokens":20,
"messages":[{"role":"user","content":"Describe the water cycle."}]}' \
| jq '.choices[0] | {finish_reason, length_reason}'
# { "finish_reason": "length", "length_reason": "max_tokens" }Errors are not stop reasons
A stop reason means a reply was produced. Failures (bad key, rate limit, model unavailable) are HTTP errors in the error envelope. A failure after a stream has started arrives as an error event, and the stream then has no stop reason at all; treat that reply as incomplete.
Build notes for the platform team
- Normalise in one function shared by both endpoints and batches; unit-test every upstream value seen in the last 90 days of logs.
stop_sequencerequires stop strings to be forwarded (see Request parameters).refusaldetection uses the upstream signal where it exists; never guess from text.