# -*- coding: utf-8 -*- """ OpenAI provider for itsulu_blog_publisher. Supports GPT-4o, GPT-4o-mini, GPT-4-turbo, and any future models. Uses the /v1/chat/completions endpoint via raw HTTP. """ import logging import requests from odoo.exceptions import UserError _logger = logging.getLogger(__name__) OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions' KNOWN_OPENAI_MODELS = { 'gpt-4o': 'gpt-4o', 'gpt-4o-mini': 'gpt-4o-mini', 'gpt-4-turbo': 'gpt-4-turbo', 'gpt-4': 'gpt-4', 'gpt-3.5-turbo': 'gpt-3.5-turbo', 'o1': 'o1', 'o1-mini': 'o1-mini', 'o3': 'o3', 'o3-mini': 'o3-mini', 'o4-mini': 'o4-mini', } class OpenAIProvider: """ Calls OpenAI /v1/chat/completions. Returns (raw_text: str, tokens_used: int). """ def __init__(self, api_key: str, model: str): self.api_key = api_key self.model = KNOWN_OPENAI_MODELS.get(model, model) def generate(self, system_prompt: str, user_prompt: str) -> tuple: headers = { 'Authorization': f'Bearer {self.api_key}', 'Content-Type': 'application/json', } payload = { 'model': self.model, 'max_tokens': 4096, 'messages': [ {'role': 'system', 'content': system_prompt}, {'role': 'user', 'content': user_prompt}, ], 'response_format': {'type': 'json_object'}, # enforce JSON mode } _logger.debug("OpenAIProvider calling %s with model %s", OPENAI_API_URL, self.model) try: resp = requests.post( OPENAI_API_URL, headers=headers, json=payload, timeout=120, ) except requests.Timeout: raise UserError("OpenAI API request timed out after 120 seconds.") except requests.RequestException as exc: raise UserError(f"OpenAI API network error: {exc}") from exc if resp.status_code == 401: raise UserError( "OpenAI API returned 401 Unauthorized. Check your API key in Settings → Blog Publisher." ) if resp.status_code == 429: raise UserError( "OpenAI API rate limit reached (429). Wait and retry, or switch provider." ) 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"OpenAI API error {resp.status_code}: {err_detail}") data = resp.json() choices = data.get('choices', []) if not choices: raise UserError("OpenAI returned an empty choices list.") raw_text = choices[0].get('message', {}).get('content', '') usage = data.get('usage', {}) tokens_used = usage.get('total_tokens', 0) _logger.info( "OpenAIProvider: model=%s total_tokens=%d", self.model, tokens_used ) return raw_text, tokens_used