From 3db778ff09f07c1af7a6069cee218f452fe309e7 Mon Sep 17 00:00:00 2001 From: renchonghan <112455231+renchonghan@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:26:09 +0800 Subject: [PATCH] fix: prevent API 400 errors by ensuring assistant messages with reasoning_content but no content or tool_calls are preserved with a placeholder content value (#8483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: preserve assistant messages with reasoning_content in sanitize pass When _sanitize_assistant_messages encounters an assistant message with empty content and no tool_calls but with reasoning_content, keep it with content set to empty string instead of dropping it. Reasoning models (DeepSeek V4, MiMo, etc.) require this history for subsequent turn validation. * Update astrbot/core/provider/sources/openai_source.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix: default to empty TokenUsage when completion.usage is None When completion.usage is None (e.g. certain proxy/streaming edge cases), llm_response.usage stayed unset (None). Plugins accessing .input_tokens on it would crash with AttributeError. Always assign llm_response.usage — extract from completion.usage if present, otherwise fall back to a zeroed TokenUsage(). Closes #8605 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: renchonghan --- .../core/provider/sources/openai_source.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 438d7aebf..e1aa08542 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -458,12 +458,20 @@ class ProviderOpenAIOfficial(Provider): tool_calls = msg.get("tool_calls") reasoning_content = msg.get("reasoning_content") - if _is_empty(content) and not tool_calls and not reasoning_content: - logger.warning(f"过滤第 {idx} 条空 assistant 消息 (无工具调用)") - continue + if _is_empty(content) and not tool_calls: + if not reasoning_content: + # 三者全空,真正的垃圾消息,丢弃 + logger.debug(f"过滤第 {idx} 条空 assistant 消息 (无 content | tool_calls | reasoning_content)") + continue + else: + # ⭐ 有 reasoning_content 但没有 content 和 tool_calls + # 不能丢(推理模型需要 reasoning 历史) + # 但 API 要求 content 或 tool_calls 至少有一个 + # → 设空字符串占位,满足校验 + msg["content"] = "" - if _is_empty(content) and tool_calls: - msg["content"] = None + elif _is_empty(content) and tool_calls: + msg["content"] = None # 有 tool_calls,按 OpenAI 规范 cleaned.append(msg) @@ -872,8 +880,11 @@ class ProviderOpenAIOfficial(Provider): llm_response.raw_completion = completion llm_response.id = completion.id - if completion.usage: - llm_response.usage = self._extract_usage(completion.usage) + llm_response.usage = ( + self._extract_usage(completion.usage) + if completion.usage + else TokenUsage() + ) return llm_response