harry0703/MoneyPrinterTurbo · error · ValueError

[{llm_provider}] returned empty message

Error message

[{llm_provider}] returned empty message

What it means

Structural guard in _extract_chat_completion_text: choices[0] exists but its .message attribute is None. Some OpenAI-compatible providers omit the message object entirely on error or partial responses; accessing .content on None would crash.

Source

Thrown at app/services/llm.py:97

    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):
        return getattr(value, key, None)


def _extract_qwen_generation_text(response) -> str:
    """

View on GitHub (pinned to 1f9f19c202)

Solutions

  1. Dump the full response object to inspect the actual choice shape
  2. Update or pin the provider adapter/SDK to a version matching the current API schema
  3. Retry once; partial-choice responses are often transient provider glitches
  4. Report/fix the gateway if it mangles the message field

Example fix

# before
message = response.choices[0].message
content = message.content  # message is None -> AttributeError

# after
choice = response.choices[0]
message = getattr(choice, "message", None)
if message is None:
    raise ValueError(f"[{provider}] returned empty message: {choice!r}")
content = message.content
Defensive patterns

Strategy: type-guard

Validate before calling

choice = response.choices[0]
assert getattr(choice, "message", None) is not None, f"choice lacks message: {choice!r}"

Type guard

def has_message(choice) -> bool:
    return getattr(choice, "message", None) is not None

Prevention

When it happens

Trigger: A completion choice with message=null — seen with providers that return choices containing only a delta/error stub, or truncated responses where the choice was constructed without a message.

Common situations: Streaming-shaped responses accidentally consumed via the non-streaming API, gateway transformations dropping fields, or provider-specific schema drift after an update.

Related errors


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