itsulu-blog-publisher/addons/itsulu_blog_publisher/services/gemini_provider.py
Nicholas Riegel 0fc4febabf Reorganize codebase into Odoo addon structure per ARCHITECTURE.md
Restructure project files to follow the addon layout:
- Move models to addons/itsulu_blog_publisher/models/
- Move services (LLM providers, routers) to addons/itsulu_blog_publisher/services/
- Move wizards to addons/itsulu_blog_publisher/wizards/
- Move views (XML templates) to addons/itsulu_blog_publisher/views/
- Move data (cron, mail templates) to addons/itsulu_blog_publisher/data/
- Move security (ACL) to addons/itsulu_blog_publisher/security/
- Move tests and factories to addons/itsulu_blog_publisher/tests/
- Move BDD features to addons/itsulu_blog_publisher/features/
- Create __init__.py files for all Python packages

This enables proper Odoo module discovery and import structure.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-29 12:11:42 -04:00

101 lines
3.4 KiB
Python

# -*- 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 odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
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) -> tuple:
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 raw_text, tokens_used