{"record":{"id":"fba20371ca398827","repo":"666ghj/MiroFish","slug":"llm-returned-invalid-json-line-strict-error-line","errorCode":null,"errorMessage":"LLM returned invalid JSON (line {strict_error.lineno}, column {strict_error.colno})","messagePattern":"LLM returned invalid JSON \\(line (.+?), column (.+?)\\)","errorType":"exception","errorClass":"LLMResponseError","httpStatus":null,"severity":"error","filePath":"backend/app/utils/llm_client.py","lineNumber":269,"sourceCode":"            )\n\n        content = _clean_chat_text(extract_chat_completion_text(response))\n        if not content:\n            raise LLMResponseError(\n                \"LLM returned empty JSON content\",\n                finish_reason=finish_reason,\n            )\n\n        try:\n            value = json.loads(content)\n        except json.JSONDecodeError as strict_error:\n            # Some compatible providers append a short explanation after an\n            # otherwise complete JSON object. Accept only an object decoded\n            # from the beginning; never repair or invent truncated JSON.\n            try:\n                value, end = json.JSONDecoder().raw_decode(content)\n            except json.JSONDecodeError:\n                raise LLMResponseError(\n                    \"LLM returned invalid JSON \"\n                    f\"(line {strict_error.lineno}, column {strict_error.colno})\",\n                    finish_reason=finish_reason,\n                ) from strict_error\n\n            trailing = content[end:].strip()\n            if trailing:\n                if _contains_additional_json_container(trailing):\n                    raise LLMResponseError(\n                        \"LLM returned multiple JSON values\",\n                        finish_reason=finish_reason,\n                    )\n                logger.warning(\"Ignoring text after a complete LLM JSON object\")\n\n        if not isinstance(value, dict):\n            raise LLMResponseError(\n                \"LLM JSON response must be a top-level JSON object\",\n                finish_reason=finish_reason,","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/utils/llm_client.py#L251-L287","documentation":"LLMResponseError raised when the response content fails both json.loads and a lenient raw_decode from position 0 — the text is not a JSON object starting at the beginning, and the client explicitly refuses to repair or invent truncated JSON. The original strict error's line/column are reported for diagnosis.","triggerScenarios":"Model wraps JSON in markdown fences or prose ('Here is the JSON: {...}') such that raw_decode from offset 0 fails, or the JSON itself is malformed/hand-edited by the model (trailing commas, unquoted keys in strict mode, single quotes).","commonSituations":"Models that preamble before JSON despite instructions, prompt templates without a strict JSON-only format, or truncation that corrupted structure (though plain truncation usually surfaces as error 73 when finish_reason is 'length').","solutions":["Force a JSON-only response mode: use the provider's response_format={'type': 'json_object'} if supported, or a system prompt that forbids any text outside the JSON","Log the raw content plus the reported line/column to identify whether it is preamble, fences, or genuine malformation","If the model persistently adds prose, strip known wrappers (code fences, leading text up to the first '{') before calling the parser — but never repair broken JSON","Retry: single malformed outputs are often non-deterministic"],"exampleFix":"# before\nresp = client.chat.completions.create(model=m, messages=msgs)\nvalue = LLMClient._parse_json_response(resp)\n\n# after\nresp = client.chat.completions.create(\n    model=m, messages=msgs,\n    response_format={\"type\": \"json_object\"},  # provider-enforced JSON\n)\nvalue = LLMClient._parse_json_response(resp)","handlingStrategy":"retry","validationCode":"def looks_like_json_object(text: str) -> bool:\n    t = text.lstrip()\n    return t.startswith('{') and t.rstrip().endswith('}')","typeGuard":null,"tryCatchPattern":"try:\n    value = LLMClient._parse_json_response(resp)\nexcept LLMResponseError as e:\n    if \"invalid JSON\" in str(e):\n        value = LLMClient._parse_json_response(retry_with_stricter_prompt())\n    else:\n        raise","preventionTips":["Enable response_format={'type': 'json_object'} on providers that support it","Forbid prose/fences in the system prompt","Strip only known safe wrappers (fences, leading prose) — never repair broken JSON"],"tags":["llm","json","parsing","prompt-engineering"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}