fix: align OpenAI http_client with SDK httpx (#7773)

* fix: align OpenAI http_client with SDK httpx

* fix: narrow openai httpx import fallback
This commit is contained in:
bugkeep
2026-04-30 10:53:34 +08:00
committed by GitHub
parent 71b6349b6a
commit 938c241799
3 changed files with 81 additions and 5 deletions

View File

@@ -441,7 +441,14 @@ class ProviderOpenAIOfficial(Provider):
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 = 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:
super().__init__(provider_config, provider_settings)

View File

@@ -1,6 +1,7 @@
"""Network error handling utilities for providers."""
import ssl
from typing import Any
import httpx
@@ -90,6 +91,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.
@@ -104,8 +106,11 @@ def create_proxy_client(
provider_label: The provider name for log prefix (e.g., "OpenAI", "Gemini")
proxy: The proxy address (e.g., "http://127.0.0.1:7890"), or None/empty
headers: Optional custom headers to include in every request
verify: Optional override for TLS verification. Defaults to the hybrid
SSL context (system store + certifi) when not provided.
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 hybrid SSL context (system store + certifi); the proxy is applied only if one is provided.
@@ -113,5 +118,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)

View File

@@ -1,3 +1,4 @@
import builtins
from types import SimpleNamespace
import pytest
@@ -5,6 +6,7 @@ from openai.types.chat.chat_completion import ChatCompletion
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 +54,66 @@ 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
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(