Skip to content

Commit 08392c9

Browse files
hjdhnxTaoiszouyonghe
authored
fix: 修复了国内配置一些模型不可用问题 (#7685)
* fix: 修复了国内配置一些模型不可用问题 1. 常见的openai和anthropic协议,如 智谱的codingpan https://open.bigmodel.cn/api/coding/paas/v4 2. 新出的一些没有模型列表的自定义模型提供商,如科大讯飞 https://maas-coding-api.cn-huabei-1.xf-yun.com/v2 * feat: 提高代码复用性 * fix(network): reuse shared SSL context * test(network): cover proxy and header forwarding * fix(network): support verify overrides --------- Co-authored-by: Taois <taoist.han@vertechs.com> Co-authored-by: 邹永赫 <1259085392@qq.com>
1 parent 406bb6c commit 08392c9

4 files changed

Lines changed: 78 additions & 14 deletions

File tree

astrbot/core/provider/sources/anthropic_source.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from astrbot.core.provider.func_tool_manager import ToolSet
1919
from astrbot.core.utils.io import download_image_by_url
2020
from astrbot.core.utils.network_utils import (
21+
create_proxy_client,
2122
is_connection_error,
2223
log_connection_failure,
2324
)
@@ -106,15 +107,13 @@ def _init_api_key(self, provider_config: dict) -> None:
106107
http_client=self._create_http_client(provider_config),
107108
)
108109

109-
def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None:
110-
"""创建带代理的 HTTP 客户端"""
111-
proxy = provider_config.get("proxy", "")
112-
if proxy:
113-
logger.info(f"[Anthropic] 使用代理: {proxy}")
114-
return httpx.AsyncClient(proxy=proxy, headers=self.custom_headers)
115-
if self.custom_headers:
116-
return httpx.AsyncClient(headers=self.custom_headers)
117-
return None
110+
def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient:
111+
"""创建带代理的 HTTP 客户端,使用系统 SSL 证书"""
112+
return create_proxy_client(
113+
"Anthropic",
114+
provider_config.get("proxy", ""),
115+
headers=self.custom_headers,
116+
)
118117

119118
def _apply_thinking_config(self, payloads: dict) -> None:
120119
thinking_type = self.thinking_config.get("type", "")

astrbot/core/provider/sources/openai_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ async def _fallback_to_text_only_and_retry(
438438
image_fallback_used,
439439
)
440440

441-
def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None:
441+
def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient:
442442
"""创建带代理的 HTTP 客户端"""
443443
proxy = provider_config.get("proxy", "")
444444
return create_proxy_client("OpenAI", proxy)

astrbot/core/utils/network_utils.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
"""Network error handling utilities for providers."""
22

3+
import ssl
4+
35
import httpx
46

57
from astrbot import logger
68

9+
_SYSTEM_SSL_CTX = ssl.create_default_context()
10+
711

812
def is_connection_error(exc: BaseException) -> bool:
913
"""Check if an exception is a connection/network related error.
@@ -83,20 +87,30 @@ def log_connection_failure(
8387
def create_proxy_client(
8488
provider_label: str,
8589
proxy: str | None = None,
86-
) -> httpx.AsyncClient | None:
90+
headers: dict[str, str] | None = None,
91+
verify: ssl.SSLContext | str | bool | None = None,
92+
) -> httpx.AsyncClient:
8793
"""Create an httpx AsyncClient with proxy configuration if provided.
8894
95+
Uses the system SSL certificate store instead of certifi, which avoids
96+
SSL verification failures for endpoints whose CA chain is not in certifi
97+
but is trusted by the operating system.
98+
8999
Note: The caller is responsible for closing the client when done.
90100
Consider using the client as a context manager or calling aclose() explicitly.
91101
92102
Args:
93103
provider_label: The provider name for log prefix (e.g., "OpenAI", "Gemini")
94104
proxy: The proxy address (e.g., "http://127.0.0.1:7890"), or None/empty
105+
headers: Optional custom headers to include in every request
106+
verify: Optional override for TLS verification. Defaults to the shared
107+
system SSL context when not provided.
95108
96109
Returns:
97-
An httpx.AsyncClient configured with the proxy, or None if no proxy
110+
An httpx.AsyncClient created with the shared system SSL context; the proxy is applied only if one is provided.
98111
"""
112+
resolved_verify = _SYSTEM_SSL_CTX if verify is None else verify
99113
if proxy:
100114
logger.info(f"[{provider_label}] 使用代理: {proxy}")
101-
return httpx.AsyncClient(proxy=proxy)
102-
return None
115+
return httpx.AsyncClient(proxy=proxy, verify=resolved_verify, headers=headers)
116+
return httpx.AsyncClient(verify=resolved_verify, headers=headers)

tests/unit/test_network_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import ssl
2+
3+
import pytest
4+
5+
from astrbot.core.utils import network_utils
6+
7+
8+
def test_create_proxy_client_reuses_shared_ssl_context(
9+
monkeypatch: pytest.MonkeyPatch,
10+
):
11+
captured_calls: list[dict] = []
12+
headers = {"X-Test-Header": "value"}
13+
14+
class _FakeAsyncClient:
15+
def __init__(self, **kwargs):
16+
captured_calls.append(kwargs)
17+
18+
monkeypatch.setattr(network_utils.httpx, "AsyncClient", _FakeAsyncClient)
19+
20+
network_utils.create_proxy_client("OpenAI")
21+
network_utils.create_proxy_client("OpenAI", proxy="http://127.0.0.1:7890")
22+
network_utils.create_proxy_client("OpenAI", headers=headers)
23+
network_utils.create_proxy_client("OpenAI", proxy="")
24+
25+
assert len(captured_calls) == 4
26+
assert "proxy" not in captured_calls[0]
27+
assert captured_calls[1]["proxy"] == "http://127.0.0.1:7890"
28+
assert captured_calls[2]["headers"] is headers
29+
assert "proxy" not in captured_calls[3]
30+
assert isinstance(captured_calls[0]["verify"], ssl.SSLContext)
31+
assert captured_calls[0]["verify"] is captured_calls[1]["verify"]
32+
assert captured_calls[1]["verify"] is captured_calls[2]["verify"]
33+
assert captured_calls[2]["verify"] is captured_calls[3]["verify"]
34+
35+
36+
def test_create_proxy_client_allows_verify_override(
37+
monkeypatch: pytest.MonkeyPatch,
38+
):
39+
captured_calls: list[dict] = []
40+
custom_verify = ssl.create_default_context()
41+
42+
class _FakeAsyncClient:
43+
def __init__(self, **kwargs):
44+
captured_calls.append(kwargs)
45+
46+
monkeypatch.setattr(network_utils.httpx, "AsyncClient", _FakeAsyncClient)
47+
48+
network_utils.create_proxy_client("OpenAI", verify=custom_verify)
49+
50+
assert len(captured_calls) == 1
51+
assert captured_calls[0]["verify"] is custom_verify

0 commit comments

Comments
 (0)