666ghj/MiroFish · error · LLMResponseError

LLM returned empty JSON content

Error message

LLM returned empty JSON content

What it means

LLMResponseError raised when the completion succeeded (finish_reason ok) but the extracted text is empty after cleaning — the model returned no content for the message. There is nothing to json.loads, and the client refuses to substitute an empty object.

Source

Thrown at backend/app/utils/llm_client.py:255

        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:
                value, end = json.JSONDecoder().raw_decode(content)
            except json.JSONDecodeError:
                raise LLMResponseError(
                    "LLM returned invalid JSON "
                    f"(line {strict_error.lineno}, column {strict_error.colno})",
                    finish_reason=finish_reason,
                ) from strict_error

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Retry the request — empty content with a clean finish_reason is frequently transient
  2. Log the raw response to see where the content actually went (e.g. tool_calls, reasoning field)
  3. Strengthen the system prompt to require a non-empty JSON object as the entire reply
  4. If a gateway consistently returns empty content, test the same request against the upstream provider to isolate the gateway bug

Example fix

# before
messages = [{"role": "user", "content": task}]

# after
messages = [{"role": "system", "content": "Reply with exactly one non-empty JSON object and nothing else."},
            {"role": "user", "content": task}]
Defensive patterns

Strategy: retry

Try / catch

try:
    value = LLMClient._parse_json_response(resp)
except LLMResponseError as e:
    if "empty JSON content" in str(e):
        value = LLMClient._parse_json_response(retry_once(create_fn))
    else:
        raise

Prevention

When it happens

Trigger: Model returns a message with empty content (some providers return empty content alongside tool_calls or when the answer was fully filtered), or whitespace-only output that _clean_chat_text reduces to ''.

Common situations: OpenAI-compatible gateways that emit an empty content field with finish_reason 'stop', models outputting only whitespace/newlines, or response_format quirks where content lives in a field the extractor does not read.

Related errors


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