{"record":{"id":"9b61e1beb08cb477","repo":"666ghj/MiroFish","slug":"llm-returned-no-choices","errorCode":null,"errorMessage":"LLM returned no choices","messagePattern":"LLM returned no choices","errorType":"exception","errorClass":"LLMResponseError","httpStatus":null,"severity":"error","filePath":"backend/app/utils/llm_client.py","lineNumber":238,"sourceCode":"                # use its model-specific output limit.\n                had_token_cap = request_max_tokens is not None\n                request_max_tokens = None\n                logger.warning(\n                    \"LLM returned unusable JSON (finish_reason=%s); \"\n                    \"retrying content generation%s\",\n                    error.finish_reason or \"unknown\",\n                    \" without an output token cap\" if had_token_cap else \"\",\n                )\n\n        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\",","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/utils/llm_client.py#L220-L256","documentation":"LLMResponseError raised in _parse_json_response when the completion response has an empty or missing 'choices' list. This means the provider acknowledged the request but returned no candidate generations — distinct from a network error or invalid JSON; the response object itself is malformed/empty at the choice level.","triggerScenarios":"A chat completion comes back with choices=[] or choices=None: provider-side incident, an OpenAI-compatible gateway returning a degenerate 200 response, or content that was filtered before generation produced any choice.","commonSituations":"Using OpenAI-compatible third-party endpoints (proxies, local servers) whose error paths return 200 with empty bodies; provider outages; misconfigured base_url pointing at a wrong route that returns an unexpected shape.","solutions":["Retry the request — empty choices from a provider is usually transient (upstream retry wrapper may already cover it; check whether this path was reached after retries)","Log the full response object and request id to identify what the provider actually returned","If using an OpenAI-compatible gateway, verify its /chat/completions implementation returns standard shapes","Verify base_url points to a real OpenAI-compatible endpoint and the model name is valid for it"],"exampleFix":"# before\nresp = client.chat.completions.create(**params)\nvalue = LLMClient._parse_json_response(resp)\n\n# after\nresp = client.chat.completions.create(**params)\nif not getattr(resp, \"choices\", None):\n    logger.warning(\"empty choices from provider, model=%s id=%s\", model, getattr(resp, \"id\", None))\n    resp = retry_once(lambda: client.chat.completions.create(**params))\nvalue = LLMClient._parse_json_response(resp)","handlingStrategy":"retry","validationCode":"if not getattr(response, \"choices\", None):\n    raise LLMResponseError(\"empty choices — retry or investigate provider\")","typeGuard":null,"tryCatchPattern":"try:\n    value = LLMClient._parse_json_response(resp)\nexcept LLMResponseError as e:\n    if \"no choices\" in str(e):\n        resp = retry_with_backoff(lambda: client.chat.completions.create(**params))\n        value = LLMClient._parse_json_response(resp)\n    else:\n        raise","preventionTips":["Wrap completions in bounded retry with backoff for transient provider anomalies","Log full provider responses (id + shape) when anomalies occur","Verify OpenAI-compatible gateways return standard completion shapes before adopting them"],"tags":["llm","openai-compat","empty-response","retry"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}