Skip to content

Commit ad51695

Browse files
authored
fix(provider): force Gemini chat client to use managed httpx client (#8112)
When both aiohttp and httpx are installed, google-genai prefers aiohttp as the async HTTP backend. In error response paths, the aiohttp backend returns raw aiohttp.ClientResponse objects that google-genai cannot handle, masking real API errors with: Unsupported response type: <class 'aiohttp.client_reqrep.ClientResponse'> This fix explicitly creates an httpx.AsyncClient and passes it via HttpOptions.httpx_async_client, ensuring the chat provider always uses the httpx backend. The managed client is closed in terminate(). - Preserve HTTP_PROXY/HTTPS_PROXY support via trust_env=True. - Preserve provider-level proxy via httpx.AsyncClient(proxy=...). - Avoid logging full proxy URLs for security. Fixes #7564
1 parent c9182c2 commit ad51695

1 file changed

Lines changed: 51 additions & 4 deletions

File tree

astrbot/core/provider/sources/gemini_source.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Literal, cast
1010
from urllib.parse import urlparse
1111

12+
import httpx
1213
from google import genai
1314
from google.genai import types
1415
from google.genai.errors import APIError
@@ -75,6 +76,8 @@ def __init__(
7576
if self.api_base and self.api_base.endswith("/"):
7677
self.api_base = self.api_base[:-1]
7778

79+
self._http_client: httpx.AsyncClient | None = None
80+
self._stale_http_clients: list[httpx.AsyncClient] = []
7881
self._init_client()
7982
self.set_model(provider_config.get("model", "unknown"))
8083
self._init_safety_settings()
@@ -86,9 +89,29 @@ def _init_client(self) -> None:
8689
base_url=self.api_base,
8790
timeout=self.timeout * 1000, # 毫秒
8891
)
92+
93+
# 强制使用 httpx 作为异步 HTTP 后端,避免 aiohttp 响应类型兼容问题 (#7564)
94+
# httpx.AsyncClient 的 timeout 单位为秒(与 HttpOptions 的毫秒不同)
95+
async_client_kwargs: dict = {
96+
"base_url": self.api_base,
97+
"timeout": self.timeout,
98+
}
8999
if proxy:
90-
http_options.async_client_args = {"proxy": proxy}
91-
logger.info(f"[Gemini] 使用代理: {proxy}")
100+
async_client_kwargs["proxy"] = proxy
101+
async_client_kwargs["trust_env"] = False
102+
logger.info("[Gemini] 使用代理")
103+
else:
104+
async_client_kwargs["trust_env"] = True
105+
106+
# Track the previous client so it can be closed in terminate() instead
107+
# of leaking when _init_client is called again (e.g. via set_key).
108+
# Only the most recent stale client is kept to avoid unbounded growth.
109+
if self._http_client is not None:
110+
self._stale_http_clients = [self._http_client]
111+
112+
self._http_client = httpx.AsyncClient(**async_client_kwargs)
113+
http_options.httpx_async_client = self._http_client
114+
92115
self.client = genai.Client(
93116
api_key=self.chosen_api_key,
94117
http_options=http_options,
@@ -1067,6 +1090,30 @@ async def encode_image_bs64(self, image_url: str) -> str:
10671090
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
10681091
return "data:image/jpeg;base64," + image_bs64
10691092

1093+
async def _close_httpx_client(self, client: httpx.AsyncClient | None) -> None:
1094+
"""Safely close an httpx.AsyncClient, swallowing errors for idempotency."""
1095+
if client is None:
1096+
return
1097+
try:
1098+
await client.aclose()
1099+
except Exception as e:
1100+
# Idempotent: ignore errors from already-closed or broken clients,
1101+
# but log at debug to aid diagnosing unexpected shutdown issues.
1102+
logger.debug(f"[Gemini] Ignored error while closing httpx client: {e}")
1103+
10701104
async def terminate(self) -> None:
1071-
if self.client:
1072-
await self.client.aclose()
1105+
# Close the active Gemini client (external httpx client is managed
1106+
# separately so genai.Client.aclose skips it).
1107+
if self.client is not None:
1108+
try:
1109+
await self.client.aclose()
1110+
except Exception:
1111+
pass
1112+
self.client = None
1113+
1114+
# Close all tracked httpx clients (stale + current).
1115+
for client in self._stale_http_clients:
1116+
await self._close_httpx_client(client)
1117+
self._stale_http_clients.clear()
1118+
await self._close_httpx_client(self._http_client)
1119+
self._http_client = None

0 commit comments

Comments
 (0)