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,172 @@
1
+ """Anthropic Messages API 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 AnthropicProvider(BaseProvider):
13
+ ID = "anthropic"
14
+ NAME = "Anthropic (Claude)"
15
+ NEEDS_KEY = True
16
+ DEFAULT_MODEL = "claude-3-5-sonnet-20240620" # fallback only — resolved from providers.json when available
17
+ API_STYLE = "anthropic"
18
+ supports_tools = True
19
+ supports_reasoning = True
20
+ supports_vision = True
21
+ supports_streaming = True
22
+
23
+ @classmethod
24
+ def _default_url(cls):
25
+ return "https://api.anthropic.com/v1"
26
+
27
+ @classmethod
28
+ def default_model(cls):
29
+ """Data-driven default from providers.json; DEFAULT_MODEL as fallback."""
30
+ from ..models import manager as _mgr
31
+ return _mgr.default_model_for(cls.ID, cls.DEFAULT_MODEL)
32
+
33
+ def _headers(self):
34
+ return {
35
+ "x-api-key": self.get_api_key(),
36
+ "anthropic-version": "2023-06-01",
37
+ "content-type": "application/json",
38
+ }
39
+
40
+ def _build_messages(self, history, prompt):
41
+ messages = [{"role": role, "content": text} for role, text in (history or [])]
42
+ messages.append({"role": "user", "content": prompt})
43
+ return messages
44
+
45
+ def fetch_models(self) -> list[str]:
46
+ return []
47
+
48
+ def connect(self) -> tuple[bool, str, list[str]]:
49
+ base = self.get_base_url()
50
+ if not base:
51
+ return False, "No base URL configured", []
52
+ try:
53
+ resp = requests.get(f"{base}/models", headers=self._headers(), timeout=8)
54
+ if resp.status_code >= 400 and resp.status_code != 404:
55
+ if resp.status_code == 401:
56
+ return False, "Invalid API key (HTTP 401)", []
57
+ if resp.status_code == 403:
58
+ return False, "Access forbidden (HTTP 403)", []
59
+ return False, f"HTTP {resp.status_code}", []
60
+ return True, f"Connected to {base}", []
61
+ except requests.exceptions.ConnectionError:
62
+ return False, f"Cannot reach {base}", []
63
+ except requests.exceptions.Timeout:
64
+ return False, "Connection timed out", []
65
+ except Exception as e:
66
+ return False, f"Connection failed: {e}"
67
+
68
+ def chat(self, prompt, system_prompt="", history=None) -> str:
69
+ base = self.get_base_url()
70
+ if not base:
71
+ return "AI not configured."
72
+ try:
73
+ payload = {
74
+ "model": self.get_model(),
75
+ "max_tokens": 1024,
76
+ "system": system_prompt or "",
77
+ "messages": self._build_messages(history, prompt),
78
+ }
79
+ temp = self.config.get("temperature")
80
+ if temp is not None:
81
+ payload["temperature"] = temp
82
+ top_p = self.config.get("top_p")
83
+ if top_p is not None:
84
+ payload["top_p"] = top_p
85
+ resp = requests.post(f"{base}/messages", headers=self._headers(),
86
+ json=payload, timeout=30)
87
+ self._raise_for_status(resp)
88
+ return resp.json()["content"][0]["text"]
89
+ except Exception as e:
90
+ return f"Error: {e}"
91
+
92
+ def stream(self, prompt, system_prompt="", history=None):
93
+ base = self.get_base_url()
94
+ if not base:
95
+ yield "AI not configured."
96
+ return
97
+ try:
98
+ payload = {
99
+ "model": self.get_model(),
100
+ "max_tokens": 1024,
101
+ "system": system_prompt or "",
102
+ "messages": self._build_messages(history, prompt),
103
+ "stream": True,
104
+ }
105
+ temp = self.config.get("temperature")
106
+ if temp is not None:
107
+ payload["temperature"] = temp
108
+ top_p = self.config.get("top_p")
109
+ if top_p is not None:
110
+ payload["top_p"] = top_p
111
+ resp = requests.post(f"{base}/messages", headers=self._headers(),
112
+ json=payload, timeout=60, stream=True)
113
+ self._raise_for_status(resp)
114
+ for line in resp.iter_lines(decode_unicode=True):
115
+ if not line or not line.startswith("data:"):
116
+ continue
117
+ try:
118
+ obj = json.loads(line[5:].strip())
119
+ except ValueError:
120
+ continue
121
+ if obj.get("type") == "content_block_delta":
122
+ piece = (obj.get("delta") or {}).get("text")
123
+ if piece:
124
+ yield piece
125
+ except Exception as e:
126
+ yield f"Error: {e}"
127
+
128
+ def vision(self, prompt, image_b64, mime) -> str:
129
+ base = self.get_base_url()
130
+ if not base:
131
+ return "AI not configured."
132
+ try:
133
+ payload = {
134
+ "model": self.get_model(),
135
+ "max_tokens": 1024,
136
+ "system": ("You are CAT AI. Describe and analyze "
137
+ "the attached image precisely."),
138
+ "messages": [{"role": "user", "content": [
139
+ {"type": "image", "source": {
140
+ "type": "base64", "media_type": mime, "data": image_b64}},
141
+ {"type": "text", "text": prompt},
142
+ ]}],
143
+ }
144
+ resp = requests.post(f"{base}/messages", headers=self._headers(),
145
+ json=payload, timeout=60)
146
+ self._raise_for_status(resp)
147
+ return resp.json()["content"][0]["text"]
148
+ except Exception as e:
149
+ return f"Vision error: {e}"
150
+
151
+ def health_check(self) -> dict:
152
+ start = time.time()
153
+ ok, msg, models = self.connect()
154
+ elapsed = time.time() - start
155
+ return {
156
+ "provider": self.ID,
157
+ "status": "ok" if ok else "error",
158
+ "latency_ms": round(elapsed * 1000),
159
+ "message": msg,
160
+ "base_url": self.get_base_url(),
161
+ }
162
+
163
+ @staticmethod
164
+ def _raise_for_status(resp):
165
+ if resp.status_code >= 400:
166
+ try:
167
+ body = resp.json()
168
+ err = (body.get("error", {}).get("message")
169
+ or str(body)[:300])
170
+ except Exception:
171
+ err = resp.text[:300] if resp.text else resp.reason
172
+ raise RuntimeError(f"HTTP {resp.status_code}: {err}")
@@ -0,0 +1,416 @@
1
+ """
2
+ CAT Provider Auto-Update System
3
+
4
+ This module automatically fetches and updates AI provider information
5
+ from provider APIs and a central registry. It ensures CAT always has
6
+ the latest models and providers available.
7
+
8
+ Features:
9
+ - Auto-discovery of new models from provider APIs
10
+ - Periodic background updates
11
+ - Fallback to cached data when offline
12
+ - Support for custom provider endpoints
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import time
18
+ import threading
19
+ import logging
20
+ from datetime import datetime, timedelta
21
+ from typing import Dict, List, Optional, Any
22
+ from pathlib import Path
23
+
24
+ _LOG = logging.getLogger("cct.auto_update")
25
+
26
+ # Configuration
27
+ UPDATE_INTERVAL_HOURS = 24 # Check for updates every 24 hours
28
+ CACHE_DIR = os.path.join(os.path.expanduser("~"), ".cct", "cache", "providers")
29
+ PROVIDERS_CACHE_FILE = os.path.join(CACHE_DIR, "providers_cache.json")
30
+ LAST_UPDATE_FILE = os.path.join(CACHE_DIR, "last_update.json")
31
+ CUSTOM_PROVIDERS_FILE = os.path.join(os.path.expanduser("~"), ".cct", "custom_providers.json")
32
+
33
+ # Central registry URL (could be a GitHub raw URL or custom server)
34
+ CENTRAL_REGISTRY_URL = "https://raw.githubusercontent.com/cct-ai/providers/main/providers.json"
35
+
36
+ # Ensure cache directory exists
37
+ os.makedirs(CACHE_DIR, exist_ok=True)
38
+
39
+
40
+ class ProviderAutoUpdater:
41
+ """Handles automatic updates of AI provider information."""
42
+
43
+ def __init__(self):
44
+ self._update_lock = threading.Lock()
45
+ self._background_thread = None
46
+ self._stop_event = threading.Event()
47
+ self._providers_data = None
48
+ self._last_update = None
49
+
50
+ def start_background_updates(self):
51
+ """Start background update thread."""
52
+ if self._background_thread and self._background_thread.is_alive():
53
+ return
54
+
55
+ self._stop_event.clear()
56
+ self._background_thread = threading.Thread(
57
+ target=self._update_loop,
58
+ daemon=True,
59
+ name="cct-provider-updater"
60
+ )
61
+ self._background_thread.start()
62
+ _LOG.info("Started background provider updater")
63
+
64
+ def stop_background_updates(self):
65
+ """Stop background update thread."""
66
+ self._stop_event.set()
67
+ if self._background_thread:
68
+ self._background_thread.join(timeout=5)
69
+ _LOG.info("Stopped background provider updater")
70
+
71
+ def _update_loop(self):
72
+ """Background loop that checks for updates periodically."""
73
+ while not self._stop_event.is_set():
74
+ try:
75
+ self.check_and_update()
76
+ except Exception as e:
77
+ _LOG.error(f"Error in update loop: {e}")
78
+
79
+ # Wait for next update interval or stop event
80
+ self._stop_event.wait(UPDATE_INTERVAL_HOURS * 3600)
81
+
82
+ def check_and_update(self, force: bool = False) -> bool:
83
+ """Check for updates and apply if available.
84
+
85
+ Args:
86
+ force: Force update even if recently checked
87
+
88
+ Returns:
89
+ True if updates were applied, False otherwise
90
+ """
91
+ with self._update_lock:
92
+ try:
93
+ # Check if we need to update
94
+ if not force and not self._needs_update():
95
+ _LOG.info("Provider data is up to date")
96
+ return False
97
+
98
+ _LOG.info("Checking for provider updates...")
99
+
100
+ # Try to fetch from central registry
101
+ new_data = self._fetch_central_registry()
102
+
103
+ if new_data:
104
+ # Merge with local custom providers
105
+ custom_providers = self._load_custom_providers()
106
+ if custom_providers:
107
+ new_data = self._merge_providers(new_data, custom_providers)
108
+
109
+ # Save to cache
110
+ self._save_to_cache(new_data)
111
+
112
+ # Update in-memory data
113
+ self._providers_data = new_data
114
+ self._last_update = datetime.now()
115
+
116
+ # Save last update timestamp
117
+ self._save_last_update()
118
+
119
+ _LOG.info(f"Updated providers: {len(new_data.get('providers', []))} providers")
120
+ return True
121
+ else:
122
+ _LOG.warning("Failed to fetch central registry, using cached data")
123
+ return False
124
+
125
+ except Exception as e:
126
+ _LOG.error(f"Error checking for updates: {e}")
127
+ return False
128
+
129
+ def _needs_update(self) -> bool:
130
+ """Check if we need to update based on last update time."""
131
+ try:
132
+ if os.path.exists(LAST_UPDATE_FILE):
133
+ with open(LAST_UPDATE_FILE, "r") as f:
134
+ data = json.load(f)
135
+ last_str = data.get("last_update")
136
+ if last_str:
137
+ last_time = datetime.fromisoformat(last_str)
138
+ if datetime.now() - last_time < timedelta(hours=UPDATE_INTERVAL_HOURS):
139
+ return False
140
+ return True
141
+ except Exception:
142
+ return True
143
+
144
+ def _fetch_central_registry(self) -> Optional[Dict]:
145
+ """Fetch provider data from central registry."""
146
+ try:
147
+ import requests
148
+
149
+ # Try GitHub raw URL first
150
+ resp = requests.get(CENTRAL_REGISTRY_URL, timeout=10)
151
+ if resp.status_code == 200:
152
+ return resp.json()
153
+
154
+ # Fallback to other sources
155
+ fallback_urls = [
156
+ "https://raw.githubusercontent.com/cct-ai/providers/main/providers.json",
157
+ "https://cdn.jsdelivr.net/gh/cct-ai/providers@main/providers.json",
158
+ ]
159
+
160
+ for url in fallback_urls:
161
+ try:
162
+ resp = requests.get(url, timeout=10)
163
+ if resp.status_code == 200:
164
+ return resp.json()
165
+ except Exception:
166
+ continue
167
+
168
+ return None
169
+
170
+ except Exception as e:
171
+ _LOG.error(f"Error fetching central registry: {e}")
172
+ return None
173
+
174
+ def _load_custom_providers(self) -> List[Dict]:
175
+ """Load custom providers from local file."""
176
+ try:
177
+ if os.path.exists(CUSTOM_PROVIDERS_FILE):
178
+ with open(CUSTOM_PROVIDERS_FILE, "r", encoding="utf-8") as f:
179
+ data = json.load(f)
180
+ return data.get("providers", [])
181
+ except Exception as e:
182
+ _LOG.error(f"Error loading custom providers: {e}")
183
+ return []
184
+
185
+ def _merge_providers(self, central_data: Dict, custom_providers: List[Dict]) -> Dict:
186
+ """Merge central registry data with custom providers."""
187
+ providers = central_data.get("providers", [])
188
+ existing_ids = {p.get("id") for p in providers}
189
+
190
+ for custom in custom_providers:
191
+ if custom.get("id") not in existing_ids:
192
+ providers.append(custom)
193
+
194
+ central_data["providers"] = providers
195
+ return central_data
196
+
197
+ def _save_to_cache(self, data: Dict):
198
+ """Save provider data to cache file."""
199
+ try:
200
+ with open(PROVIDERS_CACHE_FILE, "w", encoding="utf-8") as f:
201
+ json.dump(data, f, indent=2, ensure_ascii=False)
202
+ except Exception as e:
203
+ _LOG.error(f"Error saving to cache: {e}")
204
+
205
+ def _save_last_update(self):
206
+ """Save last update timestamp."""
207
+ try:
208
+ with open(LAST_UPDATE_FILE, "w") as f:
209
+ json.dump({"last_update": datetime.now().isoformat()}, f)
210
+ except Exception as e:
211
+ _LOG.error(f"Error saving last update: {e}")
212
+
213
+ def get_providers(self, force_refresh: bool = False) -> Dict:
214
+ """Get provider data, loading from cache if needed.
215
+
216
+ Args:
217
+ force_refresh: Force refresh from cache/network
218
+
219
+ Returns:
220
+ Dictionary with providers data
221
+ """
222
+ if self._providers_data and not force_refresh:
223
+ return self._providers_data
224
+
225
+ # Try to load from cache first
226
+ try:
227
+ if os.path.exists(PROVIDERS_CACHE_FILE):
228
+ with open(PROVIDERS_CACHE_FILE, "r", encoding="utf-8") as f:
229
+ self._providers_data = json.load(f)
230
+ return self._providers_data
231
+ except Exception as e:
232
+ _LOG.error(f"Error loading from cache: {e}")
233
+
234
+ # Return empty data if nothing available
235
+ return {"version": 1, "providers": []}
236
+
237
+ def add_custom_provider(self, provider: Dict) -> bool:
238
+ """Add a custom provider to the local configuration.
239
+
240
+ Args:
241
+ provider: Provider configuration dictionary
242
+
243
+ Returns:
244
+ True if added successfully, False otherwise
245
+ """
246
+ try:
247
+ custom_providers = self._load_custom_providers()
248
+
249
+ # Check if provider already exists
250
+ existing_ids = {p.get("id") for p in custom_providers}
251
+ if provider.get("id") in existing_ids:
252
+ _LOG.warning(f"Provider {provider.get('id')} already exists")
253
+ return False
254
+
255
+ # Add timestamp
256
+ provider["last_updated"] = datetime.now().isoformat()
257
+ custom_providers.append(provider)
258
+
259
+ # Save to file
260
+ with open(CUSTOM_PROVIDERS_FILE, "w", encoding="utf-8") as f:
261
+ json.dump({"providers": custom_providers}, f, indent=2, ensure_ascii=False)
262
+
263
+ _LOG.info(f"Added custom provider: {provider.get('id')}")
264
+ return True
265
+
266
+ except Exception as e:
267
+ _LOG.error(f"Error adding custom provider: {e}")
268
+ return False
269
+
270
+ def remove_custom_provider(self, provider_id: str) -> bool:
271
+ """Remove a custom provider from the local configuration.
272
+
273
+ Args:
274
+ provider_id: ID of the provider to remove
275
+
276
+ Returns:
277
+ True if removed successfully, False otherwise
278
+ """
279
+ try:
280
+ custom_providers = self._load_custom_providers()
281
+ original_count = len(custom_providers)
282
+
283
+ # Remove provider
284
+ custom_providers = [p for p in custom_providers if p.get("id") != provider_id]
285
+
286
+ if len(custom_providers) == original_count:
287
+ _LOG.warning(f"Provider {provider_id} not found")
288
+ return False
289
+
290
+ # Save to file
291
+ with open(CUSTOM_PROVIDERS_FILE, "w", encoding="utf-8") as f:
292
+ json.dump({"providers": custom_providers}, f, indent=2, ensure_ascii=False)
293
+
294
+ _LOG.info(f"Removed custom provider: {provider_id}")
295
+ return True
296
+
297
+ except Exception as e:
298
+ _LOG.error(f"Error removing custom provider: {e}")
299
+ return False
300
+
301
+ def discover_provider_models(self, provider_id: str, base_url: str,
302
+ api_style: str = "openai") -> List[str]:
303
+ """Discover available models from a provider's API.
304
+
305
+ Args:
306
+ provider_id: Provider identifier
307
+ base_url: API base URL
308
+ api_style: API style (openai, anthropic, gemini, ollama)
309
+
310
+ Returns:
311
+ List of available model names
312
+ """
313
+ try:
314
+ import requests
315
+
316
+ models = []
317
+
318
+ if api_style == "ollama":
319
+ # Ollama uses /api/tags
320
+ resp = requests.get(f"{base_url}/api/tags", timeout=10)
321
+ if resp.status_code == 200:
322
+ data = resp.json()
323
+ models = [m.get("name", "") for m in data.get("models", [])]
324
+
325
+ elif api_style == "openai":
326
+ # OpenAI-compatible uses /v1/models
327
+ resp = requests.get(f"{base_url}/v1/models", timeout=10)
328
+ if resp.status_code == 200:
329
+ data = resp.json()
330
+ models = [m.get("id", "") for m in data.get("data", [])]
331
+
332
+ elif api_style == "anthropic":
333
+ # Anthropic doesn't have a public models endpoint
334
+ # Use known models list
335
+ models = [
336
+ "claude-4-fable-5.1",
337
+ "claude-sonnet-4-20250514",
338
+ "claude-opus-4-20250514",
339
+ "claude-3-7-sonnet-20250219",
340
+ "claude-3-5-sonnet-20241022",
341
+ "claude-3-5-haiku-20241022",
342
+ ]
343
+
344
+ elif api_style == "gemini":
345
+ # Gemini uses specific endpoint
346
+ resp = requests.get(
347
+ f"{base_url}/models",
348
+ timeout=10
349
+ )
350
+ if resp.status_code == 200:
351
+ data = resp.json()
352
+ models = [m.get("name", "") for m in data.get("models", [])]
353
+
354
+ _LOG.info(f"Discovered {len(models)} models for {provider_id}")
355
+ return sorted(set(models))
356
+
357
+ except Exception as e:
358
+ _LOG.error(f"Error discovering models for {provider_id}: {e}")
359
+ return []
360
+
361
+
362
+ # Global updater instance
363
+ _updater = None
364
+ _updater_lock = threading.Lock()
365
+
366
+
367
+ def get_updater() -> ProviderAutoUpdater:
368
+ """Get the global provider auto-updater instance."""
369
+ global _updater
370
+ with _updater_lock:
371
+ if _updater is None:
372
+ _updater = ProviderAutoUpdater()
373
+ return _updater
374
+
375
+
376
+ def start_auto_updates():
377
+ """Start the auto-update system."""
378
+ updater = get_updater()
379
+ updater.start_background_updates()
380
+
381
+
382
+ def stop_auto_updates():
383
+ """Stop the auto-update system."""
384
+ updater = get_updater()
385
+ updater.stop_background_updates()
386
+
387
+
388
+ def force_update() -> bool:
389
+ """Force an immediate update check."""
390
+ updater = get_updater()
391
+ return updater.check_and_update(force=True)
392
+
393
+
394
+ def get_latest_providers() -> Dict:
395
+ """Get the latest provider data."""
396
+ updater = get_updater()
397
+ return updater.get_providers(force_refresh=True)
398
+
399
+
400
+ def add_custom_provider(provider: Dict) -> bool:
401
+ """Add a custom provider."""
402
+ updater = get_updater()
403
+ return updater.add_custom_provider(provider)
404
+
405
+
406
+ def remove_custom_provider(provider_id: str) -> bool:
407
+ """Remove a custom provider."""
408
+ updater = get_updater()
409
+ return updater.remove_custom_provider(provider_id)
410
+
411
+
412
+ def discover_models(provider_id: str, base_url: str,
413
+ api_style: str = "openai") -> List[str]:
414
+ """Discover models from a provider."""
415
+ updater = get_updater()
416
+ return updater.discover_provider_models(provider_id, base_url, api_style)
@@ -0,0 +1,105 @@
1
+ """Abstract base class for all AI providers in CCT."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+
7
+ class BaseProvider(ABC):
8
+ """Every provider implements this interface. Add a new one by
9
+ subclassing and registering it in ProviderManager."""
10
+
11
+ ID: str = ""
12
+ NAME: str = ""
13
+ NEEDS_KEY: bool = True
14
+ DEFAULT_MODEL: str = ""
15
+ API_STYLE: str = "openai"
16
+
17
+ # ── Capability flags (override in subclasses) ─────────────────────
18
+ supports_model_listing: bool = True
19
+ supports_streaming: bool = True
20
+ supports_tools: bool = False
21
+ supports_reasoning: bool = False
22
+ supports_vision: bool = False
23
+ supports_audio: bool = False
24
+ supports_images: bool = False
25
+ supports_embeddings: bool = False
26
+ supports_reranking: bool = False
27
+
28
+ def __init__(self, config: dict):
29
+ self.config = config
30
+
31
+ @abstractmethod
32
+ def connect(self) -> tuple[bool, str, list[str]]:
33
+ """Test connectivity. Returns (ok, message, available_models)."""
34
+
35
+ @abstractmethod
36
+ def fetch_models(self) -> list[str]:
37
+ """GET /models or equivalent. Returns list of model IDs or []."""
38
+
39
+ def fetch_models_paginated(self, cancel_event=None) -> list[str]:
40
+ """Fetch models with pagination support. Override for providers
41
+ with paginated APIs. Default implementation delegates to fetch_models().
42
+ `cancel_event` is a threading.Event; check periodically and return
43
+ early if set."""
44
+ if cancel_event and cancel_event.is_set():
45
+ return []
46
+ return self.fetch_models()
47
+
48
+ @abstractmethod
49
+ def chat(self, prompt: str, system_prompt: str = "",
50
+ history: Optional[list] = None) -> str:
51
+ """Non-streaming chat completion."""
52
+
53
+ @abstractmethod
54
+ def stream(self, prompt: str, system_prompt: str = "",
55
+ history: Optional[list] = None):
56
+ """Streaming generator yielding text fragments."""
57
+
58
+ def embeddings(self, texts: list[str]) -> list[list[float]]:
59
+ raise NotImplementedError(f"{self.ID} does not support embeddings")
60
+
61
+ def vision(self, prompt: str, image_b64: str, mime: str) -> str:
62
+ raise NotImplementedError(f"{self.ID} does not support vision")
63
+
64
+ def health_check(self) -> dict:
65
+ """Return status info: latency, version, etc."""
66
+ return {"provider": self.ID, "status": "unknown"}
67
+
68
+ def get_base_url(self) -> str:
69
+ return (self.config.get("base_url") or
70
+ self.config.get("api_url") or "").rstrip("/")
71
+
72
+ def get_api_key(self) -> str:
73
+ return self.config.get("api_key", "")
74
+
75
+ def get_model(self) -> str:
76
+ return self.config.get("model") or self.default_model()
77
+
78
+ @classmethod
79
+ def default_model(cls) -> str:
80
+ """Data-driven default; overridden by provider classes that resolve
81
+ from providers.json. Falls back to the hardcoded DEFAULT_MODEL."""
82
+ return cls.DEFAULT_MODEL
83
+
84
+ def get_extra_headers(self) -> dict:
85
+ return dict(self.config.get("extra_headers", {}))
86
+
87
+ @classmethod
88
+ def to_provider_dict(cls) -> dict:
89
+ """Return the standard provider info dict for this class."""
90
+ return {
91
+ "id": cls.ID,
92
+ "name": cls.NAME,
93
+ "url": cls._default_url() if hasattr(cls, "_default_url") else "",
94
+ "api_style": cls.API_STYLE,
95
+ "needs_key": cls.NEEDS_KEY,
96
+ "default_model": cls.default_model(),
97
+ "models": [],
98
+ "models_cache": [],
99
+ "features": [],
100
+ "openai_compat": cls.API_STYLE == "openai",
101
+ "desc": "",
102
+ }
103
+
104
+ def dispose(self):
105
+ """Cleanup hook when provider is removed."""