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

  1. Inspect the finish_reason in the exception (it is attached as finish_reason on LLMResponseError) to pick the fix
  2. content_filter: soften or rephrase the prompt content that triggered moderation
  3. tool_calls: remove/disable tool definitions for this call or instruct the model to answer with JSON only
  4. 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

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


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/420899838ad532af. Report an issue: GitHub.