detection_base / utils /openai_client.py
Zhen Ye
feat(backend): enhance inference pipeline with GLM logic and structured outputs
bb6e650
"""
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")