From 648d80c9db83d477bb678309fd1fbc23019005cb Mon Sep 17 00:00:00 2001 From: dourylee Date: Tue, 21 Jul 2026 10:51:37 +0800 Subject: [PATCH 1/2] fix(openai): convert content array to string and fill empty content Fix 422 Input Validation Error when connecting MindIE and other third-party LLM services: 1. Convert messages.content array format to plain text string 2. Fill empty content with placeholder to avoid input check failure Adapt for MindIE LLM inference framework and Qwen3-30B-A3B-w8a8 model. --- astrbot/core/provider/sources/openai_source.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index b1594d608a..906b94c5d7 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -535,6 +535,15 @@ async def _query( *, request_max_retries: int | None = None, ) -> LLMResponse: + # 修复第三方LLM服务(MindIE等)不支持content数组、空content导致422校验失败 + for msg in payloads.get("messages", []): + if isinstance(msg.get("content"), list): + # 拼接数组内所有文本内容 + texts = [block.get("text", "") for block in msg["content"] if block.get("type") == "text"] + msg["content"] = " ".join(texts) if texts else " " + # 空内容填充占位符,避免输入校验失败 + if not msg.get("content"): + msg["content"] = " " if tools: model = payloads.get("model", "").lower() omit_empty_param_field = "gemini" in model @@ -593,6 +602,15 @@ async def _query_stream( *, request_max_retries: int | None = None, ) -> AsyncGenerator[LLMResponse, None]: + # 修复第三方LLM服务(MindIE等)不支持content数组、空content导致422校验失败 + for msg in payloads.get("messages", []): + if isinstance(msg.get("content"), list): + # 拼接数组内所有文本内容 + texts = [block.get("text", "") for block in msg["content"] if block.get("type") == "text"] + msg["content"] = " ".join(texts) if texts else " " + # 空内容填充占位符,避免输入校验失败 + if not msg.get("content"): + msg["content"] = " " """流式查询API,逐步返回结果""" if tools: model = payloads.get("model", "").lower() From f3cb8796403e2bc05c8afaebb4ae8263073df96a Mon Sep 17 00:00:00 2001 From: dourylee Date: Tue, 21 Jul 2026 11:21:08 +0800 Subject: [PATCH 2/2] refactor: extract shared message adapt helper, add type guard & conditional enable 1 Extract duplicated content convert logic into _adapt_messages_for_third_party shared method 2 Add full isinstance type check to avoid AttributeError 3 Define EMPTY_CONTENT_PLACEHOLDER constant instead magic space 4 Only enable format adapt for non-official OpenAI endpoints, keep native multi-modal support Fix MindIE 422 validation error without breaking original OpenAI functionality --- .../core/provider/sources/openai_source.py | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 906b94c5d7..4ef5403b86 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -50,6 +50,28 @@ ) class ProviderOpenAIOfficial(Provider): _ERROR_TEXT_CANDIDATE_MAX_CHARS = 4096 + # 空内容占位常量,方便统一修改 + EMPTY_CONTENT_PLACEHOLDER = " " + + # 新增:统一转换消息格式,带完整类型防御 + def _adapt_messages_for_third_party(self, payloads: dict) -> None: + messages = payloads.get("messages", []) + for msg in messages: + # 过滤非字典消息,防止 .get 报错 + if not isinstance(msg, dict): + continue + content = msg.get("content") + # 处理多模态数组content + if isinstance(content, list): + text_parts = [] + for block in content: + # 仅处理标准text块,跳过图片/音频等 + if isinstance(block, dict) and block.get("type") == "text": + text_parts.append(block.get("text", "")) + msg["content"] = " ".join(text_parts) if text_parts else self.EMPTY_CONTENT_PLACEHOLDER + # 空内容填充占位符 + if not content: + msg["content"] = self.EMPTY_CONTENT_PLACEHOLDER @classmethod def _truncate_error_text_candidate(cls, text: str) -> str: @@ -535,15 +557,11 @@ async def _query( *, request_max_retries: int | None = None, ) -> LLMResponse: - # 修复第三方LLM服务(MindIE等)不支持content数组、空content导致422校验失败 - for msg in payloads.get("messages", []): - if isinstance(msg.get("content"), list): - # 拼接数组内所有文本内容 - texts = [block.get("text", "") for block in msg["content"] if block.get("type") == "text"] - msg["content"] = " ".join(texts) if texts else " " - # 空内容填充占位符,避免输入校验失败 - if not msg.get("content"): - msg["content"] = " " + # 仅非原生OpenAI服务执行格式适配(MindIE、国产推理等) + base_url = self.client.base_url.host or "" + if "openai.com" not in base_url: + self._adapt_messages_for_third_party(payloads) + if tools: model = payloads.get("model", "").lower() omit_empty_param_field = "gemini" in model @@ -602,15 +620,11 @@ async def _query_stream( *, request_max_retries: int | None = None, ) -> AsyncGenerator[LLMResponse, None]: - # 修复第三方LLM服务(MindIE等)不支持content数组、空content导致422校验失败 - for msg in payloads.get("messages", []): - if isinstance(msg.get("content"), list): - # 拼接数组内所有文本内容 - texts = [block.get("text", "") for block in msg["content"] if block.get("type") == "text"] - msg["content"] = " ".join(texts) if texts else " " - # 空内容填充占位符,避免输入校验失败 - if not msg.get("content"): - msg["content"] = " " + # 仅非原生OpenAI服务执行格式适配(MindIE、国产推理等) + base_url = self.client.base_url.host or "" + if "openai.com" not in base_url: + self._adapt_messages_for_third_party(payloads) + """流式查询API,逐步返回结果""" if tools: model = payloads.get("model", "").lower()