{"record":{"id":"751f364c3843da13","repo":"harry0703/MoneyPrinterTurbo","slug":"llm-provider-returned-empty-text-content","errorCode":null,"errorMessage":"[{llm_provider}] returned empty text content","messagePattern":"\\[(.+?)\\] returned empty text content","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"app/services/llm.py","lineNumber":52,"sourceCode":"\n## Constrains:\n1. the script is to be returned as a string with the specified number of paragraphs.\n2. do not under any circumstance reference this prompt in your response.\n3. get straight to the point, don't start with unnecessary things like, \"welcome to this video\".\n4. you must not include any type of markdown or formatting in the script, never use a title.\n5. only return the raw content of the script.\n6. do not include \"voiceover\", \"narrator\" or similar indicators of what should be spoken at the beginning of each paragraph or line.\n7. you must not mention the prompt, or anything about the script itself. also, never talk about the amount of paragraphs or lines. just write the script.\n8. respond in the same language as the video subject.\n\"\"\".strip()\n\n\ndef _normalize_text_response(content, llm_provider: str) -> str:\n    # 不同 LLM SDK 在异常或被拦截场景下，可能返回 None、空字符串，\n    # 甚至返回非字符串对象。这里统一做兜底校验，避免后续直接调用\n    # `.replace()` 时抛出 `NoneType` 之类的属性错误。\n    if content is None:\n        raise ValueError(f\"[{llm_provider}] returned empty text content\")\n\n    if not isinstance(content, str):\n        raise TypeError(\n            f\"[{llm_provider}] returned non-text content: {type(content).__name__}\"\n        )\n\n    # MiniMax M3、DeepSeek R1 这类 reasoning 模型可能会把内部推理包在\n    # `<think>...</think>` 中返回。视频脚本和关键词只需要最终可朗读文本，\n    # 如果不在服务层统一清理，WebUI、字幕和配音都会把思考过程当正文处理。\n    content = _THINK_BLOCK_RE.sub(\"\", content)\n    content = _UNCLOSED_THINK_BLOCK_RE.sub(\"\", content).strip()\n    if not content:\n        raise ValueError(f\"[{llm_provider}] returned empty text content\")\n\n    return content.replace(\"\\n\", \"\")\n\n\ndef _sanitize_error_message(error: object) -> str:","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/llm.py#L34-L70","documentation":"First empty-content branch in _normalize_text_response: the LLM SDK returned content=None. Comment notes various SDKs return None on interception/exception paths; this guard converts it into an explicit ValueError before downstream .replace() calls would crash with AttributeError on NoneType.","triggerScenarios":"An OpenAI-compatible provider returns a completion where message.content is null — typical for content-filtered responses, tool-call-only responses, or providers whose safety filter silently nulls content.","commonSituations":"Gemini/OpenAI safety filters blocking the script prompt, reasoning models that put everything in a reasoning field and leave content null, or proxies that strip content on policy grounds.","solutions":["Retry once — safety-filter nulls are often prompt-specific and transient; vary the prompt slightly if it persists","Inspect the full raw response (finish_reason/finish_reason field) to confirm content filtering vs. a provider bug","Loosen the prompt: remove topics that trigger provider filters","Switch provider or model (e.g. from a filtered hosted model to a self-hosted OpenAI-compatible endpoint)"],"exampleFix":"# before\ntext = response.choices[0].message.content.replace(\"\\n\", \"\")  # NoneType crash\n\n# after\ncontent = response.choices[0].message.content\nif content is None:\n    raise ValueError(f\"[{provider}] returned empty text content (finish_reason=\"\n                     f\"{response.choices[0].finish_reason})\")\ntext = content.replace(\"\\n\", \"\")","handlingStrategy":"type-guard","validationCode":"content = getattr(message, \"content\", None)\nif content is None:\n    finish = getattr(choice, \"finish_reason\", None)\n    raise ValueError(f\"content null (finish_reason={finish})\")","typeGuard":"def has_text_content(message) -> bool:\n    return isinstance(getattr(message, \"content\", None), str) and bool(message.content.strip())","tryCatchPattern":"try:\n    text = generate(prompt)\nexcept ValueError as e:\n    if \"empty text content\" in str(e):\n        text = generate(tweak_prompt(prompt))  # one retry, then surface to user","preventionTips":["Always null-check message.content before string ops","Check finish_reason to distinguish content filtering from provider bugs"],"tags":["llm","response-validation","content-filter"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}