""" Abstract interface for LLM providers. This module defines the interface that all LLM providers must implement, enabling support for multiple LLM backends (OpenAI, Anthropic, Gemini, Codex, etc.) """ from abc import ABC, abstractmethod from typing import Any from .response_models import LLMToolCallResult, TokenUsage class LLMInterface(ABC): """ Abstract interface for LLM providers. All LLM provider implementations must inherit from this class and implement the required methods. """ def __init__( self, provider: str, api_key: str, base_url: str, model: str, reasoning_effort: str = "low", **kwargs: Any, ): """ Initialize LLM provider. Args: provider: Provider name (e.g., "openai", "codex", "anthropic", "gemini"). api_key: API key or authentication token. base_url: Base URL for the API. model: Model name. reasoning_effort: Reasoning effort level for supported providers. **kwargs: Additional provider-specific parameters. """ self.provider = provider.lower() self.api_key = api_key self.base_url = base_url self.model = model self.reasoning_effort = reasoning_effort @abstractmethod async def verify_connection(self) -> None: """ Verify that the LLM provider is configured correctly by making a simple test call. Raises: RuntimeError: If the connection test fails. """ pass @abstractmethod async def call( self, messages: list[dict[str, str]], response_format: Any | None = None, max_completion_tokens: int | None = None, temperature: float | None = None, scope: str = "memory", max_retries: int = 10, initial_backoff: float = 1.0, max_backoff: float = 60.0, skip_validation: bool = False, strict_schema: bool = False, return_usage: bool = False, ) -> Any: """ Make an LLM API call with retry logic. Args: messages: List of message dicts with 'role' and 'content'. response_format: Optional Pydantic model for structured output. max_completion_tokens: Maximum tokens in response. temperature: Sampling temperature (0.0-2.0). scope: Scope identifier for tracking. max_retries: Maximum retry attempts. initial_backoff: Initial backoff time in seconds. max_backoff: Maximum backoff time in seconds. skip_validation: Return raw JSON without Pydantic validation. strict_schema: Use strict JSON schema enforcement (OpenAI only). return_usage: If True, return tuple (result, TokenUsage) instead of just result. Returns: If return_usage=False: Parsed response if response_format is provided, otherwise text content. If return_usage=True: Tuple of (result, TokenUsage) with token counts. Raises: OutputTooLongError: If output exceeds token limits. Exception: Re-raises API errors after retries exhausted. """ pass @abstractmethod async def call_with_tools( self, messages: list[dict[str, Any]], tools: list[dict[str, Any]], max_completion_tokens: int | None = None, temperature: float | None = None, scope: str = "tools", max_retries: int = 5, initial_backoff: float = 1.0, max_backoff: float = 30.0, tool_choice: str | dict[str, Any] = "auto", ) -> LLMToolCallResult: """ Make an LLM API call with tool/function calling support. Args: messages: List of message dicts. Can include tool results with role='tool'. tools: List of tool definitions in OpenAI format. max_completion_tokens: Maximum tokens in response. temperature: Sampling temperature (0.0-2.0). scope: Scope identifier for tracking. max_retries: Maximum retry attempts. initial_backoff: Initial backoff time in seconds. max_backoff: Maximum backoff time in seconds. tool_choice: How to choose tools - "auto", "none", "required", or specific function. Returns: LLMToolCallResult with content and/or tool_calls. """ pass async def supports_batch_api(self) -> bool: """ Check if this provider supports batch API operations. Returns: True if provider supports submit_batch/get_batch_status/retrieve_batch_results """ return False async def submit_batch( self, requests: list[dict[str, Any]], endpoint: str = "/v1/chat/completions", completion_window: str = "24h", ) -> dict[str, Any]: """ Submit a batch of requests to the provider's batch API. Args: requests: List of request dicts in JSONL format (custom_id, method, url, body) endpoint: API endpoint for the batch (e.g., "/v1/chat/completions") completion_window: Completion window (e.g., "24h") Returns: Dict with batch metadata: {"batch_id": str, "status": str, ...} Raises: NotImplementedError: If provider doesn't support batch API """ raise NotImplementedError(f"Batch API not supported for provider: {self.provider}") async def get_batch_status(self, batch_id: str) -> dict[str, Any]: """ Get the status of a batch job. Args: batch_id: Batch identifier returned from submit_batch Returns: Dict with status info: {"batch_id": str, "status": str, "completed_at": str, ...} Raises: NotImplementedError: If provider doesn't support batch API """ raise NotImplementedError(f"Batch API not supported for provider: {self.provider}") async def retrieve_batch_results(self, batch_id: str) -> list[dict[str, Any]]: """ Retrieve completed batch results. Args: batch_id: Batch identifier returned from submit_batch Returns: List of result dicts (one per request, matched by custom_id) Raises: NotImplementedError: If provider doesn't support batch API """ raise NotImplementedError(f"Batch API not supported for provider: {self.provider}") @abstractmethod async def cleanup(self) -> None: """Clean up resources (close connections, etc.).""" pass class OutputTooLongError(Exception): """ Bridge exception raised when LLM output exceeds token limits. This wraps provider-specific errors (e.g., OpenAI's LengthFinishReasonError) to allow callers to handle output length issues without depending on provider-specific implementations. """ pass