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,287 @@
1
+ """
2
+ calc_terminal/models/validator.py
3
+ =================================
4
+ 8-Stage Model Validation Pipeline for CAT's Dynamic Model Registry.
5
+
6
+ Pipeline:
7
+ 1. IDENTITY CHECK — Clean identifier, no control chars or invalid formats.
8
+ 2. PROVIDER CHECK — Provider exists and is recognized in CAT ecosystem.
9
+ 3. MODEL-ID CHECK — Matches provider naming conventions; canonical deduplication.
10
+ 4. ENDPOINT CHECK — Verifies valid, safe API URL structure.
11
+ 5. AUTH CHECK — Determines authentication requirements safely (no key logging).
12
+ 6. CAPABILITY CHECK— Discovers and assigns verified capabilities.
13
+ 7. AVAILABILITY CHECK — Confirms deployment availability state.
14
+ 8. REGISTRATION GATE — Produces clean ModelInfo or marks DISCOVERED_BUT_UNAVAILABLE.
15
+ """
16
+
17
+ import re
18
+ import urllib.parse
19
+ from dataclasses import dataclass
20
+ from typing import Optional, List, Tuple, Dict, Any
21
+
22
+ from .schema import (
23
+ ModelInfo,
24
+ STATUS_ACTIVE,
25
+ STATUS_PREVIEW,
26
+ STATUS_BETA,
27
+ STATUS_DEPRECATED,
28
+ STATUS_UNAVAILABLE,
29
+ STATUS_AUTH_REQUIRED,
30
+ STATUS_DISCOVERED_UNAVAILABLE,
31
+ AVAILABILITY_PAID_API,
32
+ AVAILABILITY_FREE_API,
33
+ AVAILABILITY_LOCAL,
34
+ )
35
+
36
+
37
+ @dataclass
38
+ class ValidationResult:
39
+ is_valid: bool
40
+ status: str
41
+ rejection_reason: Optional[str] = None
42
+ model_info: Optional[ModelInfo] = None
43
+ warnings: List[str] = None
44
+
45
+ def __post_init__(self):
46
+ if self.warnings is None:
47
+ self.warnings = []
48
+
49
+
50
+ class ModelValidator:
51
+ """Validates discovered models before they enter CAT's active registry."""
52
+
53
+ # Disallowed patterns (malformed IDs, binary garbage, HTML injections)
54
+ INVALID_ID_CHARS = re.compile(r"[\x00-\x1f\x7f<>\"'`;|&$]")
55
+
56
+ @classmethod
57
+ def validate_raw(
58
+ cls,
59
+ raw_model: Any,
60
+ provider_id: str,
61
+ provider_config: Optional[Dict[str, Any]] = None,
62
+ ) -> ValidationResult:
63
+ """Run the full 8-stage validation pipeline on a model candidate."""
64
+ config = provider_config or {}
65
+ warnings = []
66
+
67
+ # ── Stage 1: IDENTITY CHECK ──────────────────────────────────────────
68
+ if not raw_model:
69
+ return ValidationResult(
70
+ is_valid=False,
71
+ status=STATUS_DISCOVERED_UNAVAILABLE,
72
+ rejection_reason="Empty or null model object.",
73
+ )
74
+
75
+ if isinstance(raw_model, str):
76
+ model_id = raw_model.strip()
77
+ raw_dict = {"model_id": model_id}
78
+ elif isinstance(raw_model, dict):
79
+ raw_dict = dict(raw_model)
80
+ model_id = str(raw_dict.get("id") or raw_dict.get("model_id") or "").strip()
81
+ elif hasattr(raw_model, "to_dict"):
82
+ raw_dict = raw_model.to_dict()
83
+ model_id = str(raw_dict.get("model_id") or raw_dict.get("id") or "").strip()
84
+ else:
85
+ return ValidationResult(
86
+ is_valid=False,
87
+ status=STATUS_DISCOVERED_UNAVAILABLE,
88
+ rejection_reason=f"Unsupported raw model type: {type(raw_model).__name__}",
89
+ )
90
+
91
+ if not model_id:
92
+ return ValidationResult(
93
+ is_valid=False,
94
+ status=STATUS_DISCOVERED_UNAVAILABLE,
95
+ rejection_reason="Model has empty identifier.",
96
+ )
97
+
98
+ if model_id in ("*", "default", "none"):
99
+ return ValidationResult(
100
+ is_valid=False,
101
+ status=STATUS_DISCOVERED_UNAVAILABLE,
102
+ rejection_reason=f"Model ID is an invalid placeholder or wildcard: '{model_id}'",
103
+ )
104
+
105
+ if cls.INVALID_ID_CHARS.search(model_id):
106
+ return ValidationResult(
107
+ is_valid=False,
108
+ status=STATUS_DISCOVERED_UNAVAILABLE,
109
+ rejection_reason=f"Model ID contains prohibited characters: '{model_id}'",
110
+ )
111
+
112
+ # ── Stage 2: PROVIDER CHECK ──────────────────────────────────────────
113
+ pid = str(provider_id or raw_dict.get("provider") or "").strip().lower()
114
+ if not pid:
115
+ return ValidationResult(
116
+ is_valid=False,
117
+ status=STATUS_DISCOVERED_UNAVAILABLE,
118
+ rejection_reason="Missing provider identifier.",
119
+ )
120
+
121
+ # ── Stage 3: MODEL-ID CHECK & CANONICAL MAPPING ───────────────────────
122
+ canonical_id = cls._derive_canonical_id(model_id, pid)
123
+
124
+ # ── Stage 4: ENDPOINT CHECK ──────────────────────────────────────────
125
+ endpoint = str(
126
+ raw_dict.get("endpoint")
127
+ or config.get("base_url")
128
+ or config.get("api_url")
129
+ or ""
130
+ ).strip()
131
+ if endpoint:
132
+ try:
133
+ parsed = urllib.parse.urlparse(endpoint)
134
+ if parsed.scheme not in ("http", "https"):
135
+ return ValidationResult(
136
+ is_valid=False,
137
+ status=STATUS_DISCOVERED_UNAVAILABLE,
138
+ rejection_reason=f"Insecure endpoint scheme '{parsed.scheme}': must be http or https",
139
+ )
140
+ except Exception as e:
141
+ return ValidationResult(
142
+ is_valid=False,
143
+ status=STATUS_DISCOVERED_UNAVAILABLE,
144
+ rejection_reason=f"Malformed endpoint URL: {e}",
145
+ )
146
+
147
+ # ── Stage 5: AUTH CHECK ──────────────────────────────────────────────
148
+ needs_auth = config.get("needs_key", True)
149
+ if pid == "ollama":
150
+ needs_auth = False
151
+ api_key = config.get("api_key")
152
+
153
+ model_status = STATUS_ACTIVE
154
+ if needs_auth and not api_key:
155
+ # Model is known and documented, but user has not configured auth yet
156
+ model_status = STATUS_AUTH_REQUIRED
157
+
158
+ # ── Stage 6: CAPABILITY CHECK ────────────────────────────────────────
159
+ capabilities = cls._infer_capabilities(model_id, pid, raw_dict)
160
+ context_window = int(raw_dict.get("context_window") or raw_dict.get("context_length") or 128000)
161
+ max_output = int(raw_dict.get("max_output_tokens") or 4096)
162
+ reasoning_levels = raw_dict.get("reasoning_levels") or []
163
+
164
+ # Special recognition for 2026 flagship models
165
+ if pid == "openai" and "gpt-6-astra" in model_id.lower():
166
+ if not reasoning_levels:
167
+ reasoning_levels = ["low", "medium", "high", "xhigh", "max"]
168
+ context_window = max(context_window, 1050000)
169
+ max_output = max(max_output, 128000)
170
+ for cap in ("reasoning", "coding", "vision", "computer_use", "research", "document_workflows"):
171
+ if cap not in capabilities:
172
+ capabilities.append(cap)
173
+
174
+ # ── Stage 7: AVAILABILITY CHECK ──────────────────────────────────────
175
+ availability = raw_dict.get("availability") or (
176
+ AVAILABILITY_LOCAL if pid == "ollama" else AVAILABILITY_PAID_API
177
+ )
178
+ if raw_dict.get("free") or raw_dict.get("is_free"):
179
+ availability = AVAILABILITY_FREE_API
180
+
181
+ if raw_dict.get("deprecated") or "deprecated" in model_id.lower():
182
+ model_status = STATUS_DEPRECATED
183
+ elif raw_dict.get("preview") or "preview" in model_id.lower() or "beta" in model_id.lower():
184
+ model_status = STATUS_PREVIEW
185
+
186
+ # ── Stage 8: REGISTRATION GATE ───────────────────────────────────────
187
+ display_name = raw_dict.get("display_name") or cls._clean_display_name(model_id)
188
+
189
+ model_info = ModelInfo(
190
+ provider=pid,
191
+ model_id=model_id,
192
+ display_name=display_name,
193
+ canonical_model_id=canonical_id,
194
+ family=raw_dict.get("family", ""),
195
+ version=raw_dict.get("version", ""),
196
+ release_date=raw_dict.get("release_date"),
197
+ status=model_status,
198
+ availability=availability,
199
+ modalities=raw_dict.get("modalities") or ["text"],
200
+ capabilities=capabilities,
201
+ context_window=context_window,
202
+ max_output_tokens=max_output,
203
+ reasoning_levels=reasoning_levels,
204
+ pricing=raw_dict.get("pricing") or {"input_price_per_1m": 0.0, "output_price_per_1m": 0.0},
205
+ endpoint=endpoint,
206
+ authentication_required=needs_auth,
207
+ local=pid == "ollama",
208
+ cloud=pid != "ollama",
209
+ verified=bool(raw_dict.get("verified", False)),
210
+ source=raw_dict.get("source", "official_api"),
211
+ tags=raw_dict.get("tags") or [],
212
+ )
213
+
214
+ return ValidationResult(
215
+ is_valid=True,
216
+ status=model_status,
217
+ model_info=model_info,
218
+ warnings=warnings,
219
+ )
220
+
221
+ @staticmethod
222
+ def _derive_canonical_id(model_id: str, provider_id: str) -> str:
223
+ """Normalize model identifier across multi-endpoint routers."""
224
+ clean = model_id.strip()
225
+ # Remove routing prefix (e.g. 'openai/gpt-4o-mini' -> 'gpt-4o-mini')
226
+ if "/" in clean and not clean.startswith("http"):
227
+ parts = clean.split("/")
228
+ if len(parts) == 2 and parts[0].lower() in (
229
+ "openai", "anthropic", "google", "meta-llama", "mistralai", "deepseek", "qwen"
230
+ ):
231
+ clean = parts[1]
232
+ return f"{provider_id}:{clean}"
233
+
234
+ @staticmethod
235
+ def _clean_display_name(model_id: str) -> str:
236
+ # Convert raw ids like 'gpt-6-astra' -> 'GPT-6 Astra'
237
+ clean = model_id.split("/")[-1]
238
+ words = clean.replace("-", " ").replace("_", " ").split()
239
+ capitalized = []
240
+ for w in words:
241
+ if w.lower() in ("gpt", "glm", "api", "r1", "v3", "hy3", "mimo"):
242
+ capitalized.append(w.upper())
243
+ else:
244
+ capitalized.append(w.capitalize())
245
+ return " ".join(capitalized)
246
+
247
+ @staticmethod
248
+ def _infer_capabilities(model_id: str, provider_id: str, raw_dict: dict) -> List[str]:
249
+ caps = list(raw_dict.get("capabilities", []))
250
+ if not caps:
251
+ caps = ["chat", "streaming"]
252
+
253
+ # Normalize capability synonyms in input list
254
+ if any(c in ("cot", "chain_of_thought", "reasoning") for c in caps):
255
+ if "reasoning" not in caps:
256
+ caps.append("reasoning")
257
+ if any(c in ("code", "coder", "coding") for c in caps):
258
+ if "coding" not in caps:
259
+ caps.append("coding")
260
+
261
+ mid = model_id.lower()
262
+ if any(k in mid for k in ("reasoner", "r1", "o1", "o3", "k3", "qwq", "thinking")):
263
+ if "reasoning" not in caps:
264
+ caps.append("reasoning")
265
+ if any(k in mid for k in ("coder", "codestral", "astra", "code", "dev")):
266
+ if "coding" not in caps:
267
+ caps.append("coding")
268
+ if any(k in mid for k in ("vision", "4o", "4v", "pixtral", "vl", "gemini")):
269
+ if "vision" not in caps:
270
+ caps.append("vision")
271
+ if any(k in mid for k in ("mini", "flash", "turbo", "instant", "haiku", "8b")):
272
+ if "fast" not in caps:
273
+ caps.append("fast")
274
+ if "tools" in raw_dict or "function" in raw_dict:
275
+ if "tools" not in caps:
276
+ caps.append("tools")
277
+ return list(set(caps))
278
+
279
+ def validate(self, model: ModelInfo) -> Tuple[bool, str]:
280
+ """Validate a ModelInfo instance directly."""
281
+ res = self.validate_raw(model, model.provider)
282
+ if not res.is_valid:
283
+ return False, res.rejection_reason or "Validation failed"
284
+ # Check if capabilities were normalized
285
+ if res.model_info:
286
+ model.capabilities = res.model_info.capabilities
287
+ return True, "Valid"
@@ -0,0 +1,368 @@
1
+ """
2
+ CAT CLI — ModelVerificationEngine: Automated Verification for Unknown & Custom Models.
3
+
4
+ Verifies:
5
+ 1. Endpoint reachability
6
+ 2. Protocol & Auth handshake
7
+ 3. Model ID validity
8
+ 4. Live request/response generation
9
+ 5. Capability & metadata coherence
10
+
11
+ Statuses:
12
+ ✓ VERIFIED
13
+ ⚠ PARTIALLY_VERIFIED
14
+ ? UNKNOWN
15
+ ✗ FAILED
16
+ """
17
+
18
+ import time
19
+ import json
20
+ import urllib.parse
21
+ from dataclasses import dataclass, field
22
+ from typing import Dict, List, Optional, Callable, Tuple, Any, Union
23
+
24
+
25
+ class VerificationStatus:
26
+ VERIFIED = "VERIFIED"
27
+ PARTIALLY_VERIFIED = "PARTIALLY_VERIFIED"
28
+ UNKNOWN = "UNKNOWN"
29
+ FAILED = "FAILED"
30
+
31
+
32
+ def mask_secret(secret: str) -> str:
33
+ """Safely mask API key/token for display or logging."""
34
+ if not secret:
35
+ return ""
36
+ secret = str(secret).strip()
37
+ if len(secret) <= 8:
38
+ return "********"
39
+ return secret[:3] + "..." + secret[-4:]
40
+
41
+
42
+ @dataclass
43
+ class VerificationStep:
44
+ name: str
45
+ status: str # "pending", "running", "success", "warning", "failed"
46
+ message: str = ""
47
+ elapsed_ms: float = 0.0
48
+
49
+
50
+ @dataclass
51
+ class VerificationResult:
52
+ status: str = VerificationStatus.UNKNOWN
53
+ success: bool = False
54
+ message: str = ""
55
+ error: str = ""
56
+ steps: List[VerificationStep] = field(default_factory=list)
57
+ model_metadata: Dict = field(default_factory=dict)
58
+ models_found: List[str] = field(default_factory=list)
59
+ timestamp: float = field(default_factory=time.time)
60
+
61
+ def get(self, key: str, default: Any = None) -> Any:
62
+ return getattr(self, key, default)
63
+
64
+ def __getitem__(self, key: str) -> Any:
65
+ if hasattr(self, key):
66
+ return getattr(self, key)
67
+ raise KeyError(key)
68
+
69
+ def to_dict(self) -> Dict:
70
+ return {
71
+ "status": self.status,
72
+ "success": self.success,
73
+ "message": self.message,
74
+ "error": self.error,
75
+ "steps": [
76
+ {"name": s.name, "status": s.status, "message": s.message, "elapsed_ms": s.elapsed_ms}
77
+ for s in self.steps
78
+ ],
79
+ "model_metadata": self.model_metadata,
80
+ "models_found": self.models_found,
81
+ "timestamp": self.timestamp,
82
+ }
83
+
84
+
85
+ class ModelVerificationEngine:
86
+ """Executes safe, non-destructive verification against AI model endpoints."""
87
+
88
+ def __init__(self, timeout: float = 8.0):
89
+ self.timeout = timeout
90
+
91
+ def verify_custom_model(
92
+ self,
93
+ provider_id: Any = "",
94
+ model_id: str = "",
95
+ endpoint: str = "",
96
+ protocol: str = "openai",
97
+ api_key: str = "",
98
+ context_window: int = 32000,
99
+ capabilities: Optional[Dict] = None,
100
+ on_step_update: Optional[Callable[[VerificationStep], None]] = None,
101
+ ) -> VerificationResult:
102
+ """Run complete 5-stage verification workflow.
103
+
104
+ Calls on_step_update(step) as each phase progresses.
105
+ """
106
+ if isinstance(provider_id, dict):
107
+ cfg = provider_id
108
+ model_id = cfg.get("model_id") or cfg.get("model") or model_id or "default"
109
+ endpoint = cfg.get("base_url") or cfg.get("endpoint") or cfg.get("url") or endpoint
110
+ protocol = cfg.get("api_style") or cfg.get("protocol") or protocol or "openai"
111
+ api_key = cfg.get("api_key") or api_key
112
+ context_window = cfg.get("context_window", context_window)
113
+ capabilities = cfg.get("capabilities", capabilities)
114
+ provider_id = cfg.get("provider_id") or cfg.get("provider") or "custom"
115
+
116
+ result = VerificationResult()
117
+ steps: List[VerificationStep] = [
118
+ VerificationStep("Input Validation", "pending"),
119
+ VerificationStep("Endpoint Reachability", "pending"),
120
+ VerificationStep("Authentication & Protocol Handshake", "pending"),
121
+ VerificationStep("Model Existence & Response Test", "pending"),
122
+ VerificationStep("Metadata Coherence", "pending"),
123
+ ]
124
+ result.steps = steps
125
+
126
+ def update_step(idx: int, status: str, msg: str = "", t0: Optional[float] = None):
127
+ elapsed = (time.perf_counter() - t0) * 1000.0 if t0 else 0.0
128
+ steps[idx].status = status
129
+ steps[idx].message = msg
130
+ if t0:
131
+ steps[idx].elapsed_ms = round(elapsed, 1)
132
+ if on_step_update:
133
+ try:
134
+ on_step_update(steps[idx])
135
+ except Exception:
136
+ pass
137
+
138
+ # ---------------- Stage 1: Input Validation ----------------
139
+ t0 = time.perf_counter()
140
+ update_step(0, "running", "Validating model parameters...", t0)
141
+ provider_id = (provider_id or "").strip().lower()
142
+ model_id = (model_id or "").strip()
143
+ endpoint = (endpoint or "").strip()
144
+ protocol = (protocol or "openai").strip().lower()
145
+
146
+ if provider_id == "ollama":
147
+ if not endpoint:
148
+ endpoint = "http://localhost:11434"
149
+ if protocol in ("openai", "custom"):
150
+ protocol = "ollama"
151
+
152
+ if not model_id:
153
+ update_step(0, "failed", "Model ID cannot be empty", t0)
154
+ result.status = VerificationStatus.FAILED
155
+ result.error = "Model ID cannot be empty"
156
+ result.message = "Validation failed: model ID is required"
157
+ return result
158
+
159
+ if protocol not in ("openai", "anthropic", "gemini", "ollama", "custom"):
160
+ update_step(0, "failed", f"Unsupported protocol: {protocol}", t0)
161
+ result.status = VerificationStatus.FAILED
162
+ result.error = f"Unsupported protocol: {protocol}"
163
+ result.message = "Validation failed: invalid protocol"
164
+ return result
165
+
166
+ if endpoint:
167
+ parsed = urllib.parse.urlparse(endpoint)
168
+ if parsed.scheme not in ("http", "https"):
169
+ update_step(0, "failed", f"Invalid URL scheme '{parsed.scheme}'; expected http or https", t0)
170
+ result.status = VerificationStatus.FAILED
171
+ result.error = f"Invalid URL scheme '{parsed.scheme}'. Endpoint must begin with http:// or https://"
172
+ result.message = "Validation failed: invalid endpoint URL"
173
+ return result
174
+
175
+ update_step(0, "success", f"Validated: {provider_id}/{model_id} ({protocol})", t0)
176
+
177
+ # ---------------- Stage 2: Endpoint Reachability ----------------
178
+ t0 = time.perf_counter()
179
+ update_step(1, "running", "Checking endpoint reachability...", t0)
180
+
181
+ try:
182
+ import requests
183
+ HAS_REQUESTS = True
184
+ except ImportError:
185
+ HAS_REQUESTS = False
186
+
187
+ if not HAS_REQUESTS:
188
+ update_step(1, "warning", "requests library not installed; skipping network probe", t0)
189
+ else:
190
+ reach_url = endpoint or "http://localhost:11434"
191
+ try:
192
+ # Lightweight probe with short timeout
193
+ resp = requests.get(reach_url, timeout=self.timeout)
194
+ update_step(1, "success", f"Endpoint reachable (HTTP {resp.status_code})", t0)
195
+ except requests.exceptions.SSLError as e:
196
+ update_step(1, "failed", f"SSL Certificate Error: {e}", t0)
197
+ result.status = VerificationStatus.FAILED
198
+ result.error = "SSL validation failed. Check endpoint certificate."
199
+ return result
200
+ except requests.exceptions.ConnectionError:
201
+ # Try base host
202
+ parsed = urllib.parse.urlparse(reach_url)
203
+ base = f"{parsed.scheme}://{parsed.netloc}"
204
+ try:
205
+ resp = requests.get(base, timeout=4.0)
206
+ update_step(1, "success", f"Host reachable (HTTP {resp.status_code})", t0)
207
+ except Exception as ex:
208
+ update_step(1, "failed", f"Could not connect to {base}: {ex}", t0)
209
+ result.status = VerificationStatus.FAILED
210
+ result.error = f"Connection refused or unreachable: {base}"
211
+ return result
212
+ except Exception as e:
213
+ update_step(1, "warning", f"Probe warning: {e}", t0)
214
+
215
+ # ---------------- Stage 3: Auth & Protocol Handshake ----------------
216
+ t0 = time.perf_counter()
217
+ update_step(2, "running", "Performing authentication handshake...", t0)
218
+
219
+ config = {
220
+ "provider": provider_id or "custom",
221
+ "model": model_id,
222
+ "api_key": api_key,
223
+ "base_url": endpoint,
224
+ "api_url": endpoint,
225
+ "api_style": protocol if protocol != "custom" else "openai",
226
+ }
227
+
228
+ try:
229
+ from .. import aicore
230
+ ok, msg, models = aicore.verify_connection(config)
231
+ result.models_found = list(models or [])
232
+ except Exception as ex:
233
+ ok, msg, models = False, str(ex), []
234
+
235
+ if ok:
236
+ update_step(2, "success", f"Auth & Protocol verified ({msg})", t0)
237
+ elif "authentication" in msg.lower() or "401" in msg or "invalid api key" in msg.lower():
238
+ update_step(2, "failed", f"Authentication failed: {msg}", t0)
239
+ result.status = VerificationStatus.FAILED
240
+ result.error = msg
241
+ result.message = "Authentication rejected by provider"
242
+ return result
243
+ else:
244
+ # Maybe model endpoint doesn't support /models list endpoint, but accepts chat completions
245
+ update_step(2, "warning", f"Handshake returned: {msg}", t0)
246
+
247
+ # ---------------- Stage 4: Live Generation Response Test ----------------
248
+ t0 = time.perf_counter()
249
+ update_step(3, "running", f"Testing generation query on '{model_id}'...", t0)
250
+
251
+ test_prompt = "Say hello in one word."
252
+ gen_ok = False
253
+ gen_text = ""
254
+
255
+ try:
256
+ from .. import aicore
257
+ # Test direct query
258
+ reply = aicore.query_ai(test_prompt, config=config)
259
+ if aicore.is_error_response(reply):
260
+ gen_ok = False
261
+ gen_text = reply
262
+ else:
263
+ gen_ok = True
264
+ gen_text = (reply or "").strip()[:50]
265
+ except Exception as ex:
266
+ gen_ok = False
267
+ gen_text = str(ex)
268
+
269
+ if gen_ok:
270
+ update_step(3, "success", f"Model responded: '{gen_text}'", t0)
271
+ else:
272
+ if ok:
273
+ # Handshake worked but prompt failed
274
+ update_step(3, "warning", f"Model prompt returned warning: {gen_text}", t0)
275
+ else:
276
+ update_step(3, "failed", f"Test request failed: {gen_text}", t0)
277
+ result.status = VerificationStatus.FAILED
278
+ result.error = gen_text
279
+ result.message = f"Model '{model_id}' did not respond successfully"
280
+ return result
281
+
282
+ # ---------------- Stage 5: Metadata Coherence ----------------
283
+ t0 = time.perf_counter()
284
+ update_step(4, "running", "Validating metadata & capability coherence...", t0)
285
+
286
+ # Normalize capabilities
287
+ inferred_caps = capabilities or {}
288
+ if not inferred_caps:
289
+ try:
290
+ from ..model_router import capabilities_for
291
+ c_obj = capabilities_for(provider_id, model_id)
292
+ inferred_caps = c_obj.to_dict() if hasattr(c_obj, "to_dict") else dict(c_obj.__dict__)
293
+ except Exception:
294
+ inferred_caps = {"text": True, "streaming": True}
295
+
296
+ result.model_metadata = {
297
+ "model_id": model_id,
298
+ "provider_id": provider_id,
299
+ "endpoint": endpoint,
300
+ "protocol": protocol,
301
+ "context_window": max(512, int(context_window or 32000)),
302
+ "capabilities": inferred_caps,
303
+ "verified_at": time.time(),
304
+ }
305
+
306
+ update_step(4, "success", "Metadata coherent and verified", t0)
307
+
308
+ # Final status determination
309
+ if gen_ok and ok:
310
+ result.status = VerificationStatus.VERIFIED
311
+ result.success = True
312
+ result.message = f"Model '{model_id}' successfully verified!"
313
+ elif gen_ok or ok:
314
+ result.status = VerificationStatus.PARTIALLY_VERIFIED
315
+ result.success = True
316
+ result.message = f"Model '{model_id}' partially verified (connection verified)"
317
+ else:
318
+ result.status = VerificationStatus.FAILED
319
+ result.success = False
320
+ result.message = f"Verification failed: {result.error}"
321
+
322
+ return result
323
+
324
+
325
+ def verify_model(
326
+ provider_id: str,
327
+ model_id: str,
328
+ endpoint: str = "",
329
+ protocol: str = "openai",
330
+ api_key: str = "",
331
+ context_window: int = 32000,
332
+ capabilities: Optional[Dict] = None,
333
+ on_step_update: Optional[Callable[[VerificationStep], None]] = None,
334
+ ) -> VerificationResult:
335
+ """Module-level helper to execute ModelVerificationEngine."""
336
+ engine = get_verification_engine()
337
+ return engine.verify_custom_model(
338
+ provider_id=provider_id,
339
+ model_id=model_id,
340
+ endpoint=endpoint,
341
+ protocol=protocol,
342
+ api_key=api_key,
343
+ context_window=context_window,
344
+ capabilities=capabilities,
345
+ on_step_update=on_step_update,
346
+ )
347
+
348
+
349
+ _DEFAULT_ENGINE: Optional[ModelVerificationEngine] = None
350
+
351
+
352
+ def get_verification_engine(timeout: float = 8.0) -> ModelVerificationEngine:
353
+ """Return singleton instance of ModelVerificationEngine."""
354
+ global _DEFAULT_ENGINE
355
+ if _DEFAULT_ENGINE is None:
356
+ _DEFAULT_ENGINE = ModelVerificationEngine(timeout=timeout)
357
+ return _DEFAULT_ENGINE
358
+
359
+
360
+ __all__ = [
361
+ "ModelVerificationEngine",
362
+ "VerificationStatus",
363
+ "VerificationStep",
364
+ "VerificationResult",
365
+ "get_verification_engine",
366
+ "verify_model",
367
+ "mask_secret",
368
+ ]