""" Shared OpenAI HTTP client — single implementation of the chat-completions call. Replaces duplicated urllib boilerplate in gpt_reasoning, relevance, mission_parser, and threat_chat. """ import json import logging import os import urllib.request import urllib.error from typing import Dict, Optional, Tuple logger = logging.getLogger(__name__) _API_URL = "https://api.openai.com/v1/chat/completions" class OpenAIAPIError(Exception): """Raised when the OpenAI API call fails (HTTP or network error).""" def __init__(self, message: str, status_code: Optional[int] = None): self.status_code = status_code super().__init__(message) def get_api_key() -> Optional[str]: """Return the OpenAI API key from the environment, or None.""" return os.environ.get("OPENAI_API_KEY") def chat_completion(payload: Dict, *, timeout: int = 30) -> Dict: """Send a chat-completion request and return the parsed JSON response. Args: payload: Full request body (model, messages, etc.). timeout: HTTP timeout in seconds. Returns: Parsed response dict. Raises: OpenAIAPIError: On HTTP or network failure. """ api_key = get_api_key() if not api_key: raise OpenAIAPIError("OPENAI_API_KEY not set") headers = { "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", } try: req = urllib.request.Request( _API_URL, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST", ) with urllib.request.urlopen(req, timeout=timeout) as response: return json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: raise OpenAIAPIError( f"HTTP {e.code}: {e.reason}", status_code=e.code ) from e except urllib.error.URLError as e: raise OpenAIAPIError(f"URL error: {e.reason}") from e def extract_content(resp_data: Dict) -> Tuple[Optional[str], Optional[str]]: """Safely extract content and refusal from a chat-completion response. Returns: (content, refusal) — either may be None. """ choice = resp_data.get("choices", [{}])[0] message = choice.get("message", {}) return message.get("content"), message.get("refusal")