harry0703/MoneyPrinterTurbo · error · ValueError
[{llm_provider}] returned empty choices
Error message
[{llm_provider}] returned empty choices What it means
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].
Source
Thrown at app/services/llm.py:92
一些 OpenAI-compatible SDK 会把请求 URL 原样拼进异常信息。如果用户为了
代理网关配置了 `https://user:pass@example.com/v1`,直接返回 `str(e)`
就会把密码暴露给页面、API 调用方或后续日志。这里仅处理错误文案,不改变
实际请求地址,避免影响正常调用链路。
"""
message = str(error)
message = _URL_USERINFO_RE.sub(r"\1***:***@", message)
message = _SENSITIVE_QUERY_RE.sub(r"\1***", message)
return message
def _extract_chat_completion_text(response, llm_provider: str) -> str:
# OpenAI 兼容接口在异常场景下,可能返回没有 choices、
# 或者 choices/message/content 为空的响应对象。
# 这里统一做结构校验,避免出现 `NoneType is not subscriptable`
# 这类底层属性访问错误。
choices = getattr(response, "choices", None)
if not choices:
raise ValueError(f"[{llm_provider}] returned empty choices")
first_choice = choices[0]
message = getattr(first_choice, "message", None)
if message is None:
raise ValueError(f"[{llm_provider}] returned empty message")
content = getattr(message, "content", None)
return _normalize_text_response(content, llm_provider)
def _get_response_field(value, key: str):
"""兼容 dict 和 SDK 响应对象的字段读取。"""
if isinstance(value, dict):
return value.get(key)
try:
return value[key]
except (KeyError, TypeError, AttributeError):View on GitHub (pinned to 1f9f19c202)
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
Example fix
# before
text = response.choices[0].message.content # empty choices -> IndexError/TypeError
# after
choices = getattr(response, "choices", None) or []
if not choices:
raise ValueError(f"[{provider}] returned empty choices: {response}")
text = choices[0].message.content Defensive patterns
Strategy: type-guard
Validate before calling
choices = getattr(response, "choices", None)
assert choices, f"degenerate completion: {response}" Type guard
def has_choices(response) -> bool:
return bool(getattr(response, "choices", None)) Try / catch
try:
text = _extract_chat_completion_text(response, provider)
except ValueError as e:
if "empty choices" in str(e):
log_raw_response(response) # usually a gateway error envelope with HTTP 200 Prevention
- Log raw responses when integrating a new OpenAI-compatible gateway
- Configure gateways to propagate upstream errors as non-200, never empty 200s
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- [{llm_provider}] returned empty message
- [{llm_provider}] returned empty text content
- [{llm_provider}] returned non-text content: {type(content)._
- [qwen] returned empty choices
- {llm_provider}: base_url is not set, please set it in the co
AI-assisted analysis of harry0703/MoneyPrinterTurbo@1f9f19c202 (2026-08-14).
Data as JSON: /api/errors/a053e233d1c4ff70.
Report an issue: GitHub.