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,174 @@
1
+ """
2
+ CAT — Backup Provider & Resilience System: Capability Matcher
3
+ Author: Kazi Zillani
4
+
5
+ Ensures backup selection is intelligent rather than sequential:
6
+ Compares Task Requirements + Provider Capabilities + Privacy Policy + Cost Policy
7
+ to select only backups that can actually perform the user's task.
8
+ """
9
+
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+ from ..model_router import (
13
+ POLICY_BEST_AVAILABLE,
14
+ POLICY_CLOUD_FIRST,
15
+ POLICY_LOCAL_FIRST,
16
+ POLICY_NEVER_CLOUD,
17
+ ModelCapabilities,
18
+ _capabilities_for,
19
+ )
20
+ from .health_monitor import get_health_monitor
21
+ from .types import CandidateTarget, CostPolicy, SecurityLevel, TaskRequirements
22
+
23
+
24
+ class CapabilityMatcher:
25
+ """Matches task requirements against candidate provider/model endpoints."""
26
+
27
+ def __init__(self):
28
+ self.health_monitor = get_health_monitor()
29
+
30
+ def is_local_provider(self, provider: str) -> bool:
31
+ return str(provider or "").strip().lower() in ("ollama", "local", "vllm", "llamacpp")
32
+
33
+ def is_free_provider(self, provider: str, model: str = "") -> bool:
34
+ p_low = str(provider or "").strip().lower()
35
+ if self.is_local_provider(p_low):
36
+ return True
37
+ # Free cloud providers/tiers
38
+ if p_low in ("groq", "cerebras", "gemini"):
39
+ return True
40
+ return False
41
+
42
+ def evaluate_target(
43
+ self,
44
+ config: dict,
45
+ requirements: TaskRequirements,
46
+ privacy_policy: str = POLICY_LOCAL_FIRST,
47
+ cost_policy: CostPolicy = CostPolicy.ANY,
48
+ security_level: SecurityLevel = SecurityLevel.BALANCED,
49
+ entry: Optional[dict] = None,
50
+ is_backup: bool = True,
51
+ tier: int = 1,
52
+ ) -> Optional[CandidateTarget]:
53
+ """Verify eligibility and score a candidate target. Returns CandidateTarget or None."""
54
+ provider = str(config.get("provider") or "").strip().lower()
55
+ model = str(config.get("model") or "").strip()
56
+ if not provider or not model:
57
+ return None
58
+
59
+ is_local = self.is_local_provider(provider)
60
+
61
+ # 1. Privacy Policy Enforcement (ABSOLUTE RULE: User privacy overrides failover)
62
+ if privacy_policy == POLICY_NEVER_CLOUD and not is_local:
63
+ return None
64
+
65
+ # 2. Cost Policy Enforcement
66
+ if cost_policy == CostPolicy.LOCAL_ONLY and not is_local:
67
+ return None
68
+ if cost_policy == CostPolicy.FREE_ONLY and not self.is_free_provider(provider, model):
69
+ return None
70
+
71
+ # 3. Credential Check (Ollama needs no key; cloud needs API key)
72
+ needs_key = config.get("needs_key", not is_local)
73
+ if is_local:
74
+ needs_key = False
75
+ api_key = str(config.get("api_key") or "").strip()
76
+ if needs_key and not api_key:
77
+ return None
78
+
79
+ # 4. Capability Check
80
+ caps = _capabilities_for(config, is_backup=is_backup)
81
+
82
+ # Vision requirement
83
+ if requirements.vision and not caps.vision:
84
+ return None
85
+
86
+ # Tools requirement (e.g. coding agent requires tool loop compatibility)
87
+ if requirements.tools:
88
+ # Base models that cannot follow instructions should be skipped
89
+ m_low = model.lower()
90
+ if any(b in m_low for b in ("-base", "_base", "base-q", ":base")):
91
+ return None
92
+ if not caps.tools:
93
+ return None
94
+
95
+ # Context Window requirement
96
+ if requirements.min_context > 0 and caps.context_window < requirements.min_context:
97
+ return None
98
+ if requirements.long_context and caps.context_window < 60000:
99
+ return None
100
+
101
+ # 5. Circuit Breaker & Health Check
102
+ health = self.health_monitor.get_health(provider, model)
103
+ if health.circuit_open and not self.health_monitor.is_healthy(provider, model):
104
+ # Barred by active cooldown
105
+ return None
106
+
107
+ # 6. Scoring
108
+ score = self.calculate_score(caps, health, requirements, privacy_policy, is_local)
109
+
110
+ return CandidateTarget(
111
+ provider=provider,
112
+ model=model,
113
+ config=dict(config),
114
+ entry=entry,
115
+ is_backup=is_backup,
116
+ is_local=is_local,
117
+ score=score,
118
+ tier=tier,
119
+ )
120
+
121
+ def calculate_score(
122
+ self,
123
+ caps: ModelCapabilities,
124
+ health: Any,
125
+ requirements: TaskRequirements,
126
+ privacy_policy: str,
127
+ is_local: bool,
128
+ ) -> float:
129
+ """Dynamic routing score (0 to 100) based on observable CAT state."""
130
+ score = 50.0
131
+
132
+ # Capability fitness (up to +25)
133
+ if requirements.coding:
134
+ m_low = caps.model.lower()
135
+ if any(k in m_low for k in ("coder", "code", "qwen", "deepseek", "sonnet")):
136
+ score += 15.0
137
+ if requirements.reasoning and caps.reasoning:
138
+ score += 10.0
139
+ if requirements.vision and caps.vision:
140
+ score += 10.0
141
+
142
+ # Privacy fitness (up to +20)
143
+ if privacy_policy == POLICY_LOCAL_FIRST:
144
+ score += 20.0 if is_local else 0.0
145
+ elif privacy_policy == POLICY_CLOUD_FIRST:
146
+ score += 20.0 if not is_local else 5.0
147
+ elif privacy_policy == POLICY_BEST_AVAILABLE:
148
+ score += 10.0
149
+
150
+ # Health & Latency fitness (up to +20)
151
+ if health.status in ("healthy", "local"):
152
+ score += 15.0
153
+ elif health.status == "slow":
154
+ score += 5.0
155
+ if health.latency_ms:
156
+ if health.latency_ms < 1000:
157
+ score += 5.0
158
+ elif health.latency_ms > 4000:
159
+ score -= 10.0
160
+
161
+ # Consecutive failures penalty
162
+ score -= min(30.0, health.consecutive_failures * 10.0)
163
+
164
+ return max(0.0, min(100.0, score))
165
+
166
+
167
+ _GLOBAL_MATCHER: Optional[CapabilityMatcher] = None
168
+
169
+
170
+ def get_capability_matcher() -> CapabilityMatcher:
171
+ global _GLOBAL_MATCHER
172
+ if _GLOBAL_MATCHER is None:
173
+ _GLOBAL_MATCHER = CapabilityMatcher()
174
+ return _GLOBAL_MATCHER
@@ -0,0 +1,158 @@
1
+ """
2
+ CAT — Backup Provider & Resilience System: Circuit Breaker
3
+ Author: Kazi Zillani
4
+
5
+ Prevents CAT from wasting precious seconds or burning rate limits on broken,
6
+ unreachable, or rate-limited AI endpoints.
7
+ """
8
+
9
+ import threading
10
+ import time
11
+ from typing import Dict, Optional, Tuple
12
+
13
+
14
+ class CircuitBreakerState:
15
+ CLOSED = "closed" # Normal operation: requests pass through
16
+ OPEN = "open" # Tripped: requests fail immediately to prevent stalls
17
+ HALF_OPEN = "half_open" # Cooldown expired: single probe attempt allowed
18
+
19
+
20
+ class CircuitBreaker:
21
+ """Per-endpoint circuit breaker guarding providers and models."""
22
+
23
+ def __init__(
24
+ self,
25
+ failure_threshold: int = 3,
26
+ default_cooldown: float = 30.0,
27
+ rate_limit_cooldown: float = 60.0,
28
+ ):
29
+ self.failure_threshold = max(1, failure_threshold)
30
+ self.default_cooldown = max(0.01, default_cooldown)
31
+ self.rate_limit_cooldown = max(0.01, rate_limit_cooldown)
32
+ self._lock = threading.Lock()
33
+ # (provider, model) -> dict
34
+ self._endpoints: Dict[Tuple[str, str], dict] = {}
35
+
36
+ def _key(self, provider: str, model: str) -> Tuple[str, str]:
37
+ return (str(provider or "").strip().lower(), str(model or "").strip().lower())
38
+
39
+ def _get_record(self, provider: str, model: str) -> dict:
40
+ k = self._key(provider, model)
41
+ if k not in self._endpoints:
42
+ self._endpoints[k] = {
43
+ "state": CircuitBreakerState.CLOSED,
44
+ "consecutive_failures": 0,
45
+ "cooldown_until": 0.0,
46
+ "last_failure_ts": 0.0,
47
+ "last_success_ts": 0.0,
48
+ "last_error": "",
49
+ "probe_in_flight": False,
50
+ }
51
+ return self._endpoints[k]
52
+
53
+ def can_attempt(self, provider: str, model: str) -> bool:
54
+ """True if the endpoint is allowed to receive a request."""
55
+ with self._lock:
56
+ rec = self._get_record(provider, model)
57
+ now = time.time()
58
+ state = rec["state"]
59
+
60
+ if state == CircuitBreakerState.CLOSED:
61
+ return True
62
+
63
+ if state == CircuitBreakerState.OPEN:
64
+ if now >= rec["cooldown_until"]:
65
+ # Cooldown elapsed -> transition to half_open for a probe
66
+ rec["state"] = CircuitBreakerState.HALF_OPEN
67
+ rec["probe_in_flight"] = True
68
+ return True
69
+ return False
70
+
71
+ if state == CircuitBreakerState.HALF_OPEN:
72
+ # Only allow one probe at a time
73
+ if not rec["probe_in_flight"]:
74
+ rec["probe_in_flight"] = True
75
+ return True
76
+ return False
77
+
78
+ return True
79
+
80
+ def record_success(self, provider: str, model: str, latency_ms: Optional[float] = None) -> None:
81
+ """Reset breaker to CLOSED upon a successful turn."""
82
+ with self._lock:
83
+ rec = self._get_record(provider, model)
84
+ rec["state"] = CircuitBreakerState.CLOSED
85
+ rec["consecutive_failures"] = 0
86
+ rec["cooldown_until"] = 0.0
87
+ rec["last_success_ts"] = time.time()
88
+ rec["probe_in_flight"] = False
89
+
90
+ def record_failure(
91
+ self,
92
+ provider: str,
93
+ model: str,
94
+ error: str = "",
95
+ is_rate_limit: bool = False,
96
+ cooldown_seconds: Optional[float] = None,
97
+ ) -> None:
98
+ """Increment failure counter and trip breaker if threshold exceeded."""
99
+ with self._lock:
100
+ rec = self._get_record(provider, model)
101
+ rec["consecutive_failures"] += 1
102
+ now = time.time()
103
+ rec["last_failure_ts"] = now
104
+ rec["last_error"] = str(error)[:160]
105
+ rec["probe_in_flight"] = False
106
+
107
+ cooldown = cooldown_seconds or (
108
+ self.rate_limit_cooldown if is_rate_limit else self.default_cooldown
109
+ )
110
+
111
+ # In HALF_OPEN, any failure immediately trips back to OPEN with full cooldown
112
+ if rec["state"] == CircuitBreakerState.HALF_OPEN:
113
+ rec["state"] = CircuitBreakerState.OPEN
114
+ rec["cooldown_until"] = now + cooldown
115
+ elif is_rate_limit or rec["consecutive_failures"] >= self.failure_threshold:
116
+ rec["state"] = CircuitBreakerState.OPEN
117
+ rec["cooldown_until"] = now + cooldown
118
+
119
+ def get_state(self, provider: str, model: str) -> str:
120
+ """Return current breaker state string."""
121
+ with self._lock:
122
+ rec = self._get_record(provider, model)
123
+ now = time.time()
124
+ if rec["state"] == CircuitBreakerState.OPEN and now >= rec["cooldown_until"]:
125
+ return CircuitBreakerState.HALF_OPEN
126
+ return rec["state"]
127
+
128
+ def is_in_cooldown(self, provider: str, model: str) -> bool:
129
+ """True if the endpoint is strictly barred by an active cooldown."""
130
+ with self._lock:
131
+ rec = self._get_record(provider, model)
132
+ return rec["state"] == CircuitBreakerState.OPEN and time.time() < rec["cooldown_until"]
133
+
134
+ def reset(self, provider: str, model: str) -> None:
135
+ with self._lock:
136
+ rec = self._get_record(provider, model)
137
+ rec["state"] = CircuitBreakerState.CLOSED
138
+ rec["consecutive_failures"] = 0
139
+ rec["cooldown_until"] = 0.0
140
+ rec["probe_in_flight"] = False
141
+
142
+ def reset_all(self) -> None:
143
+ with self._lock:
144
+ for rec in self._endpoints.values():
145
+ rec["state"] = CircuitBreakerState.CLOSED
146
+ rec["consecutive_failures"] = 0
147
+ rec["cooldown_until"] = 0.0
148
+ rec["probe_in_flight"] = False
149
+
150
+
151
+ _GLOBAL_BREAKER: Optional[CircuitBreaker] = None
152
+
153
+
154
+ def get_circuit_breaker() -> CircuitBreaker:
155
+ global _GLOBAL_BREAKER
156
+ if _GLOBAL_BREAKER is None:
157
+ _GLOBAL_BREAKER = CircuitBreaker()
158
+ return _GLOBAL_BREAKER
@@ -0,0 +1,230 @@
1
+ """
2
+ CAT — Backup Provider & Resilience System: Failover Engine
3
+ Author: Kazi Zillani
4
+
5
+ Handles intelligent error classification and two-tier failover orchestration:
6
+ - Tier 1: Alternate fallback models within the SAME provider.
7
+ - Tier 2: Priority-ordered backup providers from the dynamic backup pool.
8
+ """
9
+
10
+ import logging
11
+ import re
12
+ import time
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ from .capability_matcher import get_capability_matcher
16
+ from .health_monitor import get_health_monitor, sanitize_error
17
+ from .types import CandidateTarget, CostPolicy, FailureType, SecurityLevel, TaskRequirements
18
+
19
+ _LOG = logging.getLogger(__name__)
20
+
21
+ # Error pattern classifiers
22
+ _RATE_LIMIT_HINTS = (
23
+ "rate limit", "rate_limit", "429", "too many requests", "resource has been exhausted",
24
+ "insufficient_quota", "quota exceeded", "exceeded your current quota", "tokens per minute",
25
+ "requests per minute", "tpm limit", "rpm limit",
26
+ )
27
+
28
+ _AUTH_HINTS = (
29
+ "401", "403", "unauthorized", "api key", "invalid api key", "authentication failed",
30
+ "forbidden", "permission denied", "invalid token", "bad api key",
31
+ )
32
+
33
+ _MODEL_UNAVAILABLE_HINTS = (
34
+ "model not found", "does not exist", "model unavailable", "not loaded", "no such model",
35
+ "404", "model_not_found", "unknown model", "deprecated",
36
+ )
37
+
38
+ _CONTEXT_HINTS = (
39
+ "maximum context length", "context length exceeded", "too many tokens",
40
+ "token limit", "prompt is too long", "exceeds maximum context",
41
+ )
42
+
43
+ _TRANSIENT_HINTS = (
44
+ "timed out", "timeout", "connection reset", "connection refused", "connection aborted",
45
+ "broken pipe", "server disconnected", "502", "503", "504", "bad gateway",
46
+ "service unavailable", "remote end closed", "incomplete read",
47
+ )
48
+
49
+ _SERVER_ERROR_HINTS = (
50
+ "500", "internal server error", "server error", "backend error",
51
+ )
52
+
53
+
54
+ class FailoverEngine:
55
+ """Classifies provider failures and generates optimal two-tier candidate targets."""
56
+
57
+ def __init__(self):
58
+ self.matcher = get_capability_matcher()
59
+ self.health_monitor = get_health_monitor()
60
+
61
+ def classify_failure(self, error_text: str, status_code: Optional[int] = None) -> FailureType:
62
+ """Classify raw error message or HTTP response into standardized FailureType."""
63
+ if not error_text and not status_code:
64
+ return FailureType.UNKNOWN
65
+
66
+ code = status_code or 0
67
+ if code == 429:
68
+ return FailureType.RATE_LIMIT
69
+ if code in (401, 403):
70
+ return FailureType.AUTH_FAILURE
71
+ if code == 404:
72
+ return FailureType.MODEL_UNAVAILABLE
73
+ if code in (502, 503, 504):
74
+ return FailureType.TRANSIENT
75
+ if code == 500:
76
+ return FailureType.SERVER_ERROR
77
+
78
+ low = (error_text or "").lower()
79
+
80
+ if any(h in low for h in _RATE_LIMIT_HINTS):
81
+ return FailureType.RATE_LIMIT
82
+ if any(h in low for h in _AUTH_HINTS):
83
+ return FailureType.AUTH_FAILURE
84
+ if any(h in low for h in _MODEL_UNAVAILABLE_HINTS):
85
+ return FailureType.MODEL_UNAVAILABLE
86
+ if any(h in low for h in _CONTEXT_HINTS):
87
+ return FailureType.CONTEXT_TOO_LARGE
88
+ if any(h in low for h in _TRANSIENT_HINTS):
89
+ return FailureType.TRANSIENT
90
+ if any(h in low for h in _SERVER_ERROR_HINTS):
91
+ return FailureType.SERVER_ERROR
92
+
93
+ return FailureType.UNKNOWN
94
+
95
+ def is_retryable(self, failure_type: FailureType) -> bool:
96
+ """True if failure is transient and warrants an immediate backoff retry on same model."""
97
+ return failure_type in (FailureType.TRANSIENT, FailureType.TIMEOUT)
98
+
99
+ def should_failover(self, result_text: str) -> bool:
100
+ """True if the assistant response represents an error needing failover."""
101
+ if not result_text or not isinstance(result_text, str):
102
+ return True
103
+ if not result_text.strip():
104
+ return True
105
+
106
+ from ..aicore import is_error_response
107
+ if is_error_response(result_text):
108
+ return True
109
+
110
+ ftype = self.classify_failure(result_text)
111
+ return ftype != FailureType.UNKNOWN
112
+
113
+ def get_provider_fallback_models(self, provider_id: str) -> List[str]:
114
+ """Fetch alternative models configured for the given provider.
115
+ For local providers like Ollama, dynamically checks actually-installed
116
+ models so non-existent catalog entries never cause failover loops."""
117
+ try:
118
+ pid = str(provider_id or "").strip().lower()
119
+ if pid == "ollama":
120
+ try:
121
+ from ..providers.adapters.ollama_adapter import OllamaAdapter
122
+ adapter = OllamaAdapter()
123
+ live = adapter.discover_models(timeout=1.5)
124
+ if live:
125
+ installed = []
126
+ for m in live:
127
+ mid = str(getattr(m, "model_id", "") or "").strip()
128
+ if mid and mid not in installed:
129
+ installed.append(mid)
130
+ if installed:
131
+ return installed
132
+ except Exception:
133
+ pass
134
+ from ..models.manager import get_provider
135
+ prov = get_provider(provider_id)
136
+ if prov and prov.get("fallback_models"):
137
+ return [str(m).strip() for m in prov["fallback_models"] if str(m).strip()]
138
+ except Exception:
139
+ pass
140
+ return []
141
+
142
+ def build_failover_targets(
143
+ self,
144
+ primary_config: dict,
145
+ requirements: Optional[TaskRequirements] = None,
146
+ privacy_policy: str = "local_first",
147
+ cost_policy: CostPolicy = CostPolicy.ANY,
148
+ security_level: SecurityLevel = SecurityLevel.BALANCED,
149
+ ) -> List[CandidateTarget]:
150
+ """Build the complete, two-tier failover walk:
151
+ Tier 1: Primary provider (active model, then alternative models on same provider).
152
+ Tier 2: Backup pool providers in configured priority order.
153
+ """
154
+ req = requirements or TaskRequirements()
155
+ targets: List[CandidateTarget] = []
156
+ seen_endpoints = set()
157
+
158
+ def _add_target(cfg: dict, entry: Optional[dict], is_backup: bool, tier: int):
159
+ prov = str(cfg.get("provider") or "").strip().lower()
160
+ model = str(cfg.get("model") or "").strip()
161
+ if not prov or not model:
162
+ return
163
+ key = (prov, model)
164
+ if key in seen_endpoints:
165
+ return
166
+
167
+ candidate = self.matcher.evaluate_target(
168
+ config=cfg,
169
+ requirements=req,
170
+ privacy_policy=privacy_policy,
171
+ cost_policy=cost_policy,
172
+ security_level=security_level,
173
+ entry=entry,
174
+ is_backup=is_backup,
175
+ tier=tier,
176
+ )
177
+ if candidate:
178
+ targets.append(candidate)
179
+ seen_endpoints.add(key)
180
+
181
+ # 1. Primary endpoint
182
+ prim = dict(primary_config or {})
183
+ prim_prov = str(prim.get("provider") or "").strip().lower()
184
+ if prim_prov:
185
+ # Ensure API key resolved
186
+ if not (prim.get("api_key") or "").strip():
187
+ try:
188
+ from ..providers.provider_manager import get_env_api_key
189
+ k = get_env_api_key(prim_prov)
190
+ if k:
191
+ prim["api_key"] = k
192
+ except Exception:
193
+ pass
194
+ _add_target(prim, entry=None, is_backup=False, tier=1)
195
+
196
+ # Tier 1 Fallback Models on the SAME provider (capped to at most 2 to avoid cascading hangs)
197
+ fb_models = self.get_provider_fallback_models(prim_prov)
198
+ for fb_model in fb_models[:2]:
199
+ if fb_model.lower() != str(prim.get("model", "")).lower():
200
+ tier1_cfg = dict(prim)
201
+ tier1_cfg["model"] = fb_model
202
+ _add_target(tier1_cfg, entry=None, is_backup=False, tier=1)
203
+
204
+ # 2. Tier 2: Dynamic Backup Pool
205
+ try:
206
+ from ..providers.provider_manager import backup_configs
207
+ for bcfg, entry in backup_configs():
208
+ _add_target(bcfg, entry=entry, is_backup=True, tier=2)
209
+ except Exception as e:
210
+ _LOG.debug("Error loading backup configs: %s", e)
211
+
212
+ # If strict security, only allow explicitly configured backup providers
213
+ if security_level == SecurityLevel.STRICT:
214
+ targets = [t for t in targets if not t.is_backup or (t.entry and t.entry.get("enabled"))]
215
+
216
+ # Prioritize fresh endpoints over endpoints recently in cooldown
217
+ fresh = [t for t in targets if self.health_monitor.is_healthy(t.provider, t.model)]
218
+ demoted = [t for t in targets if t not in fresh]
219
+
220
+ return fresh + demoted
221
+
222
+
223
+ _GLOBAL_ENGINE: Optional[FailoverEngine] = None
224
+
225
+
226
+ def get_failover_engine() -> FailoverEngine:
227
+ global _GLOBAL_ENGINE
228
+ if _GLOBAL_ENGINE is None:
229
+ _GLOBAL_ENGINE = FailoverEngine()
230
+ return _GLOBAL_ENGINE