feat: add isolated Gemini provider
This commit is contained in:
@@ -12,12 +12,18 @@ class ChatMessage:
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProviderUsage:
|
||||
input_tokens: int | None = None
|
||||
output_tokens: int | None = None
|
||||
|
||||
|
||||
class ProviderError(RuntimeError):
|
||||
"""Base class for expected local provider failures."""
|
||||
"""Base class for expected provider failures."""
|
||||
|
||||
|
||||
class ProviderUnavailableError(ProviderError):
|
||||
"""The local model runtime cannot be reached."""
|
||||
"""The selected model runtime cannot be reached."""
|
||||
|
||||
|
||||
class ModelNotInstalledError(ProviderError):
|
||||
@@ -25,7 +31,7 @@ class ModelNotInstalledError(ProviderError):
|
||||
|
||||
|
||||
class ProviderTimeoutError(ProviderError):
|
||||
"""The local model did not respond in time."""
|
||||
"""The model did not respond in time."""
|
||||
|
||||
|
||||
class ResponseAbortedError(ProviderError):
|
||||
@@ -33,7 +39,27 @@ class ResponseAbortedError(ProviderError):
|
||||
|
||||
|
||||
class InvalidProviderResponseError(ProviderError):
|
||||
"""The local runtime returned an unusable response."""
|
||||
"""The runtime returned an unusable response."""
|
||||
|
||||
|
||||
class MissingApiKeyError(ProviderError):
|
||||
"""No API key is available for the selected cloud provider."""
|
||||
|
||||
|
||||
class InvalidApiKeyError(ProviderError):
|
||||
"""The cloud provider rejected its API key."""
|
||||
|
||||
|
||||
class ProviderRateLimitError(ProviderError):
|
||||
"""The free provider quota or rate limit is exhausted."""
|
||||
|
||||
|
||||
class CloudNetworkError(ProviderError):
|
||||
"""A cloud request failed at the network layer."""
|
||||
|
||||
|
||||
class CloudModelUnavailableError(ProviderError):
|
||||
"""The configured cloud model is unavailable."""
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user