From a0d40f16fe4d8faf50c5ec5d30ba99b5c786dc35 Mon Sep 17 00:00:00 2001 From: bugkeep <1921817430@qq.com> Date: Fri, 24 Apr 2026 20:51:08 +0800 Subject: [PATCH 1/2] fix: align OpenAI http_client with SDK httpx --- .../core/provider/sources/openai_source.py | 9 +++++- astrbot/core/utils/network_utils.py | 11 ++++++-- tests/test_openai_source.py | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index f2d9474906..bb6ae4825b 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -441,7 +441,14 @@ async def _fallback_to_text_only_and_retry( def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient: """创建带代理的 HTTP 客户端""" proxy = provider_config.get("proxy", "") - return create_proxy_client("OpenAI", proxy) + httpx_module: Any = httpx + try: + from openai import _base_client as openai_base_client + + httpx_module = openai_base_client.httpx + except Exception: + httpx_module = httpx + return create_proxy_client("OpenAI", proxy, httpx_module=httpx_module) def __init__(self, provider_config, provider_settings) -> None: super().__init__(provider_config, provider_settings) diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 047529396e..028dd8e3e4 100644 --- a/astrbot/core/utils/network_utils.py +++ b/astrbot/core/utils/network_utils.py @@ -1,6 +1,7 @@ """Network error handling utilities for providers.""" import ssl +from typing import Any import httpx @@ -89,6 +90,7 @@ def create_proxy_client( proxy: str | None = None, headers: dict[str, str] | None = None, verify: ssl.SSLContext | str | bool | None = None, + httpx_module: Any = httpx, ) -> httpx.AsyncClient: """Create an httpx AsyncClient with proxy configuration if provided. @@ -105,6 +107,9 @@ def create_proxy_client( headers: Optional custom headers to include in every request verify: Optional override for TLS verification. Defaults to the shared system SSL context when not provided. + httpx_module: Optional httpx module to construct AsyncClient from. This is + useful when a provider SDK performs isinstance checks against its own + httpx import. Returns: An httpx.AsyncClient created with the shared system SSL context; the proxy is applied only if one is provided. @@ -112,5 +117,7 @@ def create_proxy_client( resolved_verify = _SYSTEM_SSL_CTX if verify is None else verify if proxy: logger.info(f"[{provider_label}] 使用代理: {proxy}") - return httpx.AsyncClient(proxy=proxy, verify=resolved_verify, headers=headers) - return httpx.AsyncClient(verify=resolved_verify, headers=headers) + return httpx_module.AsyncClient( + proxy=proxy, verify=resolved_verify, headers=headers + ) + return httpx_module.AsyncClient(verify=resolved_verify, headers=headers) diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index 83e18137c4..4413fa8a34 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -5,6 +5,7 @@ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from PIL import Image as PILImage +import astrbot.core.provider.sources.openai_source as openai_source_module from astrbot.core.exceptions import EmptyModelOutputError from astrbot.core.provider.sources.groq_source import ProviderGroq from astrbot.core.provider.sources.openai_source import ProviderOpenAIOfficial @@ -52,6 +53,33 @@ def _make_groq_provider(overrides: dict | None = None) -> ProviderGroq: ) +def test_create_http_client_uses_openai_httpx_module(monkeypatch): + captured: dict[str, object] = {} + + def fake_create_proxy_client( + provider_label: str, + proxy: str | None = None, + headers: dict[str, str] | None = None, + verify=None, + httpx_module=None, + ): + captured["httpx_module"] = httpx_module + return object() + + monkeypatch.setattr( + openai_source_module, + "create_proxy_client", + fake_create_proxy_client, + ) + + provider = ProviderOpenAIOfficial.__new__(ProviderOpenAIOfficial) + provider._create_http_client({"proxy": ""}) + + from openai import _base_client as openai_base_client + + assert captured["httpx_module"] is openai_base_client.httpx + + @pytest.mark.asyncio async def test_handle_api_error_content_moderated_removes_images(): provider = _make_provider( From 2d0b5d253bcd158071bbc5b65d1758637ee074b2 Mon Sep 17 00:00:00 2001 From: bugkeep <1921817430@qq.com> Date: Sat, 25 Apr 2026 17:34:24 +0800 Subject: [PATCH 2/2] fix: narrow openai httpx import fallback --- .../core/provider/sources/openai_source.py | 6 ++-- tests/test_openai_source.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index bb6ae4825b..1797063bd1 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -445,9 +445,9 @@ def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient: try: from openai import _base_client as openai_base_client - httpx_module = openai_base_client.httpx - except Exception: - httpx_module = httpx + httpx_module = getattr(openai_base_client, "httpx", httpx) + except ImportError: + pass return create_proxy_client("OpenAI", proxy, httpx_module=httpx_module) def __init__(self, provider_config, provider_settings) -> None: diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index 4413fa8a34..45371d5bfc 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -1,3 +1,4 @@ +import builtins from types import SimpleNamespace import pytest @@ -80,6 +81,39 @@ def fake_create_proxy_client( assert captured["httpx_module"] is openai_base_client.httpx +def test_create_http_client_falls_back_to_global_httpx_module(monkeypatch): + captured: dict[str, object] = {} + + def fake_create_proxy_client( + provider_label: str, + proxy: str | None = None, + headers: dict[str, str] | None = None, + verify=None, + httpx_module=None, + ): + captured["httpx_module"] = httpx_module + return object() + + real_import = builtins.__import__ + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "openai" and fromlist: + raise ImportError("missing openai._base_client") + return real_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr( + openai_source_module, + "create_proxy_client", + fake_create_proxy_client, + ) + monkeypatch.setattr(builtins, "__import__", fake_import) + + provider = ProviderOpenAIOfficial.__new__(ProviderOpenAIOfficial) + provider._create_http_client({"proxy": ""}) + + assert captured["httpx_module"] is openai_source_module.httpx + + @pytest.mark.asyncio async def test_handle_api_error_content_moderated_removes_images(): provider = _make_provider(