fenra 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fenra/__init__.py +187 -0
- fenra/_context.py +42 -0
- fenra/_core.py +229 -0
- fenra/integrations/__init__.py +1 -0
- fenra/integrations/anthropic/__init__.py +677 -0
- fenra/integrations/gemini/__init__.py +529 -0
- fenra/integrations/openai/__init__.py +904 -0
- fenra/py.typed +0 -0
- fenra-0.1.0.dist-info/METADATA +90 -0
- fenra-0.1.0.dist-info/RECORD +11 -0
- fenra-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""Gemini integration with auto-instrumentation via monkey patching."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fenra._context import get_context
|
|
8
|
+
from fenra._core import enqueue_transaction
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_patched_sync = False
|
|
13
|
+
_patched_async = False
|
|
14
|
+
_patched_stream_sync = False
|
|
15
|
+
_patched_stream_async = False
|
|
16
|
+
|
|
17
|
+
# Store original methods for unpatching
|
|
18
|
+
_original_generate_content: Any = None
|
|
19
|
+
_original_generate_content_async: Any = None
|
|
20
|
+
_original_generate_content_stream: Any = None
|
|
21
|
+
_original_generate_content_stream_async: Any = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _track_generate_content_response(
|
|
25
|
+
response: Any,
|
|
26
|
+
model: str,
|
|
27
|
+
context: dict[str, Any],
|
|
28
|
+
config: Any = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Extract usage from GenerateContentResponse and queue transaction.
|
|
32
|
+
|
|
33
|
+
Handles:
|
|
34
|
+
- Token usage (including thinking tokens for reasoning models)
|
|
35
|
+
- Google Search tool usage (web_search requests)
|
|
36
|
+
- Image generation output
|
|
37
|
+
"""
|
|
38
|
+
if not hasattr(response, "usage_metadata") or response.usage_metadata is None:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
usage = response.usage_metadata
|
|
42
|
+
|
|
43
|
+
# Build token metrics
|
|
44
|
+
# thoughts_token_count is used by thinking models like gemini-2.0-flash-thinking
|
|
45
|
+
output_tokens = (getattr(usage, "candidates_token_count", 0) or 0) + (
|
|
46
|
+
getattr(usage, "thoughts_token_count", None) or 0
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
metrics: dict[str, Any] = {
|
|
50
|
+
"input_tokens": getattr(usage, "prompt_token_count", 0) or 0,
|
|
51
|
+
"output_tokens": output_tokens,
|
|
52
|
+
"total_tokens": getattr(usage, "total_token_count", 0) or 0,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Include cached tokens if present
|
|
56
|
+
cached = getattr(usage, "cached_content_token_count", None)
|
|
57
|
+
if cached:
|
|
58
|
+
metrics["cached_tokens"] = cached
|
|
59
|
+
|
|
60
|
+
usage_entries: list[dict[str, Any]] = [
|
|
61
|
+
{
|
|
62
|
+
"type": "tokens",
|
|
63
|
+
"metrics": metrics,
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Detect Google Search tool usage
|
|
68
|
+
search_usage = _detect_tool_usage(response, config)
|
|
69
|
+
if search_usage:
|
|
70
|
+
usage_entries.append(search_usage)
|
|
71
|
+
|
|
72
|
+
# Detect image generation output
|
|
73
|
+
image_usage = _detect_image_output(response, config)
|
|
74
|
+
if image_usage:
|
|
75
|
+
usage_entries.append(image_usage)
|
|
76
|
+
|
|
77
|
+
transaction: dict[str, Any] = {
|
|
78
|
+
"provider": "gemini",
|
|
79
|
+
"model": model,
|
|
80
|
+
"usage": usage_entries,
|
|
81
|
+
"context": context,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Include raw usage for debugging if available
|
|
85
|
+
try:
|
|
86
|
+
transaction["provider_usage_raw"] = {
|
|
87
|
+
"prompt_token_count": getattr(usage, "prompt_token_count", None),
|
|
88
|
+
"candidates_token_count": getattr(usage, "candidates_token_count", None),
|
|
89
|
+
"total_token_count": getattr(usage, "total_token_count", None),
|
|
90
|
+
"thoughts_token_count": getattr(usage, "thoughts_token_count", None),
|
|
91
|
+
"cached_content_token_count": getattr(
|
|
92
|
+
usage, "cached_content_token_count", None
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
enqueue_transaction(transaction)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _detect_tool_usage(response: Any, config: Any) -> dict[str, Any] | None:
|
|
102
|
+
"""
|
|
103
|
+
Detect Google Search tool usage from grounding metadata.
|
|
104
|
+
|
|
105
|
+
Returns a requests usage entry if web search was used, None otherwise.
|
|
106
|
+
"""
|
|
107
|
+
if config is None:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# Check if GoogleSearch tool was configured
|
|
111
|
+
tools = getattr(config, "tools", None)
|
|
112
|
+
if not tools:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
has_google_search = False
|
|
116
|
+
for tool in tools:
|
|
117
|
+
if hasattr(tool, "google_search") and tool.google_search is not None:
|
|
118
|
+
has_google_search = True
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if not has_google_search:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Extract search count from grounding metadata
|
|
125
|
+
search_count = 0
|
|
126
|
+
candidates = getattr(response, "candidates", None) or []
|
|
127
|
+
for candidate in candidates:
|
|
128
|
+
grounding_metadata = getattr(candidate, "grounding_metadata", None)
|
|
129
|
+
if grounding_metadata:
|
|
130
|
+
web_search_queries = getattr(grounding_metadata, "web_search_queries", None)
|
|
131
|
+
if web_search_queries:
|
|
132
|
+
search_count = len(web_search_queries)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Default to 1 if tool was configured but we couldn't detect count
|
|
136
|
+
if search_count == 0:
|
|
137
|
+
search_count = 1
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"type": "requests",
|
|
141
|
+
"metrics": {"count": search_count, "request_type": "web_search"},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _detect_image_output(response: Any, config: Any) -> dict[str, Any] | None:
|
|
146
|
+
"""
|
|
147
|
+
Detect image generation from response parts.
|
|
148
|
+
|
|
149
|
+
Returns an images usage entry if images were generated, None otherwise.
|
|
150
|
+
"""
|
|
151
|
+
if config is None:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
# Check if IMAGE was in response modalities
|
|
155
|
+
response_modalities = getattr(config, "response_modalities", None)
|
|
156
|
+
if not response_modalities or "IMAGE" not in response_modalities:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# Count images from response parts
|
|
160
|
+
image_count = 0
|
|
161
|
+
candidates = getattr(response, "candidates", None) or []
|
|
162
|
+
for candidate in candidates:
|
|
163
|
+
content = getattr(candidate, "content", None)
|
|
164
|
+
if content:
|
|
165
|
+
parts = getattr(content, "parts", None) or []
|
|
166
|
+
for part in parts:
|
|
167
|
+
if hasattr(part, "inline_data") and part.inline_data:
|
|
168
|
+
image_count += 1
|
|
169
|
+
|
|
170
|
+
if image_count == 0:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"type": "images",
|
|
175
|
+
"metrics": {
|
|
176
|
+
"generated": image_count,
|
|
177
|
+
"size_px": 1024, # Default Gemini image size
|
|
178
|
+
"quality": "standard",
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _track_stream(
|
|
184
|
+
stream: Any,
|
|
185
|
+
model: str,
|
|
186
|
+
context: dict[str, Any],
|
|
187
|
+
config: Any = None,
|
|
188
|
+
) -> Any:
|
|
189
|
+
"""
|
|
190
|
+
Wrap a streaming response to track usage from the final chunk.
|
|
191
|
+
|
|
192
|
+
Returns a wrapped stream that tracks usage when the stream is exhausted.
|
|
193
|
+
"""
|
|
194
|
+
last_chunk = None
|
|
195
|
+
|
|
196
|
+
def wrapped_stream() -> Any:
|
|
197
|
+
nonlocal last_chunk
|
|
198
|
+
try:
|
|
199
|
+
for chunk in stream:
|
|
200
|
+
last_chunk = chunk
|
|
201
|
+
yield chunk
|
|
202
|
+
finally:
|
|
203
|
+
# After stream is exhausted, track usage from final chunk
|
|
204
|
+
if last_chunk:
|
|
205
|
+
try:
|
|
206
|
+
_track_generate_content_response(last_chunk, model, context, config)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(
|
|
209
|
+
f"Error tracking Gemini streaming usage: {e}", exc_info=True
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return wrapped_stream()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def _track_stream_async(
|
|
216
|
+
stream: Any,
|
|
217
|
+
model: str,
|
|
218
|
+
context: dict[str, Any],
|
|
219
|
+
config: Any = None,
|
|
220
|
+
) -> Any:
|
|
221
|
+
"""
|
|
222
|
+
Wrap an async streaming response to track usage from the final chunk.
|
|
223
|
+
|
|
224
|
+
Returns a wrapped async stream that tracks usage when the stream is exhausted.
|
|
225
|
+
"""
|
|
226
|
+
last_chunk = None
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
async for chunk in stream:
|
|
230
|
+
last_chunk = chunk
|
|
231
|
+
yield chunk
|
|
232
|
+
finally:
|
|
233
|
+
# After stream is exhausted, track usage from final chunk
|
|
234
|
+
if last_chunk:
|
|
235
|
+
try:
|
|
236
|
+
_track_generate_content_response(last_chunk, model, context, config)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(
|
|
239
|
+
f"Error tracking Gemini streaming usage: {e}", exc_info=True
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ============================================================================
|
|
244
|
+
# Sync Patching
|
|
245
|
+
# ============================================================================
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def patch_gemini() -> None:
|
|
249
|
+
"""
|
|
250
|
+
Patch Gemini client to auto-track usage.
|
|
251
|
+
|
|
252
|
+
This patches the synchronous `Models.generate_content` method.
|
|
253
|
+
"""
|
|
254
|
+
global _patched_sync, _original_generate_content
|
|
255
|
+
|
|
256
|
+
if _patched_sync:
|
|
257
|
+
logger.debug("Gemini sync already patched, skipping")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
from google.genai.models import Models
|
|
262
|
+
except ImportError:
|
|
263
|
+
logger.warning(
|
|
264
|
+
"Gemini SDK not installed. Install with: pip install 'fenra[gemini]'"
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
original_generate_content = Models.generate_content
|
|
269
|
+
_original_generate_content = original_generate_content
|
|
270
|
+
|
|
271
|
+
@functools.wraps(original_generate_content)
|
|
272
|
+
def patched_generate_content(self: Models, *args: Any, **kwargs: Any) -> Any:
|
|
273
|
+
response = original_generate_content(self, *args, **kwargs)
|
|
274
|
+
|
|
275
|
+
context = get_context()
|
|
276
|
+
|
|
277
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
278
|
+
|
|
279
|
+
config = kwargs.get("config")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
_track_generate_content_response(response, model, context, config)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Error tracking Gemini usage: {e}", exc_info=True)
|
|
285
|
+
|
|
286
|
+
return response
|
|
287
|
+
|
|
288
|
+
Models.generate_content = patched_generate_content # type: ignore[assignment]
|
|
289
|
+
_patched_sync = True
|
|
290
|
+
logger.info("Gemini SDK patched for auto-instrumentation")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def patch_gemini_stream() -> None:
|
|
294
|
+
"""
|
|
295
|
+
Patch Gemini client to auto-track streaming usage.
|
|
296
|
+
|
|
297
|
+
This patches the synchronous `Models.generate_content_stream` method.
|
|
298
|
+
"""
|
|
299
|
+
global _patched_stream_sync, _original_generate_content_stream
|
|
300
|
+
|
|
301
|
+
if _patched_stream_sync:
|
|
302
|
+
logger.debug("Gemini sync stream already patched, skipping")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
from google.genai.models import Models
|
|
307
|
+
except ImportError:
|
|
308
|
+
logger.warning(
|
|
309
|
+
"Gemini SDK not installed. Install with: pip install 'fenra[gemini]'"
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
if not hasattr(Models, "generate_content_stream"):
|
|
314
|
+
logger.debug("Gemini Models has no generate_content_stream method, skipping")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
original_generate_content_stream = Models.generate_content_stream
|
|
318
|
+
_original_generate_content_stream = original_generate_content_stream
|
|
319
|
+
|
|
320
|
+
@functools.wraps(original_generate_content_stream)
|
|
321
|
+
def patched_generate_content_stream(
|
|
322
|
+
self: Models, *args: Any, **kwargs: Any
|
|
323
|
+
) -> Any:
|
|
324
|
+
stream = original_generate_content_stream(self, *args, **kwargs)
|
|
325
|
+
|
|
326
|
+
context = get_context()
|
|
327
|
+
|
|
328
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
329
|
+
|
|
330
|
+
config = kwargs.get("config")
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
return _track_stream(stream, model, context, config)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Error wrapping Gemini stream: {e}", exc_info=True)
|
|
336
|
+
return stream
|
|
337
|
+
|
|
338
|
+
Models.generate_content_stream = patched_generate_content_stream # type: ignore[assignment]
|
|
339
|
+
_patched_stream_sync = True
|
|
340
|
+
logger.info("Gemini streaming SDK patched for auto-instrumentation")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ============================================================================
|
|
344
|
+
# Async Patching
|
|
345
|
+
# ============================================================================
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def patch_gemini_async() -> None:
|
|
349
|
+
"""
|
|
350
|
+
Patch Gemini async client to auto-track usage.
|
|
351
|
+
|
|
352
|
+
This patches the asynchronous `AsyncModels.generate_content` method.
|
|
353
|
+
"""
|
|
354
|
+
global _patched_async, _original_generate_content_async
|
|
355
|
+
|
|
356
|
+
if _patched_async:
|
|
357
|
+
logger.debug("Gemini async already patched, skipping")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
from google.genai.models import AsyncModels
|
|
362
|
+
except ImportError:
|
|
363
|
+
logger.warning(
|
|
364
|
+
"Gemini SDK not installed. Install with: pip install 'fenra[gemini]'"
|
|
365
|
+
)
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
original_generate_content = AsyncModels.generate_content
|
|
369
|
+
_original_generate_content_async = original_generate_content
|
|
370
|
+
|
|
371
|
+
@functools.wraps(original_generate_content)
|
|
372
|
+
async def patched_generate_content(
|
|
373
|
+
self: AsyncModels, *args: Any, **kwargs: Any
|
|
374
|
+
) -> Any:
|
|
375
|
+
response = await original_generate_content(self, *args, **kwargs)
|
|
376
|
+
|
|
377
|
+
context = get_context()
|
|
378
|
+
|
|
379
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
380
|
+
|
|
381
|
+
config = kwargs.get("config")
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
_track_generate_content_response(response, model, context, config)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.error(f"Error tracking Gemini async usage: {e}", exc_info=True)
|
|
387
|
+
|
|
388
|
+
return response
|
|
389
|
+
|
|
390
|
+
AsyncModels.generate_content = patched_generate_content # type: ignore[assignment]
|
|
391
|
+
_patched_async = True
|
|
392
|
+
logger.info("Gemini async SDK patched for auto-instrumentation")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def patch_gemini_stream_async() -> None:
|
|
396
|
+
"""
|
|
397
|
+
Patch Gemini async client to auto-track streaming usage.
|
|
398
|
+
|
|
399
|
+
This patches the asynchronous `AsyncModels.generate_content_stream` method.
|
|
400
|
+
"""
|
|
401
|
+
global _patched_stream_async, _original_generate_content_stream_async
|
|
402
|
+
|
|
403
|
+
if _patched_stream_async:
|
|
404
|
+
logger.debug("Gemini async stream already patched, skipping")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
from google.genai.models import AsyncModels
|
|
409
|
+
except ImportError:
|
|
410
|
+
logger.warning(
|
|
411
|
+
"Gemini SDK not installed. Install with: pip install 'fenra[gemini]'"
|
|
412
|
+
)
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
if not hasattr(AsyncModels, "generate_content_stream"):
|
|
416
|
+
logger.debug(
|
|
417
|
+
"Gemini AsyncModels has no generate_content_stream method, skipping"
|
|
418
|
+
)
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
original_generate_content_stream = AsyncModels.generate_content_stream
|
|
422
|
+
_original_generate_content_stream_async = original_generate_content_stream
|
|
423
|
+
|
|
424
|
+
@functools.wraps(original_generate_content_stream)
|
|
425
|
+
async def patched_generate_content_stream(
|
|
426
|
+
self: AsyncModels, *args: Any, **kwargs: Any
|
|
427
|
+
) -> Any:
|
|
428
|
+
stream = await original_generate_content_stream(self, *args, **kwargs)
|
|
429
|
+
|
|
430
|
+
context = get_context()
|
|
431
|
+
|
|
432
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
433
|
+
|
|
434
|
+
config = kwargs.get("config")
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
return _track_stream_async(stream, model, context, config)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Error wrapping Gemini async stream: {e}", exc_info=True)
|
|
440
|
+
return stream
|
|
441
|
+
|
|
442
|
+
AsyncModels.generate_content_stream = patched_generate_content_stream # type: ignore[assignment]
|
|
443
|
+
_patched_stream_async = True
|
|
444
|
+
logger.info("Gemini async streaming SDK patched for auto-instrumentation")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ============================================================================
|
|
448
|
+
# Unpatch Functions
|
|
449
|
+
# ============================================================================
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def unpatch_gemini() -> None:
|
|
453
|
+
"""Restore original Gemini Models.generate_content method."""
|
|
454
|
+
global _patched_sync, _original_generate_content
|
|
455
|
+
|
|
456
|
+
if not _patched_sync or _original_generate_content is None:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
from google.genai.models import Models
|
|
461
|
+
|
|
462
|
+
Models.generate_content = _original_generate_content # type: ignore[assignment]
|
|
463
|
+
_patched_sync = False
|
|
464
|
+
_original_generate_content = None
|
|
465
|
+
logger.info("Gemini SDK unpatched")
|
|
466
|
+
except ImportError:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def unpatch_gemini_stream() -> None:
|
|
471
|
+
"""Restore original Gemini Models.generate_content_stream method."""
|
|
472
|
+
global _patched_stream_sync, _original_generate_content_stream
|
|
473
|
+
|
|
474
|
+
if not _patched_stream_sync or _original_generate_content_stream is None:
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
from google.genai.models import Models
|
|
479
|
+
|
|
480
|
+
Models.generate_content_stream = _original_generate_content_stream # type: ignore[assignment]
|
|
481
|
+
_patched_stream_sync = False
|
|
482
|
+
_original_generate_content_stream = None
|
|
483
|
+
logger.info("Gemini streaming SDK unpatched")
|
|
484
|
+
except ImportError:
|
|
485
|
+
pass
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def unpatch_gemini_async() -> None:
|
|
489
|
+
"""Restore original Gemini AsyncModels.generate_content method."""
|
|
490
|
+
global _patched_async, _original_generate_content_async
|
|
491
|
+
|
|
492
|
+
if not _patched_async or _original_generate_content_async is None:
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
from google.genai.models import AsyncModels
|
|
497
|
+
|
|
498
|
+
AsyncModels.generate_content = _original_generate_content_async # type: ignore[assignment]
|
|
499
|
+
_patched_async = False
|
|
500
|
+
_original_generate_content_async = None
|
|
501
|
+
logger.info("Gemini async SDK unpatched")
|
|
502
|
+
except ImportError:
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def unpatch_gemini_stream_async() -> None:
|
|
507
|
+
"""Restore original Gemini AsyncModels.generate_content_stream method."""
|
|
508
|
+
global _patched_stream_async, _original_generate_content_stream_async
|
|
509
|
+
|
|
510
|
+
if not _patched_stream_async or _original_generate_content_stream_async is None:
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
from google.genai.models import AsyncModels
|
|
515
|
+
|
|
516
|
+
AsyncModels.generate_content_stream = _original_generate_content_stream_async # type: ignore[assignment]
|
|
517
|
+
_patched_stream_async = False
|
|
518
|
+
_original_generate_content_stream_async = None
|
|
519
|
+
logger.info("Gemini async streaming SDK unpatched")
|
|
520
|
+
except ImportError:
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def unpatch_gemini_all() -> None:
|
|
525
|
+
"""Restore all original Gemini SDK methods."""
|
|
526
|
+
unpatch_gemini()
|
|
527
|
+
unpatch_gemini_stream()
|
|
528
|
+
unpatch_gemini_async()
|
|
529
|
+
unpatch_gemini_stream_async()
|