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,510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ollama provider implementation for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module provides the Ollama provider implementation for local AI inference
|
|
5
|
+
using the Ollama server API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from ..core.models import ChatContext, ChatResponse, ModelInfo
|
|
14
|
+
from ..utils.errors import (
|
|
15
|
+
ModelNotFoundError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
ProviderConfigurationError,
|
|
18
|
+
ProviderError,
|
|
19
|
+
ProviderUnavailableError,
|
|
20
|
+
)
|
|
21
|
+
from .base import BaseProvider
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OllamaProvider(BaseProvider):
|
|
25
|
+
"""
|
|
26
|
+
Ollama provider implementation for local AI inference.
|
|
27
|
+
|
|
28
|
+
This provider connects to a local Ollama server to run AI models
|
|
29
|
+
locally without requiring external API keys.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, api_key: str = "", model: str = "", **kwargs):
|
|
33
|
+
"""
|
|
34
|
+
Initialize Ollama provider.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
api_key: Not used for Ollama (local server)
|
|
38
|
+
model: Ollama model to use (e.g., 'llama2', 'llama3', 'mistral', 'codellama')
|
|
39
|
+
**kwargs: Additional configuration including base_url
|
|
40
|
+
"""
|
|
41
|
+
# Ollama doesn't require an API key, but we maintain the interface
|
|
42
|
+
# If no model specified, default to empty string for user to set later
|
|
43
|
+
super().__init__(api_key or "local", model or "", **kwargs)
|
|
44
|
+
self.base_url = kwargs.get("base_url", "http://localhost:11434")
|
|
45
|
+
self.max_tokens = kwargs.get("max_tokens", 4096)
|
|
46
|
+
self.temperature = kwargs.get("temperature", 0.7)
|
|
47
|
+
self.timeout = kwargs.get("timeout", 60.0) # Ollama can be slower
|
|
48
|
+
|
|
49
|
+
async def send_message(self, message: str, context: ChatContext) -> ChatResponse:
|
|
50
|
+
"""
|
|
51
|
+
Send a message to Ollama server.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
message: User message
|
|
55
|
+
context: Conversation context
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ChatResponse with Ollama's reply
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
# Check if server is available first
|
|
62
|
+
await self._check_server_availability()
|
|
63
|
+
|
|
64
|
+
# Prepare messages for Ollama API
|
|
65
|
+
messages = self._prepare_messages(message, context)
|
|
66
|
+
|
|
67
|
+
# Make API request to Ollama
|
|
68
|
+
async with httpx.AsyncClient() as client:
|
|
69
|
+
response = await client.post(
|
|
70
|
+
f"{self.base_url}/api/chat",
|
|
71
|
+
json={
|
|
72
|
+
"model": self.model,
|
|
73
|
+
"messages": messages,
|
|
74
|
+
"stream": False,
|
|
75
|
+
"options": {
|
|
76
|
+
"temperature": self.temperature,
|
|
77
|
+
"num_predict": self.max_tokens,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
timeout=self.timeout,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Handle response
|
|
84
|
+
return self._handle_response(response)
|
|
85
|
+
|
|
86
|
+
except httpx.TimeoutException:
|
|
87
|
+
raise NetworkError(
|
|
88
|
+
f"Request to Ollama server timed out after {self.timeout}s. "
|
|
89
|
+
"Local inference can take longer than cloud APIs.",
|
|
90
|
+
provider="ollama",
|
|
91
|
+
)
|
|
92
|
+
except httpx.ConnectError:
|
|
93
|
+
raise NetworkError(
|
|
94
|
+
f"Cannot connect to Ollama server at {self.base_url}. "
|
|
95
|
+
"Make sure Ollama is running with 'ollama serve'.",
|
|
96
|
+
provider="ollama",
|
|
97
|
+
)
|
|
98
|
+
except httpx.RequestError as e:
|
|
99
|
+
raise NetworkError(
|
|
100
|
+
f"Network error connecting to Ollama: {e}", provider="ollama"
|
|
101
|
+
)
|
|
102
|
+
except (NetworkError, ModelNotFoundError, ProviderError):
|
|
103
|
+
# Re-raise our custom errors without wrapping them
|
|
104
|
+
raise
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise ProviderError(f"Unexpected error with Ollama: {e}", provider="ollama")
|
|
107
|
+
|
|
108
|
+
async def validate_credentials(self) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Validate Ollama server connection.
|
|
111
|
+
|
|
112
|
+
Since Ollama doesn't use API keys, this checks if the server
|
|
113
|
+
is running and the model is available.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if server is accessible and model is available
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
# Check if server is running
|
|
120
|
+
await self._check_server_availability()
|
|
121
|
+
|
|
122
|
+
# Check if the model is available
|
|
123
|
+
available_models = await self._get_server_models()
|
|
124
|
+
model_names = [model.get("name", "") for model in available_models]
|
|
125
|
+
|
|
126
|
+
return self.model in model_names
|
|
127
|
+
|
|
128
|
+
except Exception:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
async def _check_server_availability(self) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Check if Ollama server is running and accessible.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
NetworkError: If server is not accessible
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
async with httpx.AsyncClient() as client:
|
|
140
|
+
response = await client.get(f"{self.base_url}/api/tags", timeout=5.0)
|
|
141
|
+
|
|
142
|
+
if response.status_code != 200:
|
|
143
|
+
raise NetworkError(
|
|
144
|
+
f"Ollama server returned status {response.status_code}",
|
|
145
|
+
provider="ollama",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except httpx.ConnectError:
|
|
149
|
+
raise NetworkError(
|
|
150
|
+
f"Cannot connect to Ollama server at {self.base_url}. "
|
|
151
|
+
"Make sure Ollama is running with 'ollama serve'.",
|
|
152
|
+
provider="ollama",
|
|
153
|
+
)
|
|
154
|
+
except httpx.TimeoutException:
|
|
155
|
+
raise NetworkError(
|
|
156
|
+
"Ollama server is not responding. Check if it's running.",
|
|
157
|
+
provider="ollama",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def _get_server_models(self) -> List[Dict]:
|
|
161
|
+
"""
|
|
162
|
+
Get list of models from Ollama server.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of model dictionaries from Ollama API
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
NetworkError: If unable to fetch models
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
async with httpx.AsyncClient() as client:
|
|
172
|
+
response = await client.get(f"{self.base_url}/api/tags", timeout=10.0)
|
|
173
|
+
|
|
174
|
+
if response.status_code == 200:
|
|
175
|
+
data = response.json()
|
|
176
|
+
return data.get("models", [])
|
|
177
|
+
else:
|
|
178
|
+
raise NetworkError(
|
|
179
|
+
f"Failed to fetch models from Ollama server: HTTP {response.status_code}",
|
|
180
|
+
provider="ollama",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except httpx.RequestError as e:
|
|
184
|
+
raise NetworkError(
|
|
185
|
+
f"Error fetching models from Ollama: {e}", provider="ollama"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _prepare_messages(
|
|
189
|
+
self, message: str, context: ChatContext
|
|
190
|
+
) -> List[Dict[str, str]]:
|
|
191
|
+
"""
|
|
192
|
+
Prepare messages for Ollama API format.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
message: Current user message
|
|
196
|
+
context: Conversation context
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of messages formatted for Ollama API
|
|
200
|
+
"""
|
|
201
|
+
messages = []
|
|
202
|
+
|
|
203
|
+
# Add context messages
|
|
204
|
+
for msg in context.messages:
|
|
205
|
+
messages.append({"role": msg.role.value, "content": msg.content})
|
|
206
|
+
|
|
207
|
+
# Add current message
|
|
208
|
+
messages.append({"role": "user", "content": message})
|
|
209
|
+
|
|
210
|
+
return messages
|
|
211
|
+
|
|
212
|
+
def _handle_response(self, response: httpx.Response) -> ChatResponse:
|
|
213
|
+
"""
|
|
214
|
+
Handle Ollama API response with comprehensive error handling.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
response: HTTP response from Ollama API
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
ChatResponse object
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
Various provider errors based on response status
|
|
224
|
+
"""
|
|
225
|
+
if response.status_code == 200:
|
|
226
|
+
try:
|
|
227
|
+
data = response.json()
|
|
228
|
+
|
|
229
|
+
# Check for errors in successful response
|
|
230
|
+
if "error" in data:
|
|
231
|
+
error_msg = data["error"]
|
|
232
|
+
if "not found" in error_msg.lower():
|
|
233
|
+
raise ModelNotFoundError(
|
|
234
|
+
f"Model '{self.model}' not found. Use 'ollama pull {self.model}' to download it.",
|
|
235
|
+
provider="ollama",
|
|
236
|
+
model_name=self.model,
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
raise ProviderError(
|
|
240
|
+
f"Ollama error: {error_msg}", provider="ollama"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Ollama response format
|
|
244
|
+
message = data.get("message", {})
|
|
245
|
+
content = message.get("content", "")
|
|
246
|
+
|
|
247
|
+
if not content:
|
|
248
|
+
# Check if model is still loading
|
|
249
|
+
if data.get("done", True) is False:
|
|
250
|
+
raise ProviderError(
|
|
251
|
+
f"Model '{self.model}' is still loading. Please wait and try again.",
|
|
252
|
+
provider="ollama",
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
raise ProviderError(
|
|
256
|
+
"Empty response from Ollama", provider="ollama"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Check for completion status
|
|
260
|
+
done = data.get("done", True)
|
|
261
|
+
if not done:
|
|
262
|
+
raise ProviderError(
|
|
263
|
+
"Incomplete response from Ollama. The model may be overloaded.",
|
|
264
|
+
provider="ollama",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Ollama doesn't provide detailed token usage, estimate it
|
|
268
|
+
estimated_tokens = len(content.split()) * 1.3 # Rough estimate
|
|
269
|
+
|
|
270
|
+
return ChatResponse(
|
|
271
|
+
content=content,
|
|
272
|
+
model_used=self.model,
|
|
273
|
+
tokens_used=int(estimated_tokens),
|
|
274
|
+
timestamp=datetime.now(),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
except (KeyError, ValueError) as e:
|
|
278
|
+
raise ProviderError(
|
|
279
|
+
f"Invalid response format from Ollama: {e}",
|
|
280
|
+
provider="ollama",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
elif response.status_code == 400:
|
|
284
|
+
try:
|
|
285
|
+
error_data = response.json()
|
|
286
|
+
error_msg = error_data.get("error", "Bad request")
|
|
287
|
+
|
|
288
|
+
if "model" in error_msg.lower() and "not found" in error_msg.lower():
|
|
289
|
+
raise ModelNotFoundError(
|
|
290
|
+
f"Model '{self.model}' not found. Use 'ollama pull {self.model}' to download it.",
|
|
291
|
+
provider="ollama",
|
|
292
|
+
model_name=self.model,
|
|
293
|
+
)
|
|
294
|
+
elif (
|
|
295
|
+
"invalid" in error_msg.lower() and "parameter" in error_msg.lower()
|
|
296
|
+
):
|
|
297
|
+
raise ProviderConfigurationError(
|
|
298
|
+
f"Invalid parameter: {error_msg}",
|
|
299
|
+
provider="ollama",
|
|
300
|
+
suggested_fix="Check model parameters and options",
|
|
301
|
+
)
|
|
302
|
+
elif (
|
|
303
|
+
"context length" in error_msg.lower()
|
|
304
|
+
or "token" in error_msg.lower()
|
|
305
|
+
):
|
|
306
|
+
raise ProviderError(
|
|
307
|
+
f"Context too long: {error_msg}. Try reducing conversation history.",
|
|
308
|
+
provider="ollama",
|
|
309
|
+
)
|
|
310
|
+
else:
|
|
311
|
+
raise ProviderError(
|
|
312
|
+
f"Ollama API error: {error_msg}", provider="ollama"
|
|
313
|
+
)
|
|
314
|
+
except (ValueError, KeyError):
|
|
315
|
+
raise ProviderError("Bad request to Ollama API", provider="ollama")
|
|
316
|
+
|
|
317
|
+
elif response.status_code == 404:
|
|
318
|
+
# Model not found
|
|
319
|
+
try:
|
|
320
|
+
error_data = response.json()
|
|
321
|
+
error_msg = error_data.get("error", f"Model '{self.model}' not found")
|
|
322
|
+
except:
|
|
323
|
+
error_msg = f"Model '{self.model}' not found"
|
|
324
|
+
|
|
325
|
+
raise ModelNotFoundError(
|
|
326
|
+
f"{error_msg}. Use 'ollama pull {self.model}' to download it.",
|
|
327
|
+
provider="ollama",
|
|
328
|
+
model_name=self.model,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
elif response.status_code == 500:
|
|
332
|
+
try:
|
|
333
|
+
error_data = response.json()
|
|
334
|
+
error_msg = error_data.get("error", "Internal server error")
|
|
335
|
+
|
|
336
|
+
if "out of memory" in error_msg.lower() or "oom" in error_msg.lower():
|
|
337
|
+
raise ProviderError(
|
|
338
|
+
"Ollama server out of memory. Try using a smaller model or restart Ollama.",
|
|
339
|
+
provider="ollama",
|
|
340
|
+
)
|
|
341
|
+
elif "model" in error_msg.lower() and "loading" in error_msg.lower():
|
|
342
|
+
raise ProviderError(
|
|
343
|
+
f"Model '{self.model}' failed to load. Check if model is corrupted.",
|
|
344
|
+
provider="ollama",
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
raise ProviderError(f"Ollama API error: {error_msg}")
|
|
348
|
+
except (ValueError, KeyError):
|
|
349
|
+
raise ProviderError("Ollama API error: HTTP 500")
|
|
350
|
+
|
|
351
|
+
elif response.status_code == 503:
|
|
352
|
+
raise ProviderUnavailableError(
|
|
353
|
+
"Ollama server is overloaded or starting up. Please wait and try again.",
|
|
354
|
+
provider="ollama",
|
|
355
|
+
estimated_recovery="1-2 minutes",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
else:
|
|
359
|
+
try:
|
|
360
|
+
error_data = response.json()
|
|
361
|
+
error_msg = error_data.get("error", "Unknown error")
|
|
362
|
+
except:
|
|
363
|
+
error_msg = f"HTTP {response.status_code}"
|
|
364
|
+
|
|
365
|
+
if response.status_code >= 500:
|
|
366
|
+
raise ProviderUnavailableError(
|
|
367
|
+
f"Ollama server error: {error_msg}",
|
|
368
|
+
provider="ollama",
|
|
369
|
+
estimated_recovery="a few minutes",
|
|
370
|
+
)
|
|
371
|
+
else:
|
|
372
|
+
raise ProviderError(f"Ollama API error: {error_msg}", provider="ollama")
|
|
373
|
+
|
|
374
|
+
def get_model_info(self) -> ModelInfo:
|
|
375
|
+
"""
|
|
376
|
+
Get information about the current Ollama model.
|
|
377
|
+
|
|
378
|
+
Note: Ollama model info is limited compared to cloud providers.
|
|
379
|
+
"""
|
|
380
|
+
return ModelInfo(
|
|
381
|
+
name=self.model,
|
|
382
|
+
provider="ollama",
|
|
383
|
+
description=f"Ollama model {self.model} (local inference)",
|
|
384
|
+
max_tokens=self.max_tokens,
|
|
385
|
+
cost_per_token=0.0, # Local models are free
|
|
386
|
+
available=True, # Assume available if configured
|
|
387
|
+
supports_tools=False, # Ollama doesn't support tool calling yet
|
|
388
|
+
supports_multimodal=self._model_supports_multimodal(),
|
|
389
|
+
latest_version=False, # Can't determine without version info
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def get_available_models(self) -> List[ModelInfo]:
|
|
393
|
+
"""
|
|
394
|
+
Get list of available Ollama models.
|
|
395
|
+
|
|
396
|
+
This method returns models that are currently available on the
|
|
397
|
+
local Ollama server. It requires the server to be running.
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
# This is a synchronous method but we need async call
|
|
401
|
+
# We'll return a basic list and let the async version handle server queries
|
|
402
|
+
return [
|
|
403
|
+
ModelInfo(
|
|
404
|
+
name=self.model,
|
|
405
|
+
provider="ollama",
|
|
406
|
+
description=f"Ollama model {self.model} (local inference)",
|
|
407
|
+
max_tokens=self.max_tokens,
|
|
408
|
+
cost_per_token=0.0,
|
|
409
|
+
available=True,
|
|
410
|
+
supports_tools=False,
|
|
411
|
+
supports_multimodal=self._model_supports_multimodal(),
|
|
412
|
+
)
|
|
413
|
+
]
|
|
414
|
+
except Exception:
|
|
415
|
+
# Return empty list if we can't connect to server
|
|
416
|
+
return []
|
|
417
|
+
|
|
418
|
+
async def get_available_models_async(self) -> List[ModelInfo]:
|
|
419
|
+
"""
|
|
420
|
+
Async version to get available models from Ollama server.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
List of ModelInfo objects for models available on the server
|
|
424
|
+
"""
|
|
425
|
+
try:
|
|
426
|
+
server_models = await self._get_server_models()
|
|
427
|
+
model_infos = []
|
|
428
|
+
|
|
429
|
+
for model_data in server_models:
|
|
430
|
+
model_name = model_data.get("name", "")
|
|
431
|
+
if not model_name:
|
|
432
|
+
continue
|
|
433
|
+
|
|
434
|
+
# Extract size info if available
|
|
435
|
+
size_info = model_data.get("size", 0)
|
|
436
|
+
size_gb = round(size_info / (1024**3), 1) if size_info else 0
|
|
437
|
+
|
|
438
|
+
description = f"Ollama model {model_name} (local inference"
|
|
439
|
+
if size_gb > 0:
|
|
440
|
+
description += f", {size_gb}GB"
|
|
441
|
+
description += ")"
|
|
442
|
+
|
|
443
|
+
model_infos.append(
|
|
444
|
+
ModelInfo(
|
|
445
|
+
name=model_name,
|
|
446
|
+
provider="ollama",
|
|
447
|
+
description=description,
|
|
448
|
+
max_tokens=4096, # Default, varies by model
|
|
449
|
+
cost_per_token=0.0, # Local models are free
|
|
450
|
+
available=True,
|
|
451
|
+
supports_tools=False,
|
|
452
|
+
supports_multimodal=self._model_supports_multimodal(model_name),
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return model_infos
|
|
457
|
+
|
|
458
|
+
except Exception:
|
|
459
|
+
# Return current model if we can't fetch from server
|
|
460
|
+
return self.get_available_models()
|
|
461
|
+
|
|
462
|
+
def _model_supports_multimodal(self, model_name: str = None) -> bool:
|
|
463
|
+
"""
|
|
464
|
+
Check if a model supports multimodal inputs.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
model_name: Model name to check, defaults to current model
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
True if model supports multimodal inputs
|
|
471
|
+
"""
|
|
472
|
+
check_model = model_name or self.model
|
|
473
|
+
|
|
474
|
+
# Known multimodal Ollama models
|
|
475
|
+
multimodal_models = [
|
|
476
|
+
"llava",
|
|
477
|
+
"bakllava",
|
|
478
|
+
"moondream",
|
|
479
|
+
"llava-llama3",
|
|
480
|
+
"llava-phi3",
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
return any(mm_model in check_model.lower() for mm_model in multimodal_models)
|
|
484
|
+
|
|
485
|
+
def supports_tools(self) -> bool:
|
|
486
|
+
"""
|
|
487
|
+
Check if Ollama provider supports tool calling.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
False - Ollama doesn't currently support tool calling
|
|
491
|
+
"""
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
def supports_multimodal(self) -> bool:
|
|
495
|
+
"""
|
|
496
|
+
Check if current Ollama model supports multimodal inputs.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
True if current model supports multimodal inputs
|
|
500
|
+
"""
|
|
501
|
+
return self._model_supports_multimodal()
|
|
502
|
+
|
|
503
|
+
def supports_streaming(self) -> bool:
|
|
504
|
+
"""
|
|
505
|
+
Check if Ollama provider supports streaming responses.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
True - Ollama supports streaming
|
|
509
|
+
"""
|
|
510
|
+
return True
|