diff --git a/src/ai_client.py b/src/ai_client.py index 47c33ded..0e3e7a7a 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -1395,7 +1395,7 @@ def _ensure_gemini_client() -> None: genai = _require_warmed("google.genai") if _gemini_client is None: creds = _load_credentials() - _gemini_client = genai.Client(api_key=creds["gemini"]["api_key"]) + _gemini_client = genai.Client(api_key=creds["gemini"]["api_key"]) def _get_gemini_history_list(chat: Any | None) -> list[Any]: if not chat: return [] diff --git a/src/module_loader.py b/src/module_loader.py index f62f080f..5094111e 100644 --- a/src/module_loader.py +++ b/src/module_loader.py @@ -46,8 +46,23 @@ def _require_warmed(name: str) -> Any: In production: this is an O(1) sys.modules lookup. The 1+ second import cost is paid during startup on a bg thread, NOT on the first user-triggered AI call. + + For dotted names (e.g. 'google.genai.types'), the parent packages + are pre-imported first. This avoids circular-import errors in libraries + like google-genai where importing a sub-module triggers a chain that + re-enters the same sub-module (e.g. google.genai._api_client does + 'from .types import HttpOptions' during its own initialization). + Without parent pre-import, that relative import would see a + 'partially initialized' google.genai.types in sys.modules and raise + ImportError. """ mod = sys.modules.get(name) if mod is not None: return mod + if "." in name: + parts = name.split(".") + for i in range(1, len(parts)): + parent = ".".join(parts[:i]) + if parent not in sys.modules: + importlib.import_module(parent) return importlib.import_module(name) diff --git a/tests/test_discussion_compression.py b/tests/test_discussion_compression.py index 027361c4..7f727f16 100644 --- a/tests/test_discussion_compression.py +++ b/tests/test_discussion_compression.py @@ -51,7 +51,14 @@ def test_discussion_compression_deepseek(): mock_response = MagicMock() mock_response.json.return_value = {"choices": [{"message": {"content": "DeepSeek summary."}}]} - with patch("src.ai_client.requests.post", return_value=mock_response), \ + # Phase 3 (startup_speedup_20260606) removed the top-level 'import requests' + # from src/ai_client.py. The module is now resolved via _require_warmed() + # at call time. We mock _require_warmed to return a fake requests module + # that has the .post() method. + mock_requests_mod = MagicMock() + mock_requests_mod.post.return_value = mock_response + + with patch("src.ai_client._require_warmed", return_value=mock_requests_mod), \ patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test"}}): result = ai_client.run_discussion_compression("DeepSeek history")