# -*- coding: utf-8 -*- """ Google Gemini provider for itsulu_blog_publisher. Uses the Gemini generateContent REST API directly (no SDK dependency). Supports gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash, etc. """ import logging import requests from dataclasses import dataclass from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @dataclass class ProviderResponse: """Raw response from a provider.""" text: str = '' tokens_used: int = 0 GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models' KNOWN_GEMINI_MODELS = { 'gemini-2.0-flash': 'gemini-2.0-flash', 'gemini-2.0-flash-exp': 'gemini-2.0-flash-exp', 'gemini-1.5-pro': 'gemini-1.5-pro', 'gemini-1.5-flash': 'gemini-1.5-flash', 'gemini-1.5-flash-8b': 'gemini-1.5-flash-8b', 'gemini-2.5-pro': 'gemini-2.5-pro', 'gemini-2.5-flash': 'gemini-2.5-flash', } class GeminiProvider: """ Calls Google Gemini generateContent endpoint. Returns (raw_text: str, tokens_used: int). """ def __init__(self, api_key: str, model: str): self.api_key = api_key self.model = KNOWN_GEMINI_MODELS.get(model, model) def generate(self, system_prompt: str, user_prompt: str) -> ProviderResponse: url = f"{GEMINI_API_BASE}/{self.model}:generateContent?key={self.api_key}" payload = { 'system_instruction': { 'parts': [{'text': system_prompt}] }, 'contents': [ { 'role': 'user', 'parts': [{'text': user_prompt}] } ], 'generationConfig': { 'maxOutputTokens': 4096, 'responseMimeType': 'application/json', # Gemini JSON mode }, } _logger.debug("GeminiProvider calling model %s", self.model) try: resp = requests.post(url, json=payload, timeout=120) except requests.Timeout: raise UserError("Google Gemini API request timed out after 120 seconds.") except requests.RequestException as exc: raise UserError(f"Google Gemini API network error: {exc}") from exc if resp.status_code == 400: try: err_detail = resp.json().get('error', {}).get('message', resp.text[:300]) except Exception: err_detail = resp.text[:300] raise UserError(f"Google Gemini API bad request: {err_detail}") if resp.status_code == 403: raise UserError( "Google Gemini API returned 403 Forbidden. " "Check your Gemini API key in Settings → Blog Publisher." ) if not resp.ok: try: err_detail = resp.json().get('error', {}).get('message', resp.text[:300]) except Exception: err_detail = resp.text[:300] raise UserError(f"Google Gemini API error {resp.status_code}: {err_detail}") data = resp.json() candidates = data.get('candidates', []) if not candidates: raise UserError("Google Gemini returned no candidates.") parts = candidates[0].get('content', {}).get('parts', []) raw_text = ''.join(p.get('text', '') for p in parts) usage = data.get('usageMetadata', {}) tokens_used = ( usage.get('promptTokenCount', 0) + usage.get('candidatesTokenCount', 0) ) _logger.info( "GeminiProvider: model=%s tokens_used=%d", self.model, tokens_used ) return ProviderResponse(text=raw_text, tokens_used=tokens_used)