666ghj/MiroFish · error · LLMResponseError
LLM JSON generation stopped unexpectedly ({finish_reason})
Error message
LLM JSON generation stopped unexpectedly ({finish_reason}) What it means
LLMResponseError raised when finish_reason is anything other than None, 'stop', or 'length' — generation ended abnormally. Common values: 'content_filter' (safety filter stopped output), 'tool_calls' (model tried to call a tool instead of answering), or provider-specific codes.
Source
Thrown at backend/app/utils/llm_client.py:248
if last_error is not None: # pragma: no cover - defensive loop guard
raise last_error
raise LLMResponseError("LLM did not produce a JSON response")
@staticmethod
def _parse_json_response(response: Any) -> Dict[str, Any]:
choices = getattr(response, "choices", None) or []
if not choices:
raise LLMResponseError("LLM returned no choices")
choice = choices[0]
finish_reason = getattr(choice, "finish_reason", None)
if finish_reason == "length":
raise LLMResponseError(
"LLM JSON output was truncated at the token limit",
finish_reason=finish_reason,
)
if finish_reason not in {None, "stop"}:
raise LLMResponseError(
f"LLM JSON generation stopped unexpectedly ({finish_reason})",
finish_reason=finish_reason,
)
content = _clean_chat_text(extract_chat_completion_text(response))
if not content:
raise LLMResponseError(
"LLM returned empty JSON content",
finish_reason=finish_reason,
)
try:
value = json.loads(content)
except json.JSONDecodeError as strict_error:
# Some compatible providers append a short explanation after an
# otherwise complete JSON object. Accept only an object decoded
# from the beginning; never repair or invent truncated JSON.
try:View on GitHub (pinned to b5b53acc57)
Solutions
- Inspect the finish_reason in the exception (it is attached as finish_reason on LLMResponseError) to pick the fix
- content_filter: soften or rephrase the prompt content that triggered moderation
- tool_calls: remove/disable tool definitions for this call or instruct the model to answer with JSON only
- Nonstandard provider codes: pin a provider whose finish_reason contract matches OpenAI's, or map the code upstream of this parser
Example fix
# before
messages = [{"role": "user", "content": raw_user_text}]
value = client.generate_json(messages)
# after (content_filter case: pre-sanitize and constrain the task)
messages = [{"role": "system", "content": "Output only a JSON object."},
{"role": "user", "content": sanitized_task}]
value = client.generate_json(messages) Defensive patterns
Strategy: try-catch
Try / catch
try:
value = LLMClient._parse_json_response(resp)
except LLMResponseError as e:
if e.finish_reason == "content_filter":
value = client.generate_json(sanitized(messages))
else:
raise Prevention
- Check the attached finish_reason to route the fix (filter vs tool_calls vs provider quirk)
- Disable tool definitions on calls that must return pure JSON text
- Test against the actual provider's finish_reason vocabulary
When it happens
Trigger: The model's response was cut by moderation (content_filter), the model responded with a tool-call instead of JSON text, or an OpenAI-compatible provider returned a nonstandard finish_reason the strict parser refuses.
Common situations: Prompts that trip provider safety filters, models configured with tools that prefer tool_calls over text, or gateway providers with custom finish_reason vocabularies.
Related errors
- Ontology result must be an object
- LLM JSON output was truncated at the token limit
- LLM returned empty JSON content
- LLM returned invalid JSON (line {strict_error.lineno}, colum
- LLM returned multiple JSON values
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/420899838ad532af.
Report an issue: GitHub.