mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-06 04:13:48 +00:00
182 lines
7.0 KiB
Python
182 lines
7.0 KiB
Python
"""Google Gemini provider implementation.
|
|
|
|
Google's Gemini models offer a free tier and excellent quality/price ratio.
|
|
Models are loaded dynamically from the API - no hardcoded model names.
|
|
"""
|
|
from typing import Optional, List
|
|
import json
|
|
import urllib.request
|
|
import urllib.error
|
|
from .base import AIProvider, AIProviderError
|
|
|
|
|
|
class GeminiProvider(AIProvider):
|
|
"""Google Gemini provider using the Generative Language API."""
|
|
|
|
NAME = "gemini"
|
|
REQUIRES_API_KEY = True
|
|
API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
|
|
|
|
# Patterns to exclude from model list (experimental, preview, specialized)
|
|
EXCLUDED_PATTERNS = [
|
|
'preview', 'exp', 'experimental', 'computer-use',
|
|
'deep-research', 'image', 'embedding', 'aqa', 'tts',
|
|
'learnlm', 'imagen', 'veo'
|
|
]
|
|
|
|
# Deprecated models that may still appear in API but return 404
|
|
DEPRECATED_MODELS = [
|
|
'gemini-2.0-flash',
|
|
'gemini-1.0-pro',
|
|
'gemini-pro',
|
|
]
|
|
|
|
def list_models(self) -> List[str]:
|
|
"""List available Gemini models that support generateContent.
|
|
|
|
Filters to only stable text generation models, excluding:
|
|
- Preview/experimental models
|
|
- Image generation models
|
|
- Embedding models
|
|
- Specialized models (computer-use, deep-research, etc.)
|
|
|
|
Returns:
|
|
List of model IDs available for text generation.
|
|
"""
|
|
if not self.api_key:
|
|
return []
|
|
|
|
try:
|
|
url = f"{self.API_BASE}?key={self.api_key}"
|
|
req = urllib.request.Request(url, method='GET', headers={'User-Agent': 'ProxMenux/1.0'})
|
|
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
data = json.loads(resp.read().decode('utf-8'))
|
|
|
|
models = []
|
|
for model in data.get('models', []):
|
|
model_name = model.get('name', '')
|
|
# Extract just the model ID (e.g., "models/gemini-pro" -> "gemini-pro")
|
|
if model_name.startswith('models/'):
|
|
model_id = model_name[7:]
|
|
else:
|
|
model_id = model_name
|
|
|
|
# Only include models that support generateContent
|
|
supported_methods = model.get('supportedGenerationMethods', [])
|
|
if 'generateContent' not in supported_methods:
|
|
continue
|
|
|
|
# Exclude experimental, preview, and specialized models
|
|
model_lower = model_id.lower()
|
|
if any(pattern in model_lower for pattern in self.EXCLUDED_PATTERNS):
|
|
continue
|
|
|
|
# Exclude deprecated models that return 404
|
|
if model_id in self.DEPRECATED_MODELS:
|
|
continue
|
|
|
|
models.append(model_id)
|
|
|
|
# Sort with recommended models first (flash-lite, flash, pro)
|
|
def sort_key(m):
|
|
m_lower = m.lower()
|
|
if 'flash-lite' in m_lower:
|
|
return (0, m) # Best for notifications (fast, cheap)
|
|
if 'flash' in m_lower:
|
|
return (1, m)
|
|
if 'pro' in m_lower:
|
|
return (2, m)
|
|
return (3, m)
|
|
|
|
return sorted(models, key=sort_key)
|
|
except Exception as e:
|
|
print(f"[GeminiProvider] Failed to list models: {e}")
|
|
return []
|
|
|
|
def generate(self, system_prompt: str, user_message: str,
|
|
max_tokens: int = 200) -> Optional[str]:
|
|
"""Generate a response using Google's Gemini API.
|
|
|
|
Note: Gemini uses a different API format. System instructions
|
|
go in a separate systemInstruction field.
|
|
|
|
Args:
|
|
system_prompt: System instructions
|
|
user_message: User message to process
|
|
max_tokens: Maximum response length
|
|
|
|
Returns:
|
|
Generated text or None if failed
|
|
|
|
Raises:
|
|
AIProviderError: If API key is missing or request fails
|
|
"""
|
|
if not self.api_key:
|
|
raise AIProviderError("API key required for Gemini")
|
|
|
|
url = f"{self.API_BASE}/{self.model}:generateContent?key={self.api_key}"
|
|
|
|
# Gemini uses a specific format with contents array
|
|
payload = {
|
|
'systemInstruction': {
|
|
'parts': [{'text': system_prompt}]
|
|
},
|
|
'contents': [
|
|
{
|
|
'role': 'user',
|
|
'parts': [{'text': user_message}]
|
|
}
|
|
],
|
|
'generationConfig': {
|
|
'maxOutputTokens': max_tokens,
|
|
'temperature': 0.3,
|
|
}
|
|
}
|
|
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
|
|
result = self._make_request(url, payload, headers)
|
|
|
|
try:
|
|
# Gemini returns candidates array with content parts
|
|
candidates = result.get('candidates', [])
|
|
if not candidates:
|
|
# Check for blocked content or other issues
|
|
prompt_feedback = result.get('promptFeedback', {})
|
|
block_reason = prompt_feedback.get('blockReason', '')
|
|
if block_reason:
|
|
raise AIProviderError(f"Content blocked by Gemini: {block_reason}")
|
|
raise AIProviderError("No candidates in response - model may be overloaded")
|
|
|
|
# Check if response was blocked
|
|
finish_reason = candidates[0].get('finishReason', '')
|
|
if finish_reason == 'SAFETY':
|
|
safety_ratings = candidates[0].get('safetyRatings', [])
|
|
blocked_categories = [r.get('category', 'UNKNOWN') for r in safety_ratings
|
|
if r.get('blocked', False)]
|
|
raise AIProviderError(f"Response blocked by safety filter: {blocked_categories}")
|
|
|
|
content = candidates[0].get('content', {})
|
|
parts = content.get('parts', [])
|
|
if parts:
|
|
text = parts[0].get('text', '').strip()
|
|
if text:
|
|
return text
|
|
|
|
# No text content - check if it's a known issue
|
|
if finish_reason == 'MAX_TOKENS':
|
|
# MAX_TOKENS with no content could mean prompt too long OR model overload
|
|
raise AIProviderError("No response generated (MAX_TOKENS). Model may be overloaded - try again.")
|
|
elif finish_reason == 'STOP':
|
|
# Normal stop but no content - unusual
|
|
raise AIProviderError("Model returned empty response")
|
|
else:
|
|
raise AIProviderError(f"No response from model (reason: {finish_reason}). Try again later.")
|
|
except AIProviderError:
|
|
raise
|
|
except (KeyError, IndexError) as e:
|
|
raise AIProviderError(f"Unexpected response format: {e}")
|