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

  1. Log the raw response (response.model_dump() for SDK objects) to see the actual payload — usually an error envelope in disguise
  2. If a gateway is involved, fix its error passthrough so failures surface as exceptions, not empty 200s
  3. Retry once — gateway hiccups are frequently transient
  4. 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

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


AI-assisted analysis of harry0703/MoneyPrinterTurbo@1f9f19c202 (2026-08-14). Data as JSON: /api/errors/a053e233d1c4ff70. Report an issue: GitHub.