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

1"""Google/Gemini LLM provider for Local Deep Research.""" 

2 

3from loguru import logger 

4 

5from ....security.log_sanitizer import redact_secrets 

6from ..openai_base import OpenAICompatibleProvider 

7 

8 

9class GoogleProvider(OpenAICompatibleProvider): 

10 """Google Gemini provider using OpenAI-compatible endpoint. 

11 

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 """ 

16 

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 

21 

22 # Metadata for auto-discovery 

23 provider_key = "GOOGLE" 

24 company_name = "Google" 

25 is_cloud = True 

26 

27 @classmethod 

28 def requires_auth_for_models(cls): 

29 """Google requires authentication for listing models. 

30 

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 

35 

36 @classmethod 

37 def list_models_for_api(cls, api_key=None, base_url=None): 

38 """List available models using Google's native API. 

39 

40 Args: 

41 api_key: Google API key 

42 base_url: Not used - Google uses a fixed endpoint 

43 

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 [] 

50 

51 try: 

52 from ....security import safe_get 

53 

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}" 

58 

59 response = safe_get(url, timeout=10) 

60 

61 if response.status_code == 200: 

62 data = response.json() 

63 models = [] 

64 

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 

72 

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 ) 

84 

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 [] 

93 

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 []