"""Gemini provider using Google's official GA Python SDK.""" from __future__ import annotations from collections.abc import Callable from typing import Any import httpx from google import genai from google.genai import errors, types from javis.providers.base import ( ChatMessage, CloudModelUnavailableError, CloudNetworkError, InvalidApiKeyError, InvalidProviderResponseError, MissingApiKeyError, ProviderError, ProviderRateLimitError, ProviderTimeoutError, ProviderUnavailableError, ProviderUsage, ) ClientFactory = Callable[..., Any] class GeminiProvider: name = "gemini" def __init__( self, *, model: str, api_key: str | None, timeout_seconds: float, max_output_tokens: int, max_retries: int, client_factory: ClientFactory = genai.Client, ) -> None: if not api_key: raise MissingApiKeyError("Für Gemini ist kein API-Schlüssel hinterlegt.") if max_retries not in {0, 1}: raise ValueError("Gemini darf höchstens einen Wiederholungsversuch verwenden.") self.model = model self.last_usage = ProviderUsage() self._config = types.GenerateContentConfig( candidate_count=1, max_output_tokens=max_output_tokens, ) self._client = client_factory( api_key=api_key, http_options=types.HttpOptions( api_version="v1", timeout=max(1, round(timeout_seconds * 1_000)), retry_options=types.HttpRetryOptions( attempts=max_retries + 1, http_status_codes=[408, 500, 502, 503, 504], ), ), ) def chat(self, messages: list[ChatMessage]) -> str: contents = [ types.Content( role="model" if message.role == "assistant" else "user", parts=[types.Part.from_text(text=message.content)], ) for message in messages ] self.last_usage = ProviderUsage() try: response = self._client.models.generate_content( model=self.model, contents=contents, config=self._config, ) except errors.APIError as exc: self._raise_api_error(exc) except httpx.TimeoutException as exc: raise ProviderTimeoutError("Gemini hat nicht rechtzeitig geantwortet.") from exc except httpx.NetworkError as exc: raise CloudNetworkError( "Gemini ist wegen eines Netzwerkfehlers nicht erreichbar." ) from exc except (OSError, ConnectionError) as exc: raise CloudNetworkError( "Gemini ist wegen eines Netzwerkfehlers nicht erreichbar." ) from exc text = getattr(response, "text", None) if not isinstance(text, str) or not text.strip(): raise InvalidProviderResponseError( "Gemini hat keine verwendbare Textantwort geliefert." ) usage = getattr(response, "usage_metadata", None) self.last_usage = ProviderUsage( input_tokens=self._token_count(usage, "prompt_token_count"), output_tokens=self._token_count(usage, "candidates_token_count"), ) return text.strip() @staticmethod def _token_count(usage: object, name: str) -> int | None: value = getattr(usage, name, None) return value if isinstance(value, int) and value >= 0 else None @staticmethod def _raise_api_error(exc: errors.APIError) -> None: if exc.code in {401, 403}: raise InvalidApiKeyError("Der Gemini-API-Schlüssel wurde abgelehnt.") from exc if exc.code == 429: raise ProviderRateLimitError( "Das kostenlose Gemini-Kontingent ist vorübergehend ausgeschöpft." ) from exc if exc.code == 404: raise CloudModelUnavailableError( "Das konfigurierte Gemini-Modell ist nicht verfügbar." ) from exc if exc.code == 408: raise ProviderTimeoutError("Gemini hat nicht rechtzeitig geantwortet.") from exc if exc.code >= 500: raise ProviderUnavailableError("Gemini ist vorübergehend nicht verfügbar.") from exc raise ProviderError(f"Gemini-Anfrage fehlgeschlagen (HTTP {exc.code}).") from exc