From 086afb2804f5f0401840bce17aac72f81af4afc5 Mon Sep 17 00:00:00 2001 From: x1051445024 <1051445024@qq.com> Date: Tue, 19 May 2026 23:02:45 +0800 Subject: [PATCH 1/4] fix: handle delta=None chunks in streaming to prevent SDK to_dict() error When certain OpenAI-compatible providers (Gemini, DeepSeek, some proxies) return chunks with choice.delta=None (e.g. ContentBlockDeltaEvent), ChatCompletionStreamState._convert_initial_chunk_into_snapshot internally calls choice.delta.to_dict() at line 747, causing: 'NoneType' object has no attribute 'to_dict' Fix: 1. Skip handle_chunk when delta is None (delta=None chunks have no content contribution anyway) 2. Wrap get_final_completion in try/except to gracefully fall back to empty ChatCompletion if SDK state is corrupted Refs: openai-python#5069, openai-python#5047 --- .../core/provider/sources/openai_source.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 64e3a6645a..b64879f5b1 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -669,9 +669,14 @@ async def _query_stream( # Gemini and some OpenAI-compatible proxies omit this field if not hasattr(tc, "index") or tc.index is None: tc.index = idx - try: - state.handle_chunk(chunk) - except Exception as e: + # 跳过 delta=None 的 chunk,避免 SDK 内部 _convert_initial_chunk_into_snapshot + # 第 747 行 choice.delta.to_dict() 抛出 NoneType 错误。 + # refs: AstrBot#6689 / openai-python#5069 / #5047 + if delta is not None: + try: + state.handle_chunk(chunk) + except Exception as e: + logger.error("Saving chunk state error: " + str(e)) logger.error("Saving chunk state error: " + str(e)) # logger.debug(f"chunk delta: {delta}") # handle the content delta @@ -700,7 +705,19 @@ async def _query_stream( if _y: yield llm_response - final_completion = state.get_final_completion() + try: + final_completion = state.get_final_completion() + except Exception as e: + logger.error("get_final_completion error: " + str(e)) + # fallback: 构造空 ChatCompletion(内容已通过流式 yield 发出) + from openai.types.chat.chat_completion import ChatCompletion + final_completion = ChatCompletion( + id='', + choices=[], + created=0, + model='', + object='chat.completion', + ) llm_response = await self._parse_openai_completion(final_completion, tools) yield llm_response @@ -901,6 +918,9 @@ async def _parse_openai_completion( args = {} else: args = tool_call.function.arguments + # Some API may return None for tools with no parameters + if args is None: + args = {} args_ls.append(args) func_name_ls.append(tool_call.function.name) tool_call_ids.append(tool_call.id) From bc6ba58a563ae624024a54ece3c03f68613a6bf2 Mon Sep 17 00:00:00 2001 From: x1051445024 <1051445024@qq.com> Date: Tue, 19 May 2026 23:50:37 +0800 Subject: [PATCH 2/4] fix: resolve bugs found by Sourcery and gemini-code-assist review - Remove orphan logger.error that caused NameError on every chunk - Replace broken empty ChatCompletion fallback with clean return; streamed content already yielded, no data loss Co-authored-by: sourcery-ai[bot] Co-authored-by: gemini-code-assist[bot] --- astrbot/core/provider/sources/openai_source.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index b64879f5b1..fd9aa32aee 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -677,7 +677,6 @@ async def _query_stream( state.handle_chunk(chunk) except Exception as e: logger.error("Saving chunk state error: " + str(e)) - logger.error("Saving chunk state error: " + str(e)) # logger.debug(f"chunk delta: {delta}") # handle the content delta reasoning = self._extract_reasoning_content(chunk) @@ -1429,4 +1428,12 @@ async def encode_image_bs64(self, image_url: str) -> str: async def terminate(self): if self.client: + try: + final_completion = state.get_final_completion() + llm_response = await self._parse_openai_completion(final_completion, tools) + yield llm_response + except Exception as e: + logger.error("get_final_completion error: " + str(e)) + # 流式内容已通过 yield 发出,记录错误后正常结束即可 + return await self.client.close() From 16b2ead12e12a5fbd0f2c18da5aadaece7ba8ada Mon Sep 17 00:00:00 2001 From: x1051445024 <1051445024@qq.com> Date: Tue, 19 May 2026 23:57:01 +0800 Subject: [PATCH 3/4] fix: properly replace get_final_completion fallback in _query_stream Previous fix_pr_v3 wrongly injected code into terminate() instead. Now correctly: 1. Replace empty ChatCompletion fallback with clean return in _query_stream 2. Revert terminate() to original (await self.client.close() only) --- astrbot/core/provider/sources/openai_source.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index fd9aa32aee..69cd045b14 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -706,20 +706,12 @@ async def _query_stream( try: final_completion = state.get_final_completion() + llm_response = await self._parse_openai_completion(final_completion, tools) + yield llm_response except Exception as e: logger.error("get_final_completion error: " + str(e)) - # fallback: 构造空 ChatCompletion(内容已通过流式 yield 发出) - from openai.types.chat.chat_completion import ChatCompletion - final_completion = ChatCompletion( - id='', - choices=[], - created=0, - model='', - object='chat.completion', - ) - llm_response = await self._parse_openai_completion(final_completion, tools) - - yield llm_response + # 流式内容已通过 yield 发出,记录错误后正常结束即可 + return def _extract_reasoning_content( self, From ac6314f0ce4755b1cabb5005f15a673beba40c1a Mon Sep 17 00:00:00 2001 From: x1051445024 <1051445024@qq.com> Date: Wed, 20 May 2026 00:02:50 +0800 Subject: [PATCH 4/4] fix: revert corrupted terminate() to original Previous fix_pr_v3 injected wrong-indentation code into terminate(). --- astrbot/core/provider/sources/openai_source.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 69cd045b14..3d22cbb7da 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -1420,12 +1420,4 @@ async def encode_image_bs64(self, image_url: str) -> str: async def terminate(self): if self.client: - try: - final_completion = state.get_final_completion() - llm_response = await self._parse_openai_completion(final_completion, tools) - yield llm_response - except Exception as e: - logger.error("get_final_completion error: " + str(e)) - # 流式内容已通过 yield 发出,记录错误后正常结束即可 - return await self.client.close()