Skip to content

Commit b82bac1

Browse files
whatevertogoOmX
andcommitted
Force Gemini chat provider onto managed httpx client
google-genai prefers aiohttp when aiohttp is installed, and that backend can mask Gemini API failures as Unsupported response type for aiohttp.ClientResponse. Give the Gemini chat client a managed httpx AsyncClient so chat and tool paths avoid aiohttp while preserving environment proxy support, provider proxy support, timeout configuration, and clean shutdown. Also stop logging proxy URLs or API key prefixes. Constraint: Keep the fix scoped to the Gemini chat provider reported in the issue. Rejected: Do not remove aiohttp from project dependencies or switch every Gemini provider in this PR. Directive: Use a managed httpx AsyncClient when constructing the Google Gemini chat client. Confidence: medium-high Scope-risk: low Reversibility: straightforward Tested: uv run pytest tests/test_gemini_source.py tests/test_httpx_socks_dependency.py -q Tested: uv run ruff check astrbot/core/provider/sources/gemini_source.py tests/test_gemini_source.py Tested: Live non-streaming gemini-2.5-flash call returned OK. Not-tested: Successful live gemini-2.5-pro call because the provided key returned RESOURCE_EXHAUSTED quota errors. Related: #7564 Co-authored-by: OmX <omx@oh-my-codex.local>
1 parent 39386ee commit b82bac1

2 files changed

Lines changed: 119 additions & 4 deletions

File tree

astrbot/core/provider/sources/gemini_source.py

Lines changed: 15 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
@@ -82,13 +83,21 @@ def __init__(
8283
def _init_client(self) -> None:
8384
"""初始化Gemini客户端"""
8485
proxy = self.provider_config.get("proxy", "")
86+
client_kwargs = {
87+
"timeout": self.timeout,
88+
"trust_env": True,
89+
}
90+
if proxy:
91+
client_kwargs["proxy"] = proxy
8592
http_options = types.HttpOptions(
8693
base_url=self.api_base,
8794
timeout=self.timeout * 1000, # 毫秒
8895
)
96+
# issue #7564: Force google-genai to use httpx; its aiohttp error path can mask API errors.
97+
self._httpx_async_client = httpx.AsyncClient(**client_kwargs)
98+
http_options.httpx_async_client = self._httpx_async_client
8999
if proxy:
90-
http_options.async_client_args = {"proxy": proxy}
91-
logger.info(f"[Gemini] 使用代理: {proxy}")
100+
logger.info("[Gemini] 使用代理")
92101
self.client = genai.Client(
93102
api_key=self.chosen_api_key,
94103
http_options=http_options,
@@ -117,12 +126,12 @@ async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool:
117126
if len(keys) > 0:
118127
self.set_key(random.choice(keys))
119128
logger.info(
120-
f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}...",
129+
f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试...",
121130
)
122131
await asyncio.sleep(1)
123132
return True
124133
logger.error(
125-
f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...",
134+
f"检测到 Key 异常({e.message}),且已没有可用的 Key。",
126135
)
127136
raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
128137

@@ -1070,3 +1079,5 @@ async def encode_image_bs64(self, image_url: str) -> str:
10701079
async def terminate(self) -> None:
10711080
if self.client:
10721081
await self.client.aclose()
1082+
if self._httpx_async_client:
1083+
await self._httpx_async_client.aclose()

tests/test_gemini_source.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,114 @@
1+
from types import SimpleNamespace
2+
13
import pytest
24

5+
import astrbot.core.provider.sources.gemini_source as gemini_source_module
36
from astrbot.core.exceptions import EmptyModelOutputError
47
from astrbot.core.provider.entities import LLMResponse
58
from astrbot.core.provider.sources.gemini_source import ProviderGoogleGenAI
69

710

11+
def _make_provider_config(overrides: dict | None = None) -> dict:
12+
config = {
13+
"id": "test-gemini",
14+
"type": "googlegenai_chat_completion",
15+
"model": "gemini-2.5-pro",
16+
"key": ["test-key"],
17+
"timeout": 180,
18+
"gm_safety_settings": {},
19+
}
20+
if overrides:
21+
config.update(overrides)
22+
return config
23+
24+
25+
class _FakeGeminiClient:
26+
def __init__(self):
27+
self.closed = False
28+
29+
async def aclose(self):
30+
self.closed = True
31+
32+
33+
def test_gemini_client_forces_httpx_client_and_keeps_env_proxy(monkeypatch):
34+
captured: dict[str, object] = {}
35+
httpx_client = _FakeGeminiClient()
36+
37+
def fake_httpx_client(**kwargs):
38+
captured["httpx_client_kwargs"] = kwargs
39+
return httpx_client
40+
41+
def fake_client(api_key, http_options):
42+
captured["api_key"] = api_key
43+
captured["http_options"] = http_options
44+
return SimpleNamespace(aio=SimpleNamespace())
45+
46+
monkeypatch.setenv("HTTPS_PROXY", "http://global-proxy.example:8080")
47+
monkeypatch.setattr(gemini_source_module.httpx, "AsyncClient", fake_httpx_client)
48+
monkeypatch.setattr(gemini_source_module.genai, "Client", fake_client)
49+
50+
ProviderGoogleGenAI(_make_provider_config(), {})
51+
52+
http_options = captured["http_options"]
53+
assert captured["api_key"] == "test-key"
54+
assert captured["httpx_client_kwargs"] == {"timeout": 180, "trust_env": True}
55+
assert http_options.httpx_async_client is httpx_client
56+
57+
58+
def test_gemini_client_passes_proxy_to_httpx_client_without_logging_it(monkeypatch):
59+
captured: dict[str, object] = {}
60+
httpx_client = _FakeGeminiClient()
61+
proxy = "socks5://user:secret@127.0.0.1:1080"
62+
63+
def fake_httpx_client(**kwargs):
64+
captured["httpx_client_kwargs"] = kwargs
65+
return httpx_client
66+
67+
def fake_client(api_key, http_options):
68+
captured["http_options"] = http_options
69+
return SimpleNamespace(aio=SimpleNamespace())
70+
71+
def fake_log(message):
72+
captured["log_message"] = message
73+
74+
monkeypatch.setattr(gemini_source_module.httpx, "AsyncClient", fake_httpx_client)
75+
monkeypatch.setattr(gemini_source_module.genai, "Client", fake_client)
76+
monkeypatch.setattr(gemini_source_module.logger, "info", fake_log)
77+
78+
ProviderGoogleGenAI(_make_provider_config({"proxy": proxy}), {})
79+
80+
http_options = captured["http_options"]
81+
assert captured["httpx_client_kwargs"] == {
82+
"timeout": 180,
83+
"trust_env": True,
84+
"proxy": proxy,
85+
}
86+
assert http_options.httpx_async_client is httpx_client
87+
assert "secret" not in captured["log_message"]
88+
assert proxy not in captured["log_message"]
89+
90+
91+
@pytest.mark.asyncio
92+
async def test_gemini_api_key_error_log_does_not_include_key(monkeypatch):
93+
captured: dict[str, str] = {}
94+
api_key = "sensitive-api-key-value"
95+
96+
def fake_log(message):
97+
captured["message"] = message
98+
99+
monkeypatch.setattr(gemini_source_module.logger, "error", fake_log)
100+
101+
provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI)
102+
provider.chosen_api_key = api_key
103+
error = SimpleNamespace(code=429, message="quota exceeded")
104+
105+
with pytest.raises(Exception, match="Gemini"):
106+
await provider._handle_api_error(error, [api_key])
107+
108+
assert api_key not in captured["message"]
109+
assert api_key[:12] not in captured["message"]
110+
111+
8112
def test_gemini_empty_output_raises_empty_model_output_error():
9113
llm_response = LLMResponse(role="assistant")
10114

0 commit comments

Comments
 (0)