diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index 9422d6317a..3adcddc077 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -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: @@ -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() diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 512e47233a..b8ee502034 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 = 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) diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 0bf6b820e0..4cc48e0501 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 @@ -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) diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index ec5e79f492..950e2ea162 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 @@ -5,6 +6,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 +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(