{"record":{"id":"420899838ad532af","repo":"666ghj/MiroFish","slug":"llm-json-generation-stopped-unexpectedly-finish","errorCode":null,"errorMessage":"LLM JSON generation stopped unexpectedly ({finish_reason})","messagePattern":"LLM JSON generation stopped unexpectedly \\((.+?)\\)","errorType":"exception","errorClass":"LLMResponseError","httpStatus":null,"severity":"error","filePath":"backend/app/utils/llm_client.py","lineNumber":248,"sourceCode":"        if last_error is not None:  # pragma: no cover - defensive loop guard\n            raise last_error\n        raise LLMResponseError(\"LLM did not produce a JSON response\")\n\n    @staticmethod\n    def _parse_json_response(response: Any) -> Dict[str, Any]:\n        choices = getattr(response, \"choices\", None) or []\n        if not choices:\n            raise LLMResponseError(\"LLM returned no choices\")\n\n        choice = choices[0]\n        finish_reason = getattr(choice, \"finish_reason\", None)\n        if finish_reason == \"length\":\n            raise LLMResponseError(\n                \"LLM JSON output was truncated at the token limit\",\n                finish_reason=finish_reason,\n            )\n        if finish_reason not in {None, \"stop\"}:\n            raise LLMResponseError(\n                f\"LLM JSON generation stopped unexpectedly ({finish_reason})\",\n                finish_reason=finish_reason,\n            )\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:","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/utils/llm_client.py#L230-L266","documentation":"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.","triggerScenarios":"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.","commonSituations":"Prompts that trip provider safety filters, models configured with tools that prefer tool_calls over text, or gateway providers with custom finish_reason vocabularies.","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"],"exampleFix":"# before\nmessages = [{\"role\": \"user\", \"content\": raw_user_text}]\nvalue = client.generate_json(messages)\n\n# after (content_filter case: pre-sanitize and constrain the task)\nmessages = [{\"role\": \"system\", \"content\": \"Output only a JSON object.\"},\n            {\"role\": \"user\", \"content\": sanitized_task}]\nvalue = client.generate_json(messages)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    value = LLMClient._parse_json_response(resp)\nexcept LLMResponseError as e:\n    if e.finish_reason == \"content_filter\":\n        value = client.generate_json(sanitized(messages))\n    else:\n        raise","preventionTips":["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"],"tags":["llm","finish-reason","content-filter","json"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}