129 lines
4.4 KiB
Python
129 lines
4.4 KiB
Python
"""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
|