cct-cli 0.7.9.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 (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,207 @@
1
+ """
2
+ calc_terminal/providers/discovery_manager.py
3
+ ===========================================
4
+ Provider Discovery Manager for Dynamic Model and Provider Discovery.
5
+
6
+ Coordinates adapter-driven live discovery across all supported providers,
7
+ reconciles results against DynamicModelRegistry, and notifies users when
8
+ new models are detected.
9
+
10
+ Features:
11
+ - Non-blocking asynchronous discovery loops.
12
+ - Fail-safe timeout bounding and graceful error recovery.
13
+ - Offline mode detection and stale cache preservation.
14
+ - Live health status tracking (ONLINE, DEGRADED, OFFLINE, UNKNOWN).
15
+ """
16
+
17
+ import concurrent.futures
18
+ import json
19
+ import logging
20
+ import os
21
+ import threading
22
+ import time
23
+ from typing import Dict, List, Optional, Any, Callable
24
+
25
+ from .adapters import get_adapter, list_supported_adapters
26
+ from ..models.dynamic_registry import get_registry, DynamicModelRegistry
27
+ from ..models.schema import ModelInfo
28
+
29
+ _LOG = logging.getLogger("cct.discovery_manager")
30
+
31
+
32
+ class ProviderDiscoveryManager:
33
+ """Manages continuous background model discovery and reconciliation."""
34
+
35
+ _instance: Optional["ProviderDiscoveryManager"] = None
36
+ _lock = threading.Lock()
37
+
38
+ def __new__(cls, *args, **kwargs):
39
+ with cls._lock:
40
+ if cls._instance is None:
41
+ cls._instance = super(ProviderDiscoveryManager, cls).__new__(cls)
42
+ cls._instance._initialized = False
43
+ return cls._instance
44
+
45
+ def __init__(self, registry: Optional[DynamicModelRegistry] = None):
46
+ if getattr(self, "_initialized", False):
47
+ return
48
+ self.registry = registry or get_registry()
49
+ self._provider_status: Dict[str, Dict[str, Any]] = {}
50
+ self._notification_hooks: List[Callable[[List[ModelInfo]], None]] = []
51
+ self._bg_thread: Optional[threading.Thread] = None
52
+ self._stop_event = threading.Event()
53
+ self._initialized = True
54
+
55
+ # ── Notification Hooks ─────────────────────────────────────────────────────
56
+
57
+ def add_notification_hook(self, hook: Callable[[List[ModelInfo]], None]):
58
+ """Register a callback for when new models are detected."""
59
+ if hook not in self._notification_hooks:
60
+ self._notification_hooks.append(hook)
61
+
62
+ register_notification_hook = add_notification_hook
63
+
64
+ def _notify_new_models(self, new_models: List[ModelInfo]):
65
+ for hook in self._notification_hooks:
66
+ try:
67
+ hook(new_models)
68
+ except Exception as e:
69
+ _LOG.warning(f"Error executing discovery notification hook: {e}")
70
+
71
+ # ── Single Provider Discovery ──────────────────────────────────────────────
72
+
73
+ def discover_provider(
74
+ self,
75
+ provider_id: str,
76
+ config: Optional[Dict[str, Any]] = None,
77
+ timeout: float = 6.0,
78
+ ) -> List[ModelInfo]:
79
+ """Run discovery for a single provider and reconcile with registry."""
80
+ pid = provider_id.lower().strip()
81
+ adapter = get_adapter(pid, config)
82
+
83
+ try:
84
+ discovered = adapter.discover_models(timeout=timeout)
85
+ if discovered:
86
+ diff = self.registry.reconcile_models(pid, discovered)
87
+ self._provider_status[pid] = {
88
+ "status": "online",
89
+ "models_count": len(discovered),
90
+ "last_checked": time.time(),
91
+ "diff": diff,
92
+ }
93
+ # Check for newly discovered models
94
+ new_models = self.registry.pop_new_models()
95
+ if new_models:
96
+ self._notify_new_models(new_models)
97
+ return discovered
98
+ else:
99
+ self._provider_status[pid] = {
100
+ "status": "degraded",
101
+ "models_count": 0,
102
+ "last_checked": time.time(),
103
+ "message": "Zero models returned",
104
+ }
105
+ except Exception as e:
106
+ _LOG.warning(f"Discovery failed for {pid}: {e}")
107
+ self._provider_status[pid] = {
108
+ "status": "offline",
109
+ "last_checked": time.time(),
110
+ "error": str(e),
111
+ }
112
+
113
+ # Fallback to previously cached registry models
114
+ return self.registry.list_models(provider=pid)
115
+
116
+ # ── Parallel Discovery Across All Providers ────────────────────────────────
117
+
118
+ def discover_all(
119
+ self,
120
+ provider_configs: Optional[Dict[str, Dict[str, Any]]] = None,
121
+ timeout_per_provider: float = 6.0,
122
+ max_workers: int = 8,
123
+ ) -> Dict[str, List[ModelInfo]]:
124
+ """Discover models across all supported providers in parallel."""
125
+ supported = list_supported_adapters()
126
+ configs = provider_configs or {}
127
+ results: Dict[str, List[ModelInfo]] = {}
128
+
129
+ # Prioritize key providers first
130
+ priority_pids = [
131
+ "openai", "anthropic", "gemini", "deepseek", "alibaba",
132
+ "moonshot", "zhipu", "ollama", "xai", "mistral", "minimax"
133
+ ]
134
+ all_pids = list(priority_pids) + [p for p in supported.keys() if p not in priority_pids]
135
+
136
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
137
+ future_to_pid = {
138
+ executor.submit(
139
+ self.discover_provider, pid, configs.get(pid), timeout_per_provider
140
+ ): pid
141
+ for pid in all_pids
142
+ }
143
+ for future in concurrent.futures.as_completed(future_to_pid):
144
+ pid = future_to_pid[future]
145
+ try:
146
+ results[pid] = future.result()
147
+ except Exception as e:
148
+ results[pid] = []
149
+ _LOG.warning(f"Exception during discovery for {pid}: {e}")
150
+
151
+ return results
152
+
153
+ # ── Background Worker ──────────────────────────────────────────────────────
154
+
155
+ def start_background_discovery(self, interval_hours: float = 24.0, delay_startup: float = 1.0):
156
+ """Launch background discovery thread."""
157
+ if self._bg_thread and self._bg_thread.is_alive():
158
+ return
159
+
160
+ self._stop_event.clear()
161
+
162
+ def _loop():
163
+ # Initial brief delay so CAT startup finishes instantly
164
+ self._stop_event.wait(delay_startup)
165
+ if self._stop_event.is_set():
166
+ return
167
+ try:
168
+ self.discover_all()
169
+ except Exception as e:
170
+ _LOG.warning(f"Background initial discovery error: {e}")
171
+
172
+ # Periodic loop
173
+ while not self._stop_event.is_set():
174
+ self._stop_event.wait(interval_hours * 3600.0)
175
+ if self._stop_event.is_set():
176
+ break
177
+ try:
178
+ self.discover_all()
179
+ except Exception as e:
180
+ _LOG.warning(f"Background periodic discovery error: {e}")
181
+
182
+ self._bg_thread = threading.Thread(
183
+ target=_loop,
184
+ daemon=True,
185
+ name="cct-discovery-manager",
186
+ )
187
+ self._bg_thread.start()
188
+
189
+ def stop_background_discovery(self):
190
+ """Stop background discovery thread."""
191
+ self._stop_event.set()
192
+
193
+ # ── Status Inspection ──────────────────────────────────────────────────────
194
+
195
+ def get_provider_status(self, provider_id: str) -> Dict[str, Any]:
196
+ pid = provider_id.lower().strip()
197
+ return self._provider_status.get(pid, {"status": "unknown", "provider": pid})
198
+
199
+ def get_all_provider_statuses(self) -> Dict[str, Dict[str, Any]]:
200
+ return dict(self._provider_status)
201
+
202
+ get_all_provider_status = get_all_provider_statuses
203
+
204
+
205
+ # Singleton convenience accessor
206
+ def get_discovery_manager() -> ProviderDiscoveryManager:
207
+ return ProviderDiscoveryManager()
@@ -0,0 +1,178 @@
1
+ """Google Gemini provider."""
2
+
3
+ import json
4
+ import time
5
+ from typing import Optional
6
+
7
+ import requests
8
+
9
+ from .base_provider import BaseProvider
10
+
11
+
12
+ class GeminiProvider(BaseProvider):
13
+ ID = "gemini"
14
+ NAME = "Google Gemini"
15
+ NEEDS_KEY = True
16
+ DEFAULT_MODEL = "gemini-1.5-flash-latest" # fallback only — resolved from providers.json when available
17
+ API_STYLE = "gemini"
18
+ supports_tools = True
19
+ supports_reasoning = True
20
+ supports_vision = True
21
+ supports_streaming = True
22
+ supports_audio = True
23
+ supports_embeddings = True
24
+
25
+ @classmethod
26
+ def _default_url(cls):
27
+ return "https://generativelanguage.googleapis.com/v1beta"
28
+
29
+ @classmethod
30
+ def default_model(cls):
31
+ """Data-driven default from providers.json; DEFAULT_MODEL as fallback."""
32
+ from ..models import manager as _mgr
33
+ return _mgr.default_model_for(cls.ID, cls.DEFAULT_MODEL)
34
+
35
+ def _key_suffix(self):
36
+ return f"?key={self.get_api_key()}"
37
+
38
+ def _build_contents(self, history, prompt):
39
+ contents = []
40
+ for role, text in (history or []):
41
+ r = "model" if role == "assistant" else "user"
42
+ contents.append({"role": r, "parts": [{"text": text}]})
43
+ contents.append({"role": "user", "parts": [{"text": prompt}]})
44
+ return contents
45
+
46
+ def fetch_models(self) -> list[str]:
47
+ base = self.get_base_url()
48
+ key = self.get_api_key()
49
+ if not base or not key:
50
+ return []
51
+ try:
52
+ resp = requests.get(f"{base}/models{self._key_suffix()}", timeout=10)
53
+ if resp.status_code >= 400:
54
+ return []
55
+ out = []
56
+ for m in resp.json().get("models", []):
57
+ methods = m.get("supportedGenerationMethods", [])
58
+ if not methods or "generateContent" in methods:
59
+ out.append(m.get("name", "").split("/")[-1])
60
+ return sorted(set(n for n in out if n))
61
+ except Exception:
62
+ return []
63
+
64
+ def connect(self) -> tuple[bool, str, list[str]]:
65
+ models = self.fetch_models()
66
+ if models:
67
+ return True, f"Connected \u2014 {len(models)} model(s) visible", models
68
+ return False, "Could not connect \u2014 check API key", []
69
+
70
+ def chat(self, prompt, system_prompt="", history=None) -> str:
71
+ base = self.get_base_url()
72
+ if not base:
73
+ return "AI not configured."
74
+ model = self.get_model()
75
+ try:
76
+ url = f"{base}/models/{model}:generateContent{self._key_suffix()}"
77
+ payload = {
78
+ "contents": self._build_contents(history, prompt),
79
+ "systemInstruction": {"parts": [{"text": system_prompt or ""}]},
80
+ }
81
+ temp = self.config.get("temperature")
82
+ top_p = self.config.get("top_p")
83
+ if temp is not None or top_p is not None:
84
+ gen = {}
85
+ if temp is not None:
86
+ gen["temperature"] = temp
87
+ if top_p is not None:
88
+ gen["topP"] = top_p
89
+ payload["generationConfig"] = gen
90
+ resp = requests.post(url, json=payload, timeout=30)
91
+ self._raise_for_status(resp)
92
+ data = resp.json()
93
+ return data["candidates"][0]["content"]["parts"][0]["text"]
94
+ except Exception as e:
95
+ return f"Error: {e}"
96
+
97
+ def stream(self, prompt, system_prompt="", history=None):
98
+ base = self.get_base_url()
99
+ if not base:
100
+ yield "AI not configured."
101
+ return
102
+ model = self.get_model()
103
+ try:
104
+ url = (f"{base}/models/{model}:streamGenerateContent"
105
+ f"?alt=sse{self._key_suffix()}")
106
+ payload = {
107
+ "contents": self._build_contents(history, prompt),
108
+ "systemInstruction": {"parts": [{"text": system_prompt or ""}]},
109
+ }
110
+ temp = self.config.get("temperature")
111
+ top_p = self.config.get("top_p")
112
+ if temp is not None or top_p is not None:
113
+ gen = {}
114
+ if temp is not None:
115
+ gen["temperature"] = temp
116
+ if top_p is not None:
117
+ gen["topP"] = top_p
118
+ payload["generationConfig"] = gen
119
+ resp = requests.post(url, json=payload, timeout=60, stream=True)
120
+ self._raise_for_status(resp)
121
+ for line in resp.iter_lines(decode_unicode=True):
122
+ if not line or not line.startswith("data:"):
123
+ continue
124
+ try:
125
+ obj = json.loads(line[5:].strip())
126
+ except ValueError:
127
+ continue
128
+ candidates = obj.get("candidates") or []
129
+ if candidates:
130
+ parts = (candidates[0].get("content") or {}).get("parts") or []
131
+ for part in parts:
132
+ piece = part.get("text")
133
+ if piece:
134
+ yield piece
135
+ except Exception as e:
136
+ yield f"Error: {e}"
137
+
138
+ def vision(self, prompt, image_b64, mime) -> str:
139
+ base = self.get_base_url()
140
+ if not base:
141
+ return "AI not configured."
142
+ model = self.get_model()
143
+ try:
144
+ url = f"{base}/models/{model}:generateContent{self._key_suffix()}"
145
+ payload = {
146
+ "contents": [{"role": "user", "parts": [
147
+ {"text": prompt},
148
+ {"inline_data": {"mime_type": mime, "data": image_b64}},
149
+ ]}],
150
+ }
151
+ resp = requests.post(url, json=payload, timeout=60)
152
+ self._raise_for_status(resp)
153
+ return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
154
+ except Exception as e:
155
+ return f"Vision error: {e}"
156
+
157
+ def health_check(self) -> dict:
158
+ start = time.time()
159
+ models = self.fetch_models()
160
+ elapsed = time.time() - start
161
+ return {
162
+ "provider": self.ID,
163
+ "status": "ok" if models else "error",
164
+ "latency_ms": round(elapsed * 1000),
165
+ "models_count": len(models),
166
+ "base_url": self.get_base_url(),
167
+ }
168
+
169
+ @staticmethod
170
+ def _raise_for_status(resp):
171
+ if resp.status_code >= 400:
172
+ try:
173
+ body = resp.json()
174
+ err = (body.get("error", {}).get("message")
175
+ or str(body)[:300])
176
+ except Exception:
177
+ err = resp.text[:300] if resp.text else resp.reason
178
+ raise RuntimeError(f"HTTP {resp.status_code}: {err}")