From 85c3f965b5cae3267d72d6ab673c2ddf9b3542af Mon Sep 17 00:00:00 2001 From: Lex_q <154044203+Tz-WIND@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:38:25 +0800 Subject: [PATCH 1/3] Refactor OpenAI client initialization and management --- .../core/provider/sources/openai_source.py | 91 ++++++++++++++++--- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index b19f3460dd..e096e2cf6d 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -459,34 +459,77 @@ def __init__(self, provider_config, provider_settings) -> None: for key in self.custom_headers: self.custom_headers[key] = str(self.custom_headers[key]) - if "api_version" in provider_config: + self.client = self._create_openai_client() + + self.default_params = inspect.signature( + self.client.chat.completions.create, + ).parameters.keys() + + model = provider_config.get("model", "unknown") + self.set_model(model) + + self.reasoning_key = "reasoning_content" + + def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None: + """创建带代理的 HTTP 客户端""" + proxy = provider_config.get("proxy", "") + + return create_proxy_client("OpenAI", proxy) + + def _create_openai_client(self) -> AsyncOpenAI | AsyncAzureOpenAI: + """创建 OpenAI/Azure 客户端实例,将初始化逻辑解耦以便复用。""" + if "api_version" in self.provider_config: # Using Azure OpenAI API - self.client = AsyncAzureOpenAI( + return AsyncAzureOpenAI( api_key=self.chosen_api_key, - api_version=provider_config.get("api_version", None), + api_version=self.provider_config.get("api_version", None), default_headers=self.custom_headers, - base_url=provider_config.get("api_base", ""), + base_url=self.provider_config.get("api_base", ""), timeout=self.timeout, - http_client=self._create_http_client(provider_config), + http_client=self._create_http_client(self.provider_config), ) else: # Using OpenAI Official API - self.client = AsyncOpenAI( + return AsyncOpenAI( api_key=self.chosen_api_key, - base_url=provider_config.get("api_base", None), + base_url=self.provider_config.get("api_base", None), default_headers=self.custom_headers, timeout=self.timeout, - http_client=self._create_http_client(provider_config), + http_client=self._create_http_client(self.provider_config), ) - self.default_params = inspect.signature( - self.client.chat.completions.create, - ).parameters.keys() + def _is_underlying_client_closed(self) -> bool: + """集中处理对 openai SDK 私有属性的访问,便于未来替换为公开 API。 - model = provider_config.get("model", "unknown") - self.set_model(model) + 注意:此处直接访问了 openai 库的私有属性 `_client`, + 依赖其内部实现(httpx.AsyncClient 实例暴露的 is_closed 属性)。 + 若 openai 库未来版本调整了内部结构,此处可能失效。 + 目前 openai SDK 尚未提供检查底层连接是否已关闭的公开 API。 + 若未来 SDK 提供了类似 self.client.is_closed() 的公开方法, + 应及时将此处替换为对应的公开接口。 - self.reasoning_key = "reasoning_content" + 当检测逻辑因 SDK 内部结构变更而抛出 AttributeError 时,会: + 1. 记录 warning 日志,提示可能的 SDK 变更; + 2. 保守地视为"已关闭",触发后续的 client 重建逻辑。 + """ + try: + return bool(self.client and self.client._client.is_closed) + except AttributeError: + logger.warning( + "无法检测 OpenAI client 是否已关闭," + "可能是 SDK 内部结构变更导致;" + "将保守视为已关闭并触发 client 重建。" + ) + return True + + def _ensure_client(self) -> None: + """确保 client 可用。如果 client 为 None 或底层连接已关闭,则重新创建。""" + if self.client is None or self._is_underlying_client_closed(): + logger.warning("检测到 OpenAI client 已关闭或未初始化,正在重新创建...") + self.client = self._create_openai_client() + self.default_params = inspect.signature( + self.client.chat.completions.create, + ).parameters.keys() def _ollama_disable_thinking_enabled(self) -> bool: value = self.provider_config.get("ollama_disable_thinking", False) @@ -509,6 +552,7 @@ def _apply_provider_specific_extra_body_overrides( extra_body["reasoning_effort"] = "none" async def get_models(self): + self._ensure_client() try: models_str = [] models = await self.client.models.list() @@ -520,6 +564,7 @@ async def get_models(self): raise Exception(f"获取模型列表失败:{e}") async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: + self._ensure_client() if tools: model = payloads.get("model", "").lower() omit_empty_param_field = "gemini" in model @@ -592,6 +637,7 @@ async def _query_stream( tools: ToolSet | None, ) -> AsyncGenerator[LLMResponse, None]: """流式查询API,逐步返回结果""" + self._ensure_client() if tools: model = payloads.get("model", "").lower() omit_empty_param_field = "gemini" in model @@ -1145,6 +1191,7 @@ async def text_chat( retry_cnt = 0 for retry_cnt in range(max_retries): try: + self._ensure_client() self.client.api_key = chosen_key llm_response = await self._query(payloads, func_tool) break @@ -1216,6 +1263,7 @@ async def text_chat_stream( retry_cnt = 0 for retry_cnt in range(max_retries): try: + self._ensure_client() self.client.api_key = chosen_key async for response in self._query_stream(payloads, func_tool): yield response @@ -1270,12 +1318,14 @@ async def _remove_image_from_context(self, contexts: list): return new_contexts def get_current_key(self) -> str: + self._ensure_client() return self.client.api_key def get_keys(self) -> list[str]: return self.api_keys def set_key(self, key) -> None: + self._ensure_client() self.client.api_key = key async def assemble_context( @@ -1355,5 +1405,16 @@ async def encode_image_bs64(self, image_url: str) -> str: return image_data async def terminate(self): + """关闭 client 并将引用置为 None。 + + 通过 try/finally 确保即使 close() 抛出异常, + self.client 也会被清空,避免配置重载(reload)期间 + 复用已关闭的 client 导致 APIConnectionError。 + """ if self.client: - await self.client.close() + try: + await self.client.close() + except Exception as e: + logger.warning(f"关闭 OpenAI client 时出错: {e}") + finally: + self.client = None From 394150513254b1560ba350f119ada82c30536a5a Mon Sep 17 00:00:00 2001 From: Lex_q <154044203+Tz-WIND@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:51:18 +0800 Subject: [PATCH 2/3] Refactor API key handling in OpenAI source --- astrbot/core/provider/sources/openai_source.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index e096e2cf6d..7ee5b67ee4 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -469,13 +469,13 @@ def __init__(self, provider_config, provider_settings) -> None: self.set_model(model) self.reasoning_key = "reasoning_content" - + def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None: """创建带代理的 HTTP 客户端""" proxy = provider_config.get("proxy", "") - + return create_proxy_client("OpenAI", proxy) - + def _create_openai_client(self) -> AsyncOpenAI | AsyncAzureOpenAI: """创建 OpenAI/Azure 客户端实例,将初始化逻辑解耦以便复用。""" if "api_version" in self.provider_config: @@ -1192,6 +1192,7 @@ async def text_chat( for retry_cnt in range(max_retries): try: self._ensure_client() + self.chosen_api_key = chosen_key self.client.api_key = chosen_key llm_response = await self._query(payloads, func_tool) break @@ -1264,6 +1265,7 @@ async def text_chat_stream( for retry_cnt in range(max_retries): try: self._ensure_client() + self.chosen_api_key = chosen_key self.client.api_key = chosen_key async for response in self._query_stream(payloads, func_tool): yield response @@ -1325,6 +1327,7 @@ def get_keys(self) -> list[str]: return self.api_keys def set_key(self, key) -> None: + self.chosen_api_key = key self._ensure_client() self.client.api_key = key From 06efd08a62143859a764590f6eaf2ce40678112e Mon Sep 17 00:00:00 2001 From: Lex_q <154044203+Tz-WIND@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:10:52 +0800 Subject: [PATCH 3/3] Refactor OpenAI client initialization and management Refactor OpenAI client handling to improve initialization and ensure client is alive before API calls. --- .../core/provider/sources/openai_source.py | 72 +++++++------------ 1 file changed, 24 insertions(+), 48 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 7ee5b67ee4..3285c0de03 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -438,11 +438,6 @@ async def _fallback_to_text_only_and_retry( image_fallback_used, ) - def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None: - """创建带代理的 HTTP 客户端""" - proxy = provider_config.get("proxy", "") - return create_proxy_client("OpenAI", proxy) - def __init__(self, provider_config, provider_settings) -> None: super().__init__(provider_config, provider_settings) self.chosen_api_key = None @@ -450,6 +445,8 @@ def __init__(self, provider_config, provider_settings) -> None: self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None self.timeout = provider_config.get("timeout", 120) self.custom_headers = provider_config.get("custom_headers", {}) + self.client: AsyncOpenAI | AsyncAzureOpenAI | None = None + self._client_alive = False if isinstance(self.timeout, str): self.timeout = int(self.timeout) @@ -460,6 +457,7 @@ def __init__(self, provider_config, provider_settings) -> None: self.custom_headers[key] = str(self.custom_headers[key]) self.client = self._create_openai_client() + self._client_alive = True self.default_params = inspect.signature( self.client.chat.completions.create, @@ -476,12 +474,16 @@ def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None return create_proxy_client("OpenAI", proxy) - def _create_openai_client(self) -> AsyncOpenAI | AsyncAzureOpenAI: + def _create_openai_client( + self, + api_key: str | None = None, + ) -> AsyncOpenAI | AsyncAzureOpenAI: """创建 OpenAI/Azure 客户端实例,将初始化逻辑解耦以便复用。""" + api_key = api_key or self.chosen_api_key if "api_version" in self.provider_config: # Using Azure OpenAI API return AsyncAzureOpenAI( - api_key=self.chosen_api_key, + api_key=api_key, api_version=self.provider_config.get("api_version", None), default_headers=self.custom_headers, base_url=self.provider_config.get("api_base", ""), @@ -491,42 +493,19 @@ def _create_openai_client(self) -> AsyncOpenAI | AsyncAzureOpenAI: else: # Using OpenAI Official API return AsyncOpenAI( - api_key=self.chosen_api_key, + api_key=api_key, base_url=self.provider_config.get("api_base", None), default_headers=self.custom_headers, timeout=self.timeout, http_client=self._create_http_client(self.provider_config), ) - def _is_underlying_client_closed(self) -> bool: - """集中处理对 openai SDK 私有属性的访问,便于未来替换为公开 API。 - - 注意:此处直接访问了 openai 库的私有属性 `_client`, - 依赖其内部实现(httpx.AsyncClient 实例暴露的 is_closed 属性)。 - 若 openai 库未来版本调整了内部结构,此处可能失效。 - 目前 openai SDK 尚未提供检查底层连接是否已关闭的公开 API。 - 若未来 SDK 提供了类似 self.client.is_closed() 的公开方法, - 应及时将此处替换为对应的公开接口。 - - 当检测逻辑因 SDK 内部结构变更而抛出 AttributeError 时,会: - 1. 记录 warning 日志,提示可能的 SDK 变更; - 2. 保守地视为"已关闭",触发后续的 client 重建逻辑。 - """ - try: - return bool(self.client and self.client._client.is_closed) - except AttributeError: - logger.warning( - "无法检测 OpenAI client 是否已关闭," - "可能是 SDK 内部结构变更导致;" - "将保守视为已关闭并触发 client 重建。" - ) - return True - def _ensure_client(self) -> None: - """确保 client 可用。如果 client 为 None 或底层连接已关闭,则重新创建。""" - if self.client is None or self._is_underlying_client_closed(): + """确保 client 可用,仅在真实 API 调用前按需重建。""" + if self.client is None or not self._client_alive: logger.warning("检测到 OpenAI client 已关闭或未初始化,正在重新创建...") self.client = self._create_openai_client() + self._client_alive = True self.default_params = inspect.signature( self.client.chat.completions.create, ).parameters.keys() @@ -1191,9 +1170,10 @@ async def text_chat( retry_cnt = 0 for retry_cnt in range(max_retries): try: - self._ensure_client() self.chosen_api_key = chosen_key - self.client.api_key = chosen_key + self._ensure_client() + if self.client is not None: + self.client.api_key = chosen_key llm_response = await self._query(payloads, func_tool) break except Exception as e: @@ -1264,9 +1244,10 @@ async def text_chat_stream( retry_cnt = 0 for retry_cnt in range(max_retries): try: - self._ensure_client() self.chosen_api_key = chosen_key - self.client.api_key = chosen_key + self._ensure_client() + if self.client is not None: + self.client.api_key = chosen_key async for response in self._query_stream(payloads, func_tool): yield response break @@ -1320,16 +1301,15 @@ async def _remove_image_from_context(self, contexts: list): return new_contexts def get_current_key(self) -> str: - self._ensure_client() - return self.client.api_key + return self.chosen_api_key def get_keys(self) -> list[str]: return self.api_keys def set_key(self, key) -> None: self.chosen_api_key = key - self._ensure_client() - self.client.api_key = key + if self.client is not None: + self.client.api_key = key async def assemble_context( self, @@ -1408,12 +1388,7 @@ async def encode_image_bs64(self, image_url: str) -> str: return image_data async def terminate(self): - """关闭 client 并将引用置为 None。 - - 通过 try/finally 确保即使 close() 抛出异常, - self.client 也会被清空,避免配置重载(reload)期间 - 复用已关闭的 client 导致 APIConnectionError。 - """ + """关闭 client 并将引用置为 None,确保后续仅在真实调用时重建。""" if self.client: try: await self.client.close() @@ -1421,3 +1396,4 @@ async def terminate(self): logger.warning(f"关闭 OpenAI client 时出错: {e}") finally: self.client = None + self._client_alive = False