{"record":{"id":"a053e233d1c4ff70","repo":"harry0703/MoneyPrinterTurbo","slug":"llm-provider-returned-empty-choices","errorCode":null,"errorMessage":"[{llm_provider}] returned empty choices","messagePattern":"\\[(.+?)\\] returned empty choices","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"app/services/llm.py","lineNumber":92,"sourceCode":"    一些 OpenAI-compatible SDK 会把请求 URL 原样拼进异常信息。如果用户为了\n    代理网关配置了 `https://user:pass@example.com/v1`，直接返回 `str(e)`\n    就会把密码暴露给页面、API 调用方或后续日志。这里仅处理错误文案，不改变\n    实际请求地址，避免影响正常调用链路。\n    \"\"\"\n    message = str(error)\n    message = _URL_USERINFO_RE.sub(r\"\\1***:***@\", message)\n    message = _SENSITIVE_QUERY_RE.sub(r\"\\1***\", message)\n    return message\n\n\ndef _extract_chat_completion_text(response, llm_provider: str) -> str:\n    # OpenAI 兼容接口在异常场景下，可能返回没有 choices、\n    # 或者 choices/message/content 为空的响应对象。\n    # 这里统一做结构校验，避免出现 `NoneType is not subscriptable`\n    # 这类底层属性访问错误。\n    choices = getattr(response, \"choices\", None)\n    if not choices:\n        raise ValueError(f\"[{llm_provider}] returned empty choices\")\n\n    first_choice = choices[0]\n    message = getattr(first_choice, \"message\", None)\n    if message is None:\n        raise ValueError(f\"[{llm_provider}] returned empty message\")\n\n    content = getattr(message, \"content\", None)\n    return _normalize_text_response(content, llm_provider)\n\n\ndef _get_response_field(value, key: str):\n    \"\"\"兼容 dict 和 SDK 响应对象的字段读取。\"\"\"\n    if isinstance(value, dict):\n        return value.get(key)\n\n    try:\n        return value[key]\n    except (KeyError, TypeError, AttributeError):","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/llm.py#L74-L110","documentation":"Structural guard in _extract_chat_completion_text: response has no usable choices attribute (missing or empty list). OpenAI-compatible endpoints occasionally return such degenerate objects on error paths; the guard prevents 'NoneType is not subscriptable' when indexing choices[0].","triggerScenarios":"chat.completions.create returns an object whose .choices is None or [] — happens with some proxies on upstream 5xx, LiteLLM fallback edge cases, or providers returning an error envelope with HTTP 200.","commonSituations":"Self-hosted gateways (one-api/new-api style) that map upstream errors to 200 with an empty body, provider outages where the SDK parses a truncated response, or SDK/pydantic version mismatches dropping the choices field.","solutions":["Log the raw response (response.model_dump() for SDK objects) to see the actual payload — usually an error envelope in disguise","If a gateway is involved, fix its error passthrough so failures surface as exceptions, not empty 200s","Retry once — gateway hiccups are frequently transient","Pin openai SDK versions known to parse your provider correctly"],"exampleFix":"# before\ntext = response.choices[0].message.content  # empty choices -> IndexError/TypeError\n\n# after\nchoices = getattr(response, \"choices\", None) or []\nif not choices:\n    raise ValueError(f\"[{provider}] returned empty choices: {response}\")\ntext = choices[0].message.content","handlingStrategy":"type-guard","validationCode":"choices = getattr(response, \"choices\", None)\nassert choices, f\"degenerate completion: {response}\"","typeGuard":"def has_choices(response) -> bool:\n    return bool(getattr(response, \"choices\", None))","tryCatchPattern":"try:\n    text = _extract_chat_completion_text(response, provider)\nexcept ValueError as e:\n    if \"empty choices\" in str(e):\n        log_raw_response(response)  # usually a gateway error envelope with HTTP 200","preventionTips":["Log raw responses when integrating a new OpenAI-compatible gateway","Configure gateways to propagate upstream errors as non-200, never empty 200s"],"tags":["llm","response-validation","openai-compatible"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}