Skip to content

Commit 361bc54

Browse files
Alex-Hunterzclaude
andcommitted
migrate gemini provider to google-genai SDK
- drop deprecated google.generativeai - native async via client.aio.models.generate_content (no run_in_executor) - key resolution: explicit > GEMINI_API_KEY > GOOGLE_API_KEY Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 560b6a1 commit 361bc54

1 file changed

Lines changed: 30 additions & 125 deletions

File tree

vektori/models/gemini.py

Lines changed: 30 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
"""
22
Google Gemini API provider for fact and episode extraction.
33
4-
Direct integration with google-generativeai library.
5-
Supports: gemini-2-flash, gemini-2.5-flash, gemini-3-pro, etc.
4+
Direct integration with google-genai library.
5+
Supports: gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-flash-preview, etc.
66
7-
Features:
8-
- Direct API calls (no middleware)
9-
- Automatic retry with exponential backoff
10-
- Fault tolerance for transient errors
11-
- Rate limiting awareness
12-
13-
Install: pip install google-generativeai
14-
15-
Usage:
16-
v = Vektori(extraction_model="gemini:gemini-2.5-flash-lite")
7+
Install: pip install google-genai
178
"""
189

1910
from __future__ import annotations
2011

2112
import asyncio
2213
import json
2314
import logging
15+
import os
2416
import random
2517
from typing import Any
2618

@@ -30,31 +22,17 @@
3022

3123
DEFAULT_MODEL = "gemini-2.5-flash-lite"
3224

33-
# Retry configuration
3425
DEFAULT_MAX_RETRIES = 3
35-
DEFAULT_INITIAL_BACKOFF = 1.0 # seconds
36-
DEFAULT_MAX_BACKOFF = 32.0 # seconds
26+
DEFAULT_INITIAL_BACKOFF = 1.0
27+
DEFAULT_MAX_BACKOFF = 32.0
3728
DEFAULT_BACKOFF_MULTIPLIER = 2.0
3829

3930

4031
class GeminiLLM(LLMProvider):
4132
"""
42-
Google Gemini API provider for fact + episode extraction.
43-
44-
Direct google-generativeai integration (not via LiteLLM).
45-
Supports all Gemini models: gemini-2-flash, gemini-2.5-flash, gemini-3-pro, etc.
46-
47-
Features:
48-
- Automatic retry with exponential backoff for transient failures
49-
- Handles rate limiting, network errors, and API timeouts
50-
- Configurable max retries and backoff strategy
51-
- Fault-tolerant: continues despite temporary failures
52-
53-
Install: pip install google-generativeai
33+
Google Gemini API provider using the google-genai SDK.
5434
55-
Usage:
56-
provider = GeminiLLM(model="gemini-2-5-flash-lite")
57-
response = await provider.generate("Extract facts from: ...", max_tokens=2000)
35+
Supports all Gemini models. Reads GEMINI_API_KEY or GOOGLE_API_KEY from env.
5836
"""
5937

6038
def __init__(
@@ -65,82 +43,47 @@ def __init__(
6543
initial_backoff: float = DEFAULT_INITIAL_BACKOFF,
6644
max_backoff: float = DEFAULT_MAX_BACKOFF,
6745
) -> None:
68-
"""
69-
Initialize Gemini LLM provider with retry configuration.
70-
71-
Args:
72-
model: Model name (e.g., "gemini-2-5-flash-lite", "gemini-2-flash", "gemini-3-pro")
73-
Defaults to "gemini-2-5-flash-lite" if not specified.
74-
api_key: Google API key. If not provided, uses GOOGLE_API_KEY environment variable.
75-
max_retries: Maximum number of retry attempts (default: 3)
76-
initial_backoff: Initial backoff time in seconds (default: 1.0)
77-
max_backoff: Maximum backoff time in seconds (default: 32.0)
78-
"""
7946
self.model = model or DEFAULT_MODEL
8047
self._api_key = api_key
8148
self._client = None
82-
83-
# Retry configuration
8449
self.max_retries = max_retries
8550
self.initial_backoff = initial_backoff
8651
self.max_backoff = max_backoff
8752

8853
def _get_client(self):
89-
"""Lazy-load Gemini client."""
9054
if self._client is None:
9155
try:
92-
import google.generativeai as genai
56+
from google import genai
9357
except ImportError as e:
94-
raise ImportError(
95-
"google-generativeai required: pip install google-generativeai"
96-
) from e
97-
98-
# Configure API key
99-
genai.configure(api_key=self._api_key)
100-
self._client = genai
101-
58+
raise ImportError("google-genai required: pip install google-genai") from e
59+
60+
api_key = (
61+
self._api_key
62+
or os.environ.get("GEMINI_API_KEY")
63+
or os.environ.get("GOOGLE_API_KEY")
64+
)
65+
self._client = genai.Client(api_key=api_key)
10266
return self._client
10367

10468
async def _calculate_backoff(self, attempt: int) -> float:
105-
"""
106-
Calculate backoff time with exponential backoff + jitter.
107-
108-
Prevents thundering herd problem when many requests retry simultaneously.
109-
"""
11069
backoff = min(
11170
self.initial_backoff * (DEFAULT_BACKOFF_MULTIPLIER**attempt),
11271
self.max_backoff,
11372
)
114-
# Add jitter: ±25% randomness
115-
jitter = backoff * random.uniform(0.75, 1.25)
116-
return jitter
73+
return backoff * random.uniform(0.75, 1.25)
11774

11875
async def generate(self, prompt: str, max_tokens: int | None = None) -> str:
119-
"""
120-
Generate a completion using Gemini API with automatic retry.
121-
122-
Args:
123-
prompt: The prompt to send to Gemini
124-
max_tokens: Maximum tokens in response. None = use Gemini defaults.
76+
from google.genai import types
12577

126-
Returns:
127-
Generated text (should be valid JSON for extraction prompts)
78+
client = self._get_client()
12879

129-
Raises:
130-
RuntimeError: If all retry attempts fail
131-
"""
132-
genai = self._get_client()
133-
134-
# Build request kwargs — force JSON output so the model never returns prose
135-
kwargs: dict[str, Any] = {
136-
"temperature": 0.1, # Low randomness for extraction tasks
80+
config_kwargs: dict[str, Any] = {
81+
"temperature": 0.1,
13782
"response_mime_type": "application/json",
13883
}
139-
14084
if max_tokens is not None:
141-
kwargs["max_output_tokens"] = max_tokens
85+
config_kwargs["max_output_tokens"] = max_tokens
14286

143-
# Retry loop with exponential backoff
14487
last_exception = None
14588

14689
for attempt in range(self.max_retries + 1):
@@ -150,80 +93,42 @@ async def generate(self, prompt: str, max_tokens: int | None = None) -> str:
15093
f"model={self.model}, tokens={max_tokens}"
15194
)
15295

153-
# Run in executor to avoid blocking event loop
154-
loop = asyncio.get_event_loop()
155-
156-
def _generate():
157-
try:
158-
model = genai.GenerativeModel(self.model)
159-
response = model.generate_content(
160-
prompt,
161-
generation_config=genai.types.GenerationConfig(**kwargs),
162-
)
163-
return response.text or ""
164-
except Exception as e:
165-
raise e
166-
167-
response_text = await loop.run_in_executor(None, _generate)
96+
response = await client.aio.models.generate_content(
97+
model=self.model,
98+
contents=prompt,
99+
config=types.GenerateContentConfig(**config_kwargs),
100+
)
168101

169-
# Success
170102
logger.debug(f"Gemini API call succeeded on attempt {attempt + 1}")
171-
return response_text
103+
return response.text or ""
172104

173105
except Exception as e:
174106
last_exception = e
175-
176-
# Check if this is a retryable error
177107
error_msg = str(e).lower()
178108
is_retryable = any(
179109
term in error_msg
180-
for term in [
181-
"timeout",
182-
"500",
183-
"429", # Rate limit
184-
"503", # Service unavailable
185-
"connection",
186-
"network",
187-
"temporarily",
188-
"try again",
189-
]
110+
for term in ["timeout", "500", "429", "503", "connection", "network", "temporarily", "try again"]
190111
)
191112

192-
# If not retryable or last attempt, raise
193113
if not is_retryable or attempt >= self.max_retries:
194114
logger.error(f"Gemini API call failed after {attempt + 1} attempts: {e}")
195115
raise RuntimeError(
196116
f"Gemini API call failed after {self.max_retries + 1} attempts: {e}"
197117
) from e
198118

199-
# Calculate backoff and wait
200119
backoff_time = await self._calculate_backoff(attempt)
201120
logger.warning(
202121
f"Gemini API call failed (attempt {attempt + 1}): {e}. "
203122
f"Retrying in {backoff_time:.1f}s..."
204123
)
205124
await asyncio.sleep(backoff_time)
206125

207-
# Should never reach here, but just in case
208126
raise RuntimeError(
209127
f"Gemini API call failed after {self.max_retries + 1} attempts"
210128
) from last_exception
211129

212130
async def generate_json(self, prompt: str, max_tokens: int | None = None) -> dict[str, Any]:
213-
"""
214-
Generate JSON response from Gemini with retry.
215-
216-
Convenience method that parses the response as JSON.
217-
218-
Args:
219-
prompt: Prompt that asks for JSON output
220-
max_tokens: Maximum tokens
221-
222-
Returns:
223-
Parsed JSON object
224-
"""
225131
response_text = await self.generate(prompt, max_tokens)
226-
227132
try:
228133
return json.loads(response_text)
229134
except json.JSONDecodeError as e:

0 commit comments

Comments
 (0)