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,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