abstractcore 2.9.0__py3-none-any.whl → 2.11.2__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 (83) hide show
  1. abstractcore/__init__.py +7 -27
  2. abstractcore/apps/extractor.py +33 -100
  3. abstractcore/apps/intent.py +19 -0
  4. abstractcore/apps/judge.py +20 -1
  5. abstractcore/apps/summarizer.py +20 -1
  6. abstractcore/architectures/detection.py +34 -1
  7. abstractcore/architectures/response_postprocessing.py +313 -0
  8. abstractcore/assets/architecture_formats.json +38 -8
  9. abstractcore/assets/model_capabilities.json +781 -160
  10. abstractcore/compression/__init__.py +1 -2
  11. abstractcore/compression/glyph_processor.py +6 -4
  12. abstractcore/config/main.py +31 -19
  13. abstractcore/config/manager.py +389 -11
  14. abstractcore/config/vision_config.py +5 -5
  15. abstractcore/core/interface.py +151 -3
  16. abstractcore/core/session.py +16 -10
  17. abstractcore/download.py +1 -1
  18. abstractcore/embeddings/manager.py +20 -6
  19. abstractcore/endpoint/__init__.py +2 -0
  20. abstractcore/endpoint/app.py +458 -0
  21. abstractcore/mcp/client.py +3 -1
  22. abstractcore/media/__init__.py +52 -17
  23. abstractcore/media/auto_handler.py +42 -22
  24. abstractcore/media/base.py +44 -1
  25. abstractcore/media/capabilities.py +12 -33
  26. abstractcore/media/enrichment.py +105 -0
  27. abstractcore/media/handlers/anthropic_handler.py +19 -28
  28. abstractcore/media/handlers/local_handler.py +124 -70
  29. abstractcore/media/handlers/openai_handler.py +19 -31
  30. abstractcore/media/processors/__init__.py +4 -2
  31. abstractcore/media/processors/audio_processor.py +57 -0
  32. abstractcore/media/processors/office_processor.py +8 -3
  33. abstractcore/media/processors/pdf_processor.py +46 -3
  34. abstractcore/media/processors/text_processor.py +22 -24
  35. abstractcore/media/processors/video_processor.py +58 -0
  36. abstractcore/media/types.py +97 -4
  37. abstractcore/media/utils/image_scaler.py +20 -2
  38. abstractcore/media/utils/video_frames.py +219 -0
  39. abstractcore/media/vision_fallback.py +136 -22
  40. abstractcore/processing/__init__.py +32 -3
  41. abstractcore/processing/basic_deepsearch.py +15 -10
  42. abstractcore/processing/basic_intent.py +3 -2
  43. abstractcore/processing/basic_judge.py +3 -2
  44. abstractcore/processing/basic_summarizer.py +1 -1
  45. abstractcore/providers/__init__.py +3 -1
  46. abstractcore/providers/anthropic_provider.py +95 -8
  47. abstractcore/providers/base.py +1516 -81
  48. abstractcore/providers/huggingface_provider.py +546 -69
  49. abstractcore/providers/lmstudio_provider.py +35 -923
  50. abstractcore/providers/mlx_provider.py +382 -35
  51. abstractcore/providers/model_capabilities.py +5 -1
  52. abstractcore/providers/ollama_provider.py +99 -15
  53. abstractcore/providers/openai_compatible_provider.py +406 -180
  54. abstractcore/providers/openai_provider.py +188 -44
  55. abstractcore/providers/openrouter_provider.py +76 -0
  56. abstractcore/providers/registry.py +61 -5
  57. abstractcore/providers/streaming.py +138 -33
  58. abstractcore/providers/vllm_provider.py +92 -817
  59. abstractcore/server/app.py +461 -13
  60. abstractcore/server/audio_endpoints.py +139 -0
  61. abstractcore/server/vision_endpoints.py +1319 -0
  62. abstractcore/structured/handler.py +316 -41
  63. abstractcore/tools/common_tools.py +5501 -2012
  64. abstractcore/tools/comms_tools.py +1641 -0
  65. abstractcore/tools/core.py +37 -7
  66. abstractcore/tools/handler.py +4 -9
  67. abstractcore/tools/parser.py +49 -2
  68. abstractcore/tools/tag_rewriter.py +2 -1
  69. abstractcore/tools/telegram_tdlib.py +407 -0
  70. abstractcore/tools/telegram_tools.py +261 -0
  71. abstractcore/utils/cli.py +1085 -72
  72. abstractcore/utils/token_utils.py +2 -0
  73. abstractcore/utils/truncation.py +29 -0
  74. abstractcore/utils/version.py +3 -4
  75. abstractcore/utils/vlm_token_calculator.py +12 -2
  76. abstractcore-2.11.2.dist-info/METADATA +562 -0
  77. abstractcore-2.11.2.dist-info/RECORD +133 -0
  78. {abstractcore-2.9.0.dist-info → abstractcore-2.11.2.dist-info}/WHEEL +1 -1
  79. {abstractcore-2.9.0.dist-info → abstractcore-2.11.2.dist-info}/entry_points.txt +1 -0
  80. abstractcore-2.9.0.dist-info/METADATA +0 -1189
  81. abstractcore-2.9.0.dist-info/RECORD +0 -119
  82. {abstractcore-2.9.0.dist-info → abstractcore-2.11.2.dist-info}/licenses/LICENSE +0 -0
  83. {abstractcore-2.9.0.dist-info → abstractcore-2.11.2.dist-info}/top_level.txt +0 -0
@@ -25,6 +25,48 @@ class VisionConfig:
25
25
  self.fallback_chain = []
26
26
 
27
27
 
28
+ @dataclass
29
+ class AudioConfig:
30
+ """Audio configuration settings (input policy + optional fallback)."""
31
+ # Default: do not silently change semantics. Allow native audio when supported,
32
+ # otherwise error unless the caller explicitly requests STT/caption.
33
+ strategy: str = "native_only" # native_only|speech_to_text|caption|auto
34
+ # Optional preferred STT backend (capabilities plugin backend_id).
35
+ stt_backend_id: Optional[str] = None
36
+ stt_language: Optional[str] = None
37
+ # Reserved for future "audio caption" backends.
38
+ caption_provider: Optional[str] = None
39
+ caption_model: Optional[str] = None
40
+ fallback_chain: list = None
41
+
42
+ def __post_init__(self):
43
+ if self.fallback_chain is None:
44
+ self.fallback_chain = []
45
+
46
+
47
+ @dataclass
48
+ class VideoConfig:
49
+ """Video configuration settings (input policy + optional fallback)."""
50
+ # Default: best-effort usability. Prefer native video when supported; otherwise fall back
51
+ # to sampled frames routed through existing image/vision handling.
52
+ strategy: str = "auto" # native_only|frames_caption|auto
53
+
54
+ # Frame sampling controls for frames-based fallback.
55
+ max_frames: int = 3
56
+ # Native video models typically require more temporal coverage than the fallback path.
57
+ # This default is used when the selected model supports native video input (v0: HF only).
58
+ max_frames_native: int = 8
59
+ frame_format: str = "jpg" # jpg|png
60
+ sampling_strategy: str = "uniform" # uniform|keyframes
61
+
62
+ # Downscale extracted frames (preserve aspect ratio; never upscale). Helps memory + token pressure.
63
+ # Applies to both frames_caption fallback and HF native video ingestion (which uses ffmpeg frames).
64
+ max_frame_side: int = 1024
65
+
66
+ # Maximum video size allowed for processing (bytes). None => use media handler defaults.
67
+ max_video_size_bytes: Optional[int] = None
68
+
69
+
28
70
  @dataclass
29
71
  class EmbeddingsConfig:
30
72
  """Embeddings configuration settings."""
@@ -47,6 +89,44 @@ class AppDefaults:
47
89
  intent_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
48
90
 
49
91
 
92
+ @dataclass
93
+ class MaintenanceConfig:
94
+ """Maintenance agent configuration (triage, stewardship)."""
95
+
96
+ # LLM assist (optional, local-first).
97
+ triage_llm_enabled: bool = False
98
+ triage_llm_base_url: str = "http://localhost:1234"
99
+ triage_llm_model: str = "qwen/qwen3-next-80b"
100
+ triage_llm_temperature: float = 0.2
101
+ triage_llm_max_tokens: int = 800
102
+ triage_llm_timeout_s: float = 30.0
103
+
104
+
105
+ @dataclass
106
+ class EmailConfig:
107
+ """Email defaults (SMTP outbound + IMAP inbound).
108
+
109
+ These defaults are used by framework-native comms tools and gateway bridges when
110
+ explicit parameters are omitted (env vars still take precedence).
111
+ """
112
+
113
+ # SMTP (outbound)
114
+ smtp_host: str = ""
115
+ smtp_port: int = 587
116
+ smtp_username: str = ""
117
+ smtp_password_env_var: str = "EMAIL_PASSWORD"
118
+ smtp_use_starttls: bool = True
119
+ from_email: Optional[str] = None
120
+ reply_to: Optional[str] = None
121
+
122
+ # IMAP (inbound)
123
+ imap_host: str = ""
124
+ imap_port: int = 993
125
+ imap_username: str = ""
126
+ imap_password_env_var: str = "EMAIL_PASSWORD"
127
+ imap_folder: str = "INBOX"
128
+
129
+
50
130
  @dataclass
51
131
  class DefaultModels:
52
132
  """Global default model configurations."""
@@ -61,6 +141,7 @@ class ApiKeysConfig:
61
141
  """API keys configuration."""
62
142
  openai: Optional[str] = None
63
143
  anthropic: Optional[str] = None
144
+ openrouter: Optional[str] = None
64
145
  google: Optional[str] = None
65
146
 
66
147
 
@@ -85,6 +166,12 @@ class LoggingConfig:
85
166
  file_json: bool = True
86
167
 
87
168
 
169
+ @dataclass
170
+ class StreamingConfig:
171
+ """Streaming configuration settings."""
172
+ cli_stream_default: bool = False
173
+
174
+
88
175
  @dataclass
89
176
  class TimeoutConfig:
90
177
  """Timeout configuration settings."""
@@ -106,28 +193,38 @@ class OfflineConfig:
106
193
  class AbstractCoreConfig:
107
194
  """Main configuration class."""
108
195
  vision: VisionConfig
196
+ audio: AudioConfig
197
+ video: VideoConfig
109
198
  embeddings: EmbeddingsConfig
110
199
  app_defaults: AppDefaults
111
200
  default_models: DefaultModels
112
201
  api_keys: ApiKeysConfig
113
202
  cache: CacheConfig
114
203
  logging: LoggingConfig
204
+ streaming: StreamingConfig
115
205
  timeouts: TimeoutConfig
116
206
  offline: OfflineConfig
207
+ maintenance: MaintenanceConfig
208
+ email: EmailConfig
117
209
 
118
210
  @classmethod
119
211
  def default(cls):
120
212
  """Create default configuration."""
121
213
  return cls(
122
214
  vision=VisionConfig(),
215
+ audio=AudioConfig(),
216
+ video=VideoConfig(),
123
217
  embeddings=EmbeddingsConfig(),
124
218
  app_defaults=AppDefaults(),
125
219
  default_models=DefaultModels(),
126
220
  api_keys=ApiKeysConfig(),
127
221
  cache=CacheConfig(),
128
222
  logging=LoggingConfig(),
223
+ streaming=StreamingConfig(),
129
224
  timeouts=TimeoutConfig(),
130
- offline=OfflineConfig()
225
+ offline=OfflineConfig(),
226
+ maintenance=MaintenanceConfig(),
227
+ email=EmailConfig(),
131
228
  )
132
229
 
133
230
 
@@ -157,25 +254,35 @@ class ConfigurationManager:
157
254
  """Convert dictionary to config object."""
158
255
  # Create config objects from dictionary data
159
256
  vision = VisionConfig(**data.get('vision', {}))
257
+ audio = AudioConfig(**data.get('audio', {}))
258
+ video = VideoConfig(**data.get('video', {}))
160
259
  embeddings = EmbeddingsConfig(**data.get('embeddings', {}))
161
260
  app_defaults = AppDefaults(**data.get('app_defaults', {}))
162
261
  default_models = DefaultModels(**data.get('default_models', {}))
163
262
  api_keys = ApiKeysConfig(**data.get('api_keys', {}))
164
263
  cache = CacheConfig(**data.get('cache', {}))
165
264
  logging = LoggingConfig(**data.get('logging', {}))
265
+ streaming = StreamingConfig(**data.get('streaming', {}))
166
266
  timeouts = TimeoutConfig(**data.get('timeouts', {}))
167
267
  offline = OfflineConfig(**data.get('offline', {}))
268
+ maintenance = MaintenanceConfig(**data.get('maintenance', {}))
269
+ email_cfg = EmailConfig(**data.get('email', {}))
168
270
 
169
271
  return AbstractCoreConfig(
170
272
  vision=vision,
273
+ audio=audio,
274
+ video=video,
171
275
  embeddings=embeddings,
172
276
  app_defaults=app_defaults,
173
277
  default_models=default_models,
174
278
  api_keys=api_keys,
175
279
  cache=cache,
176
280
  logging=logging,
281
+ streaming=streaming,
177
282
  timeouts=timeouts,
178
- offline=offline
283
+ offline=offline,
284
+ maintenance=maintenance,
285
+ email=email_cfg,
179
286
  )
180
287
 
181
288
  def _save_config(self):
@@ -185,14 +292,18 @@ class ConfigurationManager:
185
292
  # Convert config to dictionary
186
293
  config_dict = {
187
294
  'vision': asdict(self.config.vision),
295
+ 'audio': asdict(self.config.audio),
296
+ 'video': asdict(self.config.video),
188
297
  'embeddings': asdict(self.config.embeddings),
189
298
  'app_defaults': asdict(self.config.app_defaults),
190
299
  'default_models': asdict(self.config.default_models),
191
300
  'api_keys': asdict(self.config.api_keys),
192
301
  'cache': asdict(self.config.cache),
193
302
  'logging': asdict(self.config.logging),
303
+ 'streaming': asdict(self.config.streaming),
194
304
  'timeouts': asdict(self.config.timeouts),
195
- 'offline': asdict(self.config.offline)
305
+ 'offline': asdict(self.config.offline),
306
+ 'maintenance': asdict(self.config.maintenance),
196
307
  }
197
308
 
198
309
  with open(self.config_file, 'w') as f:
@@ -242,6 +353,14 @@ class ConfigurationManager:
242
353
  "caption_provider": self.config.vision.caption_provider,
243
354
  "caption_model": self.config.vision.caption_model
244
355
  },
356
+ "video": {
357
+ "strategy": self.config.video.strategy,
358
+ "max_frames": self.config.video.max_frames,
359
+ "max_frames_native": getattr(self.config.video, "max_frames_native", None),
360
+ "frame_format": self.config.video.frame_format,
361
+ "sampling_strategy": getattr(self.config.video, "sampling_strategy", None),
362
+ "max_frame_side": getattr(self.config.video, "max_frame_side", None),
363
+ },
245
364
  "app_defaults": {
246
365
  "cli": {
247
366
  "provider": self.config.app_defaults.cli_provider,
@@ -276,7 +395,7 @@ class ConfigurationManager:
276
395
  "model": self.config.embeddings.model
277
396
  },
278
397
  "streaming": {
279
- "cli_stream_default": False # Default value
398
+ "cli_stream_default": self.config.streaming.cli_stream_default
280
399
  },
281
400
  "logging": {
282
401
  "console_level": self.config.logging.console_level,
@@ -288,11 +407,15 @@ class ConfigurationManager:
288
407
  "tool_timeout": self.config.timeouts.tool_timeout
289
408
  },
290
409
  "cache": {
291
- "default_cache_dir": self.config.cache.default_cache_dir
410
+ "default_cache_dir": self.config.cache.default_cache_dir,
411
+ "huggingface_cache_dir": self.config.cache.huggingface_cache_dir,
412
+ "local_models_cache_dir": self.config.cache.local_models_cache_dir,
413
+ "glyph_cache_dir": self.config.cache.glyph_cache_dir,
292
414
  },
293
415
  "api_keys": {
294
416
  "openai": "✅ Set" if self.config.api_keys.openai else "❌ Not set",
295
417
  "anthropic": "✅ Set" if self.config.api_keys.anthropic else "❌ Not set",
418
+ "openrouter": "✅ Set" if self.config.api_keys.openrouter else "❌ Not set",
296
419
  "google": "✅ Set" if self.config.api_keys.google else "❌ Not set"
297
420
  },
298
421
  "offline": {
@@ -302,6 +425,16 @@ class ConfigurationManager:
302
425
  }
303
426
  }
304
427
 
428
+ def reset_configuration(self) -> bool:
429
+ """Reset all configuration to built-in defaults."""
430
+ try:
431
+ self.config = AbstractCoreConfig.default()
432
+ self._provider_config.clear()
433
+ self._save_config()
434
+ return True
435
+ except Exception:
436
+ return False
437
+
305
438
  def set_global_default_model(self, provider_model: str) -> bool:
306
439
  """Set global default model in provider/model format."""
307
440
  try:
@@ -319,6 +452,243 @@ class ConfigurationManager:
319
452
  except Exception:
320
453
  return False
321
454
 
455
+ def set_default_model(self, provider_model: str) -> bool:
456
+ """Legacy alias for setting the global default model."""
457
+ return self.set_global_default_model(provider_model)
458
+
459
+ def set_global_default_provider(self, provider: str) -> bool:
460
+ """Set global default provider (legacy)."""
461
+ try:
462
+ provider = str(provider or "").strip()
463
+ if not provider:
464
+ raise ValueError("Provider cannot be empty")
465
+ self.config.default_models.global_provider = provider
466
+ self._save_config()
467
+ return True
468
+ except Exception:
469
+ return False
470
+
471
+ def set_chat_model(self, provider_model: str) -> bool:
472
+ """Set specialized chat model (provider/model string)."""
473
+ try:
474
+ model = str(provider_model or "").strip()
475
+ if not model:
476
+ raise ValueError("Model cannot be empty")
477
+ self.config.default_models.chat_model = model
478
+ self._save_config()
479
+ return True
480
+ except Exception:
481
+ return False
482
+
483
+ def set_code_model(self, provider_model: str) -> bool:
484
+ """Set specialized code model (provider/model string)."""
485
+ try:
486
+ model = str(provider_model or "").strip()
487
+ if not model:
488
+ raise ValueError("Model cannot be empty")
489
+ self.config.default_models.code_model = model
490
+ self._save_config()
491
+ return True
492
+ except Exception:
493
+ return False
494
+
495
+ def set_embeddings_model(self, provider_model: str) -> bool:
496
+ """Set embeddings provider/model from a provider/model string (preferred)."""
497
+ try:
498
+ value = str(provider_model or "").strip()
499
+ if not value:
500
+ raise ValueError("Embeddings model cannot be empty")
501
+
502
+ if "/" in value:
503
+ provider, model = value.split("/", 1)
504
+ self.config.embeddings.provider = provider.strip() or self.config.embeddings.provider
505
+ self.config.embeddings.model = model.strip()
506
+ else:
507
+ self.config.embeddings.model = value
508
+
509
+ self._save_config()
510
+ return True
511
+ except Exception:
512
+ return False
513
+
514
+ def set_embeddings_provider(self, provider: str) -> bool:
515
+ """Set embeddings provider."""
516
+ try:
517
+ value = str(provider or "").strip()
518
+ if not value:
519
+ raise ValueError("Embeddings provider cannot be empty")
520
+ self.config.embeddings.provider = value
521
+ self._save_config()
522
+ return True
523
+ except Exception:
524
+ return False
525
+
526
+ def set_default_cache_dir(self, path: str) -> bool:
527
+ """Set default cache directory for AbstractCore."""
528
+ try:
529
+ value = str(path or "").strip()
530
+ if not value:
531
+ raise ValueError("Cache directory cannot be empty")
532
+ self.config.cache.default_cache_dir = value
533
+ self._save_config()
534
+ return True
535
+ except Exception:
536
+ return False
537
+
538
+ def set_huggingface_cache_dir(self, path: str) -> bool:
539
+ """Set HuggingFace cache directory."""
540
+ try:
541
+ value = str(path or "").strip()
542
+ if not value:
543
+ raise ValueError("HuggingFace cache directory cannot be empty")
544
+ self.config.cache.huggingface_cache_dir = value
545
+ self._save_config()
546
+ return True
547
+ except Exception:
548
+ return False
549
+
550
+ def set_local_models_cache_dir(self, path: str) -> bool:
551
+ """Set local models cache directory."""
552
+ try:
553
+ value = str(path or "").strip()
554
+ if not value:
555
+ raise ValueError("Local models cache directory cannot be empty")
556
+ self.config.cache.local_models_cache_dir = value
557
+ self._save_config()
558
+ return True
559
+ except Exception:
560
+ return False
561
+
562
+ def set_log_base_dir(self, path: str) -> bool:
563
+ """Set log base directory."""
564
+ try:
565
+ value = str(path or "").strip()
566
+ if not value:
567
+ raise ValueError("Log base directory cannot be empty")
568
+ self.config.logging.log_base_dir = value
569
+ self._save_config()
570
+ return True
571
+ except Exception:
572
+ return False
573
+
574
+ def set_console_log_level(self, level: str) -> bool:
575
+ """Set console logging level."""
576
+ try:
577
+ value = str(level or "").strip().upper()
578
+ if not value:
579
+ raise ValueError("Console log level cannot be empty")
580
+ self.config.logging.console_level = value
581
+ self._save_config()
582
+ return True
583
+ except Exception:
584
+ return False
585
+
586
+ def set_file_log_level(self, level: str) -> bool:
587
+ """Set file logging level."""
588
+ try:
589
+ value = str(level or "").strip().upper()
590
+ if not value:
591
+ raise ValueError("File log level cannot be empty")
592
+ self.config.logging.file_level = value
593
+ self._save_config()
594
+ return True
595
+ except Exception:
596
+ return False
597
+
598
+ def enable_debug_logging(self) -> bool:
599
+ """Enable debug logging for both console and file."""
600
+ try:
601
+ self.config.logging.console_level = "DEBUG"
602
+ self.config.logging.file_level = "DEBUG"
603
+ self._save_config()
604
+ return True
605
+ except Exception:
606
+ return False
607
+
608
+ def disable_console_logging(self) -> bool:
609
+ """Disable console logging output."""
610
+ try:
611
+ self.config.logging.console_level = "NONE"
612
+ self._save_config()
613
+ return True
614
+ except Exception:
615
+ return False
616
+
617
+ def enable_file_logging(self) -> bool:
618
+ """Enable file logging."""
619
+ try:
620
+ self.config.logging.file_logging_enabled = True
621
+ self._save_config()
622
+ return True
623
+ except Exception:
624
+ return False
625
+
626
+ def disable_file_logging(self) -> bool:
627
+ """Disable file logging."""
628
+ try:
629
+ self.config.logging.file_logging_enabled = False
630
+ self._save_config()
631
+ return True
632
+ except Exception:
633
+ return False
634
+
635
+ def set_streaming_default(self, app_name: str, enabled: bool) -> bool:
636
+ """Set default streaming behavior for a given app (currently: cli)."""
637
+ try:
638
+ app = str(app_name or "").strip().lower()
639
+ if app != "cli":
640
+ return False
641
+ self.config.streaming.cli_stream_default = bool(enabled)
642
+ self._save_config()
643
+ return True
644
+ except Exception:
645
+ return False
646
+
647
+ def get_streaming_default(self, app_name: str) -> bool:
648
+ """Get default streaming behavior for a given app (currently: cli)."""
649
+ app = str(app_name or "").strip().lower()
650
+ if app == "cli":
651
+ return bool(self.config.streaming.cli_stream_default)
652
+ return False
653
+
654
+ def enable_cli_streaming(self) -> bool:
655
+ """Enable streaming by default for the CLI."""
656
+ return self.set_streaming_default("cli", True)
657
+
658
+ def disable_cli_streaming(self) -> bool:
659
+ """Disable streaming by default for the CLI."""
660
+ return self.set_streaming_default("cli", False)
661
+
662
+ def add_vision_fallback(self, provider: str, model: str) -> bool:
663
+ """Add a vision fallback provider/model to the chain."""
664
+ try:
665
+ provider_val = str(provider or "").strip()
666
+ model_val = str(model or "").strip()
667
+ if not provider_val or not model_val:
668
+ raise ValueError("Provider and model are required")
669
+
670
+ self.config.vision.fallback_chain.append({"provider": provider_val, "model": model_val})
671
+ # If vision is configured at all, assume two_stage (caption -> text model).
672
+ if not self.config.vision.strategy or self.config.vision.strategy == "disabled":
673
+ self.config.vision.strategy = "two_stage"
674
+ self._save_config()
675
+ return True
676
+ except Exception:
677
+ return False
678
+
679
+ def disable_vision(self) -> bool:
680
+ """Disable vision fallback for text-only models."""
681
+ try:
682
+ self.config.vision.strategy = "disabled"
683
+ self.config.vision.caption_provider = None
684
+ self.config.vision.caption_model = None
685
+ self.config.vision.fallback_chain = []
686
+ self._save_config()
687
+ return True
688
+ except Exception:
689
+ return False
690
+
691
+
322
692
  def set_app_default(self, app_name: str, provider: str, model: str) -> bool:
323
693
  """Set app-specific default provider and model."""
324
694
  try:
@@ -352,6 +722,8 @@ class ConfigurationManager:
352
722
  self.config.api_keys.openai = key
353
723
  elif provider == "anthropic":
354
724
  self.config.api_keys.anthropic = key
725
+ elif provider == "openrouter":
726
+ self.config.api_keys.openrouter = key
355
727
  elif provider == "google":
356
728
  self.config.api_keys.google = key
357
729
  else:
@@ -383,9 +755,12 @@ class ConfigurationManager:
383
755
  def set_default_timeout(self, timeout: float) -> bool:
384
756
  """Set default HTTP request timeout in seconds."""
385
757
  try:
386
- if timeout <= 0:
387
- raise ValueError("Timeout must be positive")
388
- self.config.timeouts.default_timeout = timeout
758
+ # #[WARNING:TIMEOUT]
759
+ # Contract: allow `0` to mean "unlimited" (the provider layer normalizes <=0 to None).
760
+ timeout_f = float(timeout)
761
+ if timeout_f < 0:
762
+ raise ValueError("Timeout must be >= 0 (0 = unlimited)")
763
+ self.config.timeouts.default_timeout = timeout_f
389
764
  self._save_config()
390
765
  return True
391
766
  except Exception:
@@ -394,9 +769,12 @@ class ConfigurationManager:
394
769
  def set_tool_timeout(self, timeout: float) -> bool:
395
770
  """Set tool execution timeout in seconds."""
396
771
  try:
397
- if timeout <= 0:
398
- raise ValueError("Timeout must be positive")
399
- self.config.timeouts.tool_timeout = timeout
772
+ # #[WARNING:TIMEOUT]
773
+ # Contract: allow `0` to mean "unlimited".
774
+ timeout_f = float(timeout)
775
+ if timeout_f < 0:
776
+ raise ValueError("Timeout must be >= 0 (0 = unlimited)")
777
+ self.config.timeouts.tool_timeout = timeout_f
400
778
  self._save_config()
401
779
  return True
402
780
  except Exception:
@@ -155,9 +155,9 @@ def handle_list_vision(handler: 'VisionFallbackHandler') -> bool:
155
155
  "gpt-4-turbo-with-vision - GPT-4 Turbo Vision"
156
156
  ],
157
157
  "anthropic": [
158
- "claude-3.5-sonnet - Claude 3.5 Sonnet",
159
- "claude-3.5-haiku - Claude 3.5 Haiku",
160
- "claude-3-opus - Claude 3 Opus"
158
+ "claude-haiku-4-5 - Claude Haiku 4.5 (vision, cost-effective)",
159
+ "claude-sonnet-4-5 - Claude Sonnet 4.5 (vision)",
160
+ "claude-opus-4-5 - Claude Opus 4.5 (vision)"
161
161
  ],
162
162
  "huggingface": [
163
163
  "unsloth/Qwen2.5-VL-7B-Instruct-GGUF - GGUF format",
@@ -400,7 +400,7 @@ def configure_cloud_provider(handler: 'VisionFallbackHandler') -> bool:
400
400
  # Suggest models based on provider
401
401
  model_suggestions = {
402
402
  "openai": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo-with-vision"],
403
- "anthropic": ["claude-3.5-sonnet", "claude-3.5-haiku", "claude-3-opus"]
403
+ "anthropic": ["claude-haiku-4-5", "claude-sonnet-4-5", "claude-opus-4-5"]
404
404
  }
405
405
 
406
406
  print(f"\nSuggested models for {provider}:")
@@ -488,4 +488,4 @@ def add_vision_arguments(parser: argparse.ArgumentParser):
488
488
  vision_group.add_argument('--download-vision-model', nargs='?', const=True, metavar='MODEL',
489
489
  help='Download vision model for offline use (default: blip-base-caption)')
490
490
  vision_group.add_argument('--configure', choices=['vision'],
491
- help='Interactive configuration mode')
491
+ help='Interactive configuration mode')