omnimancer-cli 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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,708 @@
1
+ """
2
+ Google Gemini provider implementation for Omnimancer.
3
+
4
+ This module provides the Google Gemini AI provider implementation using Google AI Studio API.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Dict, List
9
+
10
+ import httpx
11
+
12
+ from ..core.models import (
13
+ ChatContext,
14
+ ChatResponse,
15
+ ModelInfo,
16
+ ToolCall,
17
+ ToolDefinition,
18
+ )
19
+ from ..utils.errors import (
20
+ AuthenticationError,
21
+ ModelNotFoundError,
22
+ NetworkError,
23
+ ProviderError,
24
+ ProviderUnavailableError,
25
+ QuotaExceededError,
26
+ RateLimitError,
27
+ )
28
+ from .base import BaseProvider
29
+
30
+
31
+ class GeminiProvider(BaseProvider):
32
+ """
33
+ Google Gemini AI provider implementation using Google AI Studio API.
34
+ """
35
+
36
+ BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
37
+
38
+ def __init__(self, api_key: str, model: str = "", **kwargs):
39
+ """
40
+ Initialize Gemini provider.
41
+
42
+ Args:
43
+ api_key: Google AI Studio API key
44
+ model: Gemini model to use (e.g., 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-2.0-flash-exp')
45
+ **kwargs: Additional configuration
46
+ """
47
+ super().__init__(api_key, model or "gemini-1.5-pro", **kwargs)
48
+ self.max_tokens = kwargs.get("max_tokens", 8192)
49
+ self.temperature = kwargs.get("temperature", 0.7)
50
+
51
+ async def send_message(self, message: str, context: ChatContext) -> ChatResponse:
52
+ """
53
+ Send a message to Gemini API.
54
+
55
+ Args:
56
+ message: User message
57
+ context: Conversation context
58
+
59
+ Returns:
60
+ ChatResponse with Gemini's reply
61
+ """
62
+ try:
63
+ # Prepare messages for Gemini API
64
+ contents = self._prepare_contents(message, context)
65
+
66
+ # Make API request
67
+ async with httpx.AsyncClient() as client:
68
+ response = await client.post(
69
+ f"{self.BASE_URL}/models/{self.model}:generateContent",
70
+ headers={"Content-Type": "application/json"},
71
+ params={"key": self.api_key},
72
+ json={
73
+ "contents": contents,
74
+ "generationConfig": {
75
+ "maxOutputTokens": self.max_tokens,
76
+ "temperature": self.temperature,
77
+ },
78
+ },
79
+ timeout=30.0,
80
+ )
81
+
82
+ # Handle response
83
+ return self._handle_response(response)
84
+
85
+ except httpx.TimeoutException:
86
+ raise NetworkError("Request to Gemini API timed out")
87
+ except httpx.RequestError as e:
88
+ raise NetworkError(f"Network error: {e}")
89
+ except (
90
+ AuthenticationError,
91
+ RateLimitError,
92
+ ModelNotFoundError,
93
+ ProviderError,
94
+ ):
95
+ # Re-raise our custom exceptions from _handle_response
96
+ raise
97
+ except Exception as e:
98
+ raise ProviderError(f"Unexpected error: {e}")
99
+
100
+ async def validate_credentials(self) -> bool:
101
+ """
102
+ Validate Gemini API credentials by making a test request.
103
+
104
+ Returns:
105
+ True if credentials are valid
106
+
107
+ Raises:
108
+ AuthenticationError: If API key is invalid
109
+ NetworkError: If network request fails
110
+ ProviderError: For other validation errors
111
+ """
112
+ if not self.api_key:
113
+ raise AuthenticationError("Gemini API key is required")
114
+
115
+ if not self.api_key.startswith("AIza"):
116
+ raise AuthenticationError(
117
+ "Invalid Gemini API key format - should start with 'AIza'"
118
+ )
119
+
120
+ try:
121
+ # Test with a minimal request to validate credentials
122
+ async with httpx.AsyncClient() as client:
123
+ response = await client.post(
124
+ f"{self.BASE_URL}/models/{self.model}:generateContent",
125
+ headers={"Content-Type": "application/json"},
126
+ params={"key": self.api_key},
127
+ json={
128
+ "contents": [{"parts": [{"text": "test"}]}],
129
+ "generationConfig": {
130
+ "maxOutputTokens": 1,
131
+ "temperature": 0,
132
+ },
133
+ },
134
+ timeout=10.0,
135
+ )
136
+
137
+ if response.status_code == 200:
138
+ return True
139
+ elif response.status_code == 400:
140
+ try:
141
+ error_data = response.json()
142
+ error_msg = error_data.get("error", {}).get("message", "")
143
+ if "API_KEY" in error_msg.upper() or "INVALID" in error_msg.upper():
144
+ raise AuthenticationError(
145
+ f"Invalid Gemini API key: {error_msg}"
146
+ )
147
+ else:
148
+ raise ProviderError(f"Gemini API validation error: {error_msg}")
149
+ except (ValueError, KeyError):
150
+ raise ProviderError(
151
+ "Invalid response from Gemini API during validation"
152
+ )
153
+ elif response.status_code == 403:
154
+ raise AuthenticationError(
155
+ "Gemini API access forbidden - check API key permissions"
156
+ )
157
+ elif response.status_code == 404:
158
+ raise ModelNotFoundError(
159
+ f"Gemini model '{self.model}' not found or not accessible"
160
+ )
161
+ else:
162
+ try:
163
+ error_data = response.json()
164
+ error_msg = error_data.get("error", {}).get(
165
+ "message", f"HTTP {response.status_code}"
166
+ )
167
+ except:
168
+ error_msg = f"HTTP {response.status_code}"
169
+ raise ProviderError(f"Gemini API validation failed: {error_msg}")
170
+
171
+ except httpx.TimeoutException:
172
+ raise NetworkError("Gemini API validation request timed out")
173
+ except httpx.RequestError as e:
174
+ raise NetworkError(f"Network error during Gemini API validation: {e}")
175
+ except (
176
+ AuthenticationError,
177
+ NetworkError,
178
+ ProviderError,
179
+ ModelNotFoundError,
180
+ ):
181
+ # Re-raise our custom exceptions
182
+ raise
183
+ except Exception as e:
184
+ raise ProviderError(f"Unexpected error during Gemini API validation: {e}")
185
+
186
+ def _prepare_contents(self, message: str, context: ChatContext) -> List[Dict]:
187
+ """
188
+ Prepare contents for Gemini API format.
189
+
190
+ Args:
191
+ message: Current user message
192
+ context: Conversation context
193
+
194
+ Returns:
195
+ List of contents formatted for Gemini API
196
+ """
197
+ contents = []
198
+
199
+ # Add context messages
200
+ for msg in context.messages:
201
+ role = "user" if msg.role.value == "user" else "model"
202
+ contents.append({"role": role, "parts": [{"text": msg.content}]})
203
+
204
+ # Add current message
205
+ contents.append({"role": "user", "parts": [{"text": message}]})
206
+
207
+ return contents
208
+
209
+ def _handle_response(self, response: httpx.Response) -> ChatResponse:
210
+ """
211
+ Handle Gemini API response with comprehensive error handling.
212
+
213
+ Args:
214
+ response: HTTP response from Gemini API
215
+
216
+ Returns:
217
+ ChatResponse object
218
+
219
+ Raises:
220
+ Various provider errors based on response status
221
+ """
222
+ if response.status_code == 200:
223
+ try:
224
+ data = response.json()
225
+ candidates = data.get("candidates", [])
226
+
227
+ if candidates and len(candidates) > 0:
228
+ candidate = candidates[0]
229
+
230
+ # Check for safety filtering or other blocking
231
+ finish_reason = candidate.get("finishReason")
232
+ if finish_reason and finish_reason != "STOP":
233
+ if finish_reason == "SAFETY":
234
+ raise ProviderError(
235
+ "Response blocked by Gemini safety filters. Try rephrasing your request.",
236
+ provider="gemini",
237
+ )
238
+ elif finish_reason == "MAX_TOKENS":
239
+ raise ProviderError(
240
+ "Response truncated due to token limit. Consider reducing context or max_tokens.",
241
+ provider="gemini",
242
+ )
243
+ else:
244
+ raise ProviderError(
245
+ f"Response generation stopped: {finish_reason}",
246
+ provider="gemini",
247
+ )
248
+
249
+ content = candidate.get("content", {})
250
+ parts = content.get("parts", [])
251
+
252
+ if parts and len(parts) > 0:
253
+ text_content = parts[0].get("text", "")
254
+ usage_metadata = data.get("usageMetadata", {})
255
+
256
+ return ChatResponse(
257
+ content=text_content,
258
+ model_used=self.model,
259
+ tokens_used=usage_metadata.get("totalTokenCount", 0),
260
+ timestamp=datetime.now(),
261
+ )
262
+ else:
263
+ raise ProviderError(
264
+ "Empty parts in Gemini API response",
265
+ provider="gemini",
266
+ )
267
+ else:
268
+ raise ProviderError(
269
+ "Empty candidates in Gemini API response",
270
+ provider="gemini",
271
+ )
272
+ except (ValueError, KeyError) as e:
273
+ raise ProviderError(
274
+ f"Invalid JSON response from Gemini API: {e}",
275
+ provider="gemini",
276
+ )
277
+
278
+ elif response.status_code == 400:
279
+ try:
280
+ error_data = response.json()
281
+ error_info = error_data.get("error", {})
282
+ error_msg = error_info.get("message", "Bad request")
283
+ error_code = error_info.get("code")
284
+
285
+ if (
286
+ "API_KEY" in error_msg.upper()
287
+ or "INVALID_API_KEY" in error_msg.upper()
288
+ ):
289
+ raise AuthenticationError(
290
+ "Invalid Gemini API key. Check your Google AI Studio API key.",
291
+ provider="gemini",
292
+ )
293
+ elif "QUOTA" in error_msg.upper() or error_code == 429:
294
+ raise QuotaExceededError(
295
+ "Gemini API quota exceeded. Check your usage limits.",
296
+ provider="gemini",
297
+ )
298
+ elif "MODEL_NOT_FOUND" in error_msg.upper():
299
+ available_models = [m.name for m in self.get_available_models()]
300
+ raise ModelNotFoundError(
301
+ f"Gemini model '{self.model}' not found",
302
+ provider="gemini",
303
+ model_name=self.model,
304
+ available_models=available_models,
305
+ )
306
+ else:
307
+ raise ProviderError(
308
+ f"Gemini API error: {error_msg}", provider="gemini"
309
+ )
310
+ except (ValueError, KeyError):
311
+ raise ProviderError("Bad request to Gemini API", provider="gemini")
312
+
313
+ elif response.status_code == 401:
314
+ raise AuthenticationError(
315
+ "Gemini API authentication failed. Verify your API key.",
316
+ provider="gemini",
317
+ )
318
+
319
+ elif response.status_code == 403:
320
+ try:
321
+ error_data = response.json()
322
+ error_msg = error_data.get("error", {}).get("message", "")
323
+ if "PERMISSION_DENIED" in error_msg:
324
+ raise AuthenticationError(
325
+ "Gemini API access denied. Check API key permissions and billing.",
326
+ provider="gemini",
327
+ )
328
+ else:
329
+ raise AuthenticationError(
330
+ "Gemini API access forbidden - check API key permissions",
331
+ provider="gemini",
332
+ )
333
+ except (ValueError, KeyError):
334
+ raise AuthenticationError(
335
+ "Gemini API access forbidden - check API key permissions",
336
+ provider="gemini",
337
+ )
338
+
339
+ elif response.status_code == 429:
340
+ # Extract retry-after header if available
341
+ retry_after = response.headers.get("Retry-After")
342
+ if retry_after:
343
+ try:
344
+ retry_seconds = int(retry_after)
345
+ raise RateLimitError(
346
+ "Gemini API rate limit exceeded",
347
+ provider="gemini",
348
+ retry_after=retry_seconds,
349
+ )
350
+ except ValueError:
351
+ pass
352
+
353
+ raise RateLimitError(
354
+ "Gemini API rate limit exceeded. Wait before retrying.",
355
+ provider="gemini",
356
+ )
357
+
358
+ elif response.status_code == 404:
359
+ available_models = [m.name for m in self.get_available_models()]
360
+ raise ModelNotFoundError(
361
+ f"Gemini model '{self.model}' not found or not accessible",
362
+ provider="gemini",
363
+ model_name=self.model,
364
+ available_models=available_models,
365
+ )
366
+
367
+ elif response.status_code == 500:
368
+ raise ProviderUnavailableError(
369
+ "Gemini API server error. Try again later.",
370
+ provider="gemini",
371
+ estimated_recovery="a few minutes",
372
+ )
373
+
374
+ elif response.status_code == 502 or response.status_code == 503:
375
+ raise ProviderUnavailableError(
376
+ "Gemini API temporarily unavailable. Service may be under maintenance.",
377
+ provider="gemini",
378
+ estimated_recovery="10-30 minutes",
379
+ )
380
+
381
+ else:
382
+ try:
383
+ error_data = response.json()
384
+ error_msg = error_data.get("error", {}).get("message", "Unknown error")
385
+ except:
386
+ error_msg = f"HTTP {response.status_code}"
387
+
388
+ raise ProviderError(f"Gemini API error: {error_msg}", provider="gemini")
389
+
390
+ def get_model_info(self) -> ModelInfo:
391
+ """
392
+ Get information about the current Gemini model.
393
+ """
394
+ model_configs = {
395
+ "gemini-1.5-pro": {
396
+ "description": "Gemini 1.5 Pro - Most capable multimodal model",
397
+ "max_tokens": 2097152, # 2M tokens context
398
+ "cost_per_token": 0.0000035, # Approximate cost per token
399
+ "supports_tools": True,
400
+ "supports_multimodal": True,
401
+ },
402
+ "gemini-1.5-flash": {
403
+ "description": "Gemini 1.5 Flash - Fast and efficient multimodal model",
404
+ "max_tokens": 1048576, # 1M tokens context
405
+ "cost_per_token": 0.00000035, # Approximate cost per token
406
+ "supports_tools": True,
407
+ "supports_multimodal": True,
408
+ },
409
+ "gemini-1.0-pro": {
410
+ "description": "Gemini 1.0 Pro - Reliable text generation model",
411
+ "max_tokens": 32768,
412
+ "cost_per_token": 0.0000005, # Approximate cost per token
413
+ "supports_tools": False,
414
+ "supports_multimodal": False,
415
+ },
416
+ }
417
+
418
+ config = model_configs.get(
419
+ self.model,
420
+ {
421
+ "description": f"Gemini model {self.model}",
422
+ "max_tokens": 32768,
423
+ "cost_per_token": 0.0000035,
424
+ "supports_tools": True,
425
+ "supports_multimodal": True,
426
+ },
427
+ )
428
+
429
+ return ModelInfo(
430
+ name=self.model,
431
+ provider="gemini",
432
+ description=config["description"],
433
+ max_tokens=config["max_tokens"],
434
+ cost_per_token=config["cost_per_token"],
435
+ available=True,
436
+ supports_tools=config["supports_tools"],
437
+ supports_multimodal=config["supports_multimodal"],
438
+ latest_version=self.model == "gemini-1.5-pro",
439
+ )
440
+
441
+ def get_available_models(self) -> List[ModelInfo]:
442
+ """
443
+ Get list of available Gemini models.
444
+ """
445
+ return [
446
+ ModelInfo(
447
+ name="gemini-1.5-pro",
448
+ provider="gemini",
449
+ description="Gemini 1.5 Pro - Most capable multimodal model",
450
+ max_tokens=2097152,
451
+ cost_per_token=0.0000035,
452
+ available=True,
453
+ supports_tools=True,
454
+ supports_multimodal=True,
455
+ latest_version=True,
456
+ ),
457
+ ModelInfo(
458
+ name="gemini-1.5-flash",
459
+ provider="gemini",
460
+ description="Gemini 1.5 Flash - Fast and efficient multimodal model",
461
+ max_tokens=1048576,
462
+ cost_per_token=0.00000035,
463
+ available=True,
464
+ supports_tools=True,
465
+ supports_multimodal=True,
466
+ ),
467
+ ModelInfo(
468
+ name="gemini-1.0-pro",
469
+ provider="gemini",
470
+ description="Gemini 1.0 Pro - Reliable text generation model",
471
+ max_tokens=32768,
472
+ cost_per_token=0.0000005,
473
+ available=True,
474
+ supports_tools=False,
475
+ supports_multimodal=False,
476
+ ),
477
+ ]
478
+
479
+ async def fetch_live_models(self) -> List[ModelInfo]:
480
+ """
481
+ Fetch live model list from Google AI API.
482
+
483
+ Returns:
484
+ List of ModelInfo objects from Google AI API
485
+ """
486
+ try:
487
+ # Use the models.list endpoint
488
+ async with httpx.AsyncClient() as client:
489
+ response = await client.get(
490
+ f"{self.BASE_URL}/models",
491
+ params={"key": self.api_key},
492
+ timeout=30.0,
493
+ )
494
+ response.raise_for_status()
495
+
496
+ data = response.json()
497
+ models = []
498
+
499
+ # Parse models from API response
500
+ for model_data in data.get("models", []):
501
+ model_name = model_data.get("name", "").replace("models/", "")
502
+
503
+ # Only include generative models
504
+ if "gemini" in model_name.lower():
505
+ # Extract model capabilities
506
+ supported_methods = model_data.get(
507
+ "supportedGenerationMethods", []
508
+ )
509
+ supports_tools = "generateContent" in supported_methods
510
+
511
+ # Determine if multimodal based on model name
512
+ supports_multimodal = (
513
+ "vision" in model_name or "1.5" in model_name
514
+ )
515
+
516
+ # Get token limits
517
+ model_data.get("inputTokenLimit", 32768)
518
+ output_limit = model_data.get("outputTokenLimit", 8192)
519
+
520
+ # Estimate costs (these are approximate)
521
+ cost_per_token = 0.0000005 # Default
522
+ if "pro" in model_name.lower():
523
+ cost_per_token = 0.0000035
524
+ elif "flash" in model_name.lower():
525
+ cost_per_token = 0.00000035
526
+
527
+ models.append(
528
+ ModelInfo(
529
+ name=model_name,
530
+ provider="gemini",
531
+ description=model_data.get("displayName", model_name),
532
+ max_tokens=output_limit,
533
+ cost_per_token=cost_per_token,
534
+ available=True,
535
+ supports_tools=supports_tools,
536
+ supports_multimodal=supports_multimodal,
537
+ latest_version="latest"
538
+ in model_data.get("version", ""),
539
+ )
540
+ )
541
+
542
+ return models if models else self.get_available_models()
543
+
544
+ except Exception:
545
+ # Fall back to static model list if API call fails
546
+ return self.get_available_models()
547
+
548
+ def supports_tools(self) -> bool:
549
+ """
550
+ Check if Gemini provider supports tool calling.
551
+
552
+ Returns:
553
+ True for newer models, False for gemini-1.0-pro
554
+ """
555
+ # Only newer Gemini models support function calling
556
+ return self.model != "gemini-1.0-pro"
557
+
558
+ def supports_multimodal(self) -> bool:
559
+ """
560
+ Check if Gemini provider supports multimodal inputs.
561
+
562
+ Returns:
563
+ True for newer models, False for gemini-1.0-pro
564
+ """
565
+ # Only newer Gemini models support multimodal inputs
566
+ return self.model != "gemini-1.0-pro"
567
+
568
+ async def send_message_with_tools(
569
+ self,
570
+ message: str,
571
+ context: ChatContext,
572
+ available_tools: List[ToolDefinition],
573
+ ) -> ChatResponse:
574
+ """
575
+ Send a message with available tools for Gemini to use.
576
+
577
+ Args:
578
+ message: The user's message
579
+ context: Current conversation context
580
+ available_tools: List of tools available for the AI to use
581
+
582
+ Returns:
583
+ ChatResponse with the AI's reply and any tool calls
584
+ """
585
+ # If model doesn't support tools, fall back to regular message sending
586
+ if not self.supports_tools():
587
+ return await self.send_message(message, context)
588
+
589
+ try:
590
+ # Prepare messages for Gemini API
591
+ contents = self._prepare_contents(message, context)
592
+
593
+ # Convert tools to Gemini function format
594
+ tools = self._convert_tools_to_gemini_format(available_tools)
595
+
596
+ # Make API request with tools
597
+ async with httpx.AsyncClient() as client:
598
+ request_data = {
599
+ "contents": contents,
600
+ "generationConfig": {
601
+ "maxOutputTokens": self.max_tokens,
602
+ "temperature": self.temperature,
603
+ },
604
+ }
605
+
606
+ # Add tools if available
607
+ if tools:
608
+ request_data["tools"] = tools
609
+
610
+ response = await client.post(
611
+ f"{self.BASE_URL}/models/{self.model}:generateContent",
612
+ headers={"Content-Type": "application/json"},
613
+ params={"key": self.api_key},
614
+ json=request_data,
615
+ timeout=30.0,
616
+ )
617
+
618
+ # Handle response with potential tool calls
619
+ return self._handle_response_with_tools(response)
620
+
621
+ except httpx.TimeoutException:
622
+ raise NetworkError("Request to Gemini API timed out")
623
+ except httpx.RequestError as e:
624
+ raise NetworkError(f"Network error: {e}")
625
+ except Exception as e:
626
+ raise ProviderError(f"Unexpected error: {e}")
627
+
628
+ def _convert_tools_to_gemini_format(
629
+ self, tools: List[ToolDefinition]
630
+ ) -> List[Dict]:
631
+ """
632
+ Convert ToolDefinition objects to Gemini function calling format.
633
+
634
+ Args:
635
+ tools: List of tool definitions
636
+
637
+ Returns:
638
+ List of tools formatted for Gemini API
639
+ """
640
+ if not tools:
641
+ return []
642
+
643
+ gemini_tools = []
644
+ function_declarations = []
645
+
646
+ for tool in tools:
647
+ function_declaration = {
648
+ "name": tool.name,
649
+ "description": tool.description,
650
+ "parameters": tool.parameters,
651
+ }
652
+ function_declarations.append(function_declaration)
653
+
654
+ if function_declarations:
655
+ gemini_tools.append({"function_declarations": function_declarations})
656
+
657
+ return gemini_tools
658
+
659
+ def _handle_response_with_tools(self, response: httpx.Response) -> ChatResponse:
660
+ """
661
+ Handle Gemini API response that may contain tool calls.
662
+
663
+ Args:
664
+ response: HTTP response from Gemini API
665
+
666
+ Returns:
667
+ ChatResponse object with potential tool calls
668
+ """
669
+ if response.status_code == 200:
670
+ data = response.json()
671
+ candidates = data.get("candidates", [])
672
+
673
+ if candidates and len(candidates) > 0:
674
+ candidate = candidates[0]
675
+ content = candidate.get("content", {})
676
+ parts = content.get("parts", [])
677
+
678
+ text_content = ""
679
+ tool_calls = []
680
+
681
+ # Process all parts in the response
682
+ for part in parts:
683
+ if "text" in part:
684
+ text_content += part["text"]
685
+ elif "functionCall" in part:
686
+ # Extract function call information
687
+ function_call = part["functionCall"]
688
+ tool_call = ToolCall(
689
+ name=function_call.get("name", ""),
690
+ arguments=function_call.get("args", {}),
691
+ server_name=None, # Will be set by MCP manager
692
+ )
693
+ tool_calls.append(tool_call)
694
+
695
+ usage_metadata = data.get("usageMetadata", {})
696
+
697
+ return ChatResponse(
698
+ content=text_content,
699
+ model_used=self.model,
700
+ tokens_used=usage_metadata.get("totalTokenCount", 0),
701
+ timestamp=datetime.now(),
702
+ tool_calls=tool_calls if tool_calls else None,
703
+ )
704
+ else:
705
+ raise ProviderError("Empty candidates in Gemini API response")
706
+ else:
707
+ # Use the existing error handling for non-200 responses
708
+ return self._handle_response(response)