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_errorView on GitHub (pinned to b5b53acc57)
Solutions
- Retry the request — empty content with a clean finish_reason is frequently transient
- Log the raw response to see where the content actually went (e.g. tool_calls, reasoning field)
- Strengthen the system prompt to require a non-empty JSON object as the entire reply
- 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
- Use a strict system prompt: one non-empty JSON object, nothing else
- Prefer provider JSON mode (response_format json_object) where available
- Log raw responses to spot content living in unexpected fields
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
- Ontology result must be an object
- LLM returned no choices
- LLM JSON output was truncated at the token limit
- LLM JSON generation stopped unexpectedly ({finish_reason})
- LLM returned invalid JSON (line {strict_error.lineno}, colum
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/62f3ff96886edb88.
Report an issue: GitHub.