mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user