Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions astrbot/core/pipeline/process_stage/method/star_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ async def process(
handlers_parsed_params = {}

for handler in activated_handlers:
if event.is_stopped():
break
params = handlers_parsed_params.get(handler.handler_full_name, {})
md = star_map.get(handler.handler_module_path)
if not md:
Expand All @@ -46,6 +48,8 @@ async def process(
wrapper = call_handler(event, handler.handler, **params)
async for ret in wrapper:
yield ret
if event.is_stopped():
break
event.clear_result() # 清除上一个 handler 的结果
except Exception as e:
traceback_text = traceback.format_exc()
Expand Down
9 changes: 8 additions & 1 deletion astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 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)
Expand Down
15 changes: 11 additions & 4 deletions astrbot/core/utils/network_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Network error handling utilities for providers."""

import ssl
from typing import Any

import httpx

Expand Down Expand Up @@ -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.

Expand All @@ -104,14 +106,19 @@ 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.
"""
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)
62 changes: 62 additions & 0 deletions tests/test_openai_source.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import builtins
from types import SimpleNamespace

import pytest
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
Expand Down Expand Up @@ -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(
Expand Down
Loading