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
- Dump the full response object to inspect the actual choice shape
- Update or pin the provider adapter/SDK to a version matching the current API schema
- Retry once; partial-choice responses are often transient provider glitches
- 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
- Never index into choices without checking message first
- Watch for streaming-shaped responses consumed via non-streaming APIs
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
- [{llm_provider}] returned empty choices
- [{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/50ab5ec459a58722.
Report an issue: GitHub.