Coverage for src/local_deep_research/llm/providers/implementations/google.py: 100%
42 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
1"""Google/Gemini LLM provider for Local Deep Research."""
3from loguru import logger
5from ....security.log_sanitizer import redact_secrets
6from ..openai_base import OpenAICompatibleProvider
9class GoogleProvider(OpenAICompatibleProvider):
10 """Google Gemini provider using OpenAI-compatible endpoint.
12 This uses Google's OpenAI-compatible API endpoint to access Gemini models,
13 which automatically supports all current and future Gemini models without
14 needing to update the code.
15 """
17 provider_name = "Google Gemini"
18 api_key_setting = "llm.google.api_key"
19 default_base_url = "https://generativelanguage.googleapis.com/v1beta/openai"
20 default_model = "" # User must explicitly pick a model — no silent fallback
22 # Metadata for auto-discovery
23 provider_key = "GOOGLE"
24 company_name = "Google"
25 is_cloud = True
27 @classmethod
28 def requires_auth_for_models(cls):
29 """Google requires authentication for listing models.
31 Note: Google's OpenAI-compatible /models endpoint has a bug (returns 401).
32 The native Gemini API endpoint requires an API key.
33 """
34 return True
36 @classmethod
37 def list_models_for_api(cls, api_key=None, base_url=None):
38 """List available models using Google's native API.
40 Args:
41 api_key: Google API key
42 base_url: Not used - Google uses a fixed endpoint
44 Google's OpenAI-compatible /models endpoint returns 401 (bug),
45 so we use the native Gemini API endpoint instead.
46 """
47 if not api_key:
48 logger.debug("Google Gemini requires API key for listing models")
49 return []
51 try:
52 from ....security import safe_get
54 # Use the native Gemini API endpoint (not OpenAI-compatible)
55 # Note: Google's API requires the key as a query parameter, not in headers
56 # This is their documented approach: https://ai.google.dev/api/rest
57 url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
59 response = safe_get(url, timeout=10)
61 if response.status_code == 200:
62 data = response.json()
63 models = []
65 for model in data.get("models", []):
66 model_name = model.get("name", "")
67 # Extract just the model ID from "models/gemini-1.5-flash"
68 if model_name.startswith("models/"):
69 model_id = model_name[7:] # Remove "models/" prefix
70 else:
71 model_id = model_name
73 # Only include generative models (not embedding models)
74 supported_methods = model.get(
75 "supportedGenerationMethods", []
76 )
77 if "generateContent" in supported_methods and model_id:
78 models.append(
79 {
80 "value": model_id,
81 "label": model_id,
82 }
83 )
85 logger.info(
86 f"Found {len(models)} generative models from Google Gemini API"
87 )
88 return models
89 logger.warning(
90 f"Google Gemini API returned status {response.status_code}"
91 )
92 return []
94 except Exception as e:
95 # Google's API requires the key as a ?key=... query
96 # parameter, so requests/urllib3 exceptions often embed the
97 # full URL — and therefore the key — in str(e).
98 # logger.exception would write that to every loguru sink;
99 # instead, redact and log via logger.warning so the exception
100 # chain (which also carries the URL in earlier frames) is
101 # dropped. The redacted message is captured in a local
102 # before the logger call so the check-sensitive-logging
103 # pre-commit hook does not flag the exception variable as
104 # referenced inside the log call.
105 safe_msg = redact_secrets(str(e), api_key)
106 logger.warning(f"Error fetching Google Gemini models: {safe_msg}")
107 return []