Handle interrupted and declined replies
A streamed reply can stop before it is complete, and a model can decline a request. Both leave your application holding a partial or unexpected answer. This page covers how to detect each case today and what to show the user.
Ways a reply can end
| Situation | How you detect it today | What to do |
|---|---|---|
| Finished normally | finish_reason: "stop", then data: [DONE] | Show the reply. |
| Hit the length cap | finish_reason: "length" | Show it with a “continue” option, or raise max_tokens next time. |
| Waiting for tools | finish_reason: "tool_calls" | Run the tools and continue the loop. |
| Failed mid-stream | A final event whose error.code is stream_error (error.type api_error) | Treat the text as incomplete; retry or tell the user. |
| Connection dropped | The stream ends without data: [DONE] | Same as a mid-stream failure. |
| Model declined | finish_reason: "stop" with a refusal in the text | Show it; offer another route if your product has one. |
Detect incomplete streams
import json
import os
import requests
def stream_reply(body: dict):
"""Yields text pieces; returns a status of 'complete', 'length', 'tool_calls' or 'incomplete'."""
status = "incomplete"
with requests.post(
"https://app.syntic.ai/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['SYNTIC_API_KEY']}"},
json={**body, "stream": True},
stream=True,
timeout=(10, 300),
) as r:
r.raise_for_status()
finish = None
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
if line == "data: [DONE]":
status = {"stop": "complete", "length": "length", "tool_calls": "tool_calls"}.get(finish, "complete")
break
event = json.loads(line[6:])
if "error" in event:
break # stream_error: leave status as incomplete
choice = (event.get("choices") or [{}])[0]
finish = choice.get("finish_reason") or finish
piece = choice.get("delta", {}).get("content")
if piece:
yield piece
return statusWhat to show for a partial reply
- Keep what arrived. Partial text is often useful. Display it with a clear marker (“The answer was cut off”) and a Retry button.
- Do not store it as a finished turn. If you keep conversation history, mark partial assistant turns so the next request does not treat them as complete statements, or drop them before retrying.
- Retry the whole request. There is no way to resume a stream from where it stopped. A retry produces a new reply that may be worded differently; replace the partial text rather than appending to it.
- Bill awareness. Tokens generated before the failure are billed. Cap automatic retries at one or two; see Retries with back-off.
Declined requests
Sometimes the model declines: the request conflicts with its guidelines, or with your system message. Today this arrives as an ordinary reply that says so, with finish_reason: "stop". If your product needs to react differently to a refusal (for example, offer a human agent), detect it with a cheap follow-up classification, or ask the model in your system message to start such replies with a fixed marker you can test for:
If you cannot help with a request, start your reply with "[unable]" and then explain briefly.⚠️
Preview — this feature is being built; the design below is the spec. A fixed list of finish reasons, including
refusal and content_filter, is specified on Stop reasons, together with mid-stream errors in the standard error envelope (Streaming). When they ship, check finish_reason instead of a text marker.Related pages
Was this page helpful?