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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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)
|