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,584 @@
1
+ """
2
+ calc_terminal/models/dynamic_registry.py
3
+ ========================================
4
+ Central Dynamic Model Registry for CAT (Version: 2026.09).
5
+
6
+ Maintains a living, persistent, continuously updated catalog of AI models and
7
+ providers. Automatically discovers newly available models from official APIs
8
+ and registers them without requiring CAT application updates.
9
+
10
+ Features:
11
+ - Atomic persistence to disk (~/.cct/registry/ and cache/models/).
12
+ - Thread-safe querying, updating, and reconciliation.
13
+ - Multi-category categorization and intelligent task-based ranking.
14
+ - Deprecation tracking and stale model protection against temporary network errors.
15
+ - Full backward compatibility with legacy ModelInfo lookups.
16
+ """
17
+
18
+ import glob
19
+ import json
20
+ import logging
21
+ import os
22
+ import re
23
+ import threading
24
+ import time
25
+ from datetime import datetime, timezone
26
+ from typing import Dict, List, Optional, Tuple, Any
27
+
28
+ from .schema import (
29
+ ModelInfo,
30
+ STATUS_ACTIVE,
31
+ STATUS_PREVIEW,
32
+ STATUS_BETA,
33
+ STATUS_DEPRECATED,
34
+ STATUS_RETIRED,
35
+ STATUS_UNAVAILABLE,
36
+ STATUS_AUTH_REQUIRED,
37
+ STATUS_DISCOVERED_UNAVAILABLE,
38
+ CATEGORY_GENERAL,
39
+ CATEGORY_REASONING,
40
+ CATEGORY_CODING,
41
+ CATEGORY_FAST,
42
+ CATEGORY_CHEAP,
43
+ CATEGORY_LONG_CONTEXT,
44
+ CATEGORY_VISION,
45
+ CATEGORY_AUDIO,
46
+ CATEGORY_MULTIMODAL,
47
+ CATEGORY_AGENT,
48
+ CATEGORY_RESEARCH,
49
+ CATEGORY_LOCAL,
50
+ CATEGORY_OPEN_WEIGHT,
51
+ CATEGORY_EMBEDDING,
52
+ CATEGORY_SPECIALIZED,
53
+ AVAILABILITY_LOCAL,
54
+ AVAILABILITY_FREE_API,
55
+ AVAILABILITY_PAID_API,
56
+ AVAILABILITY_OPEN_WEIGHT,
57
+ )
58
+ from .validator import ModelValidator
59
+
60
+ _LOG = logging.getLogger("cct.dynamic_registry")
61
+
62
+
63
+ def _resolve_registry_dir() -> str:
64
+ """Find the best writable persistent registry directory."""
65
+ local_app = os.environ.get("LOCALAPPDATA")
66
+ if local_app:
67
+ base = os.path.join(local_app, "CCT", "registry")
68
+ else:
69
+ base = os.path.expanduser("~/.cct/registry")
70
+ try:
71
+ os.makedirs(base, exist_ok=True)
72
+ except Exception:
73
+ pass
74
+ return base
75
+
76
+
77
+ class DynamicModelRegistry:
78
+ """Central dynamic repository of AI models and provider capabilities."""
79
+
80
+ _instance: Optional["DynamicModelRegistry"] = None
81
+ _lock = threading.Lock()
82
+
83
+ @classmethod
84
+ def reset_instance(cls):
85
+ with cls._lock:
86
+ cls._instance = None
87
+
88
+ def __new__(cls, *args, **kwargs):
89
+ with cls._lock:
90
+ if cls._instance is None:
91
+ cls._instance = super(DynamicModelRegistry, cls).__new__(cls)
92
+ cls._instance._initialized = False
93
+ return cls._instance
94
+
95
+ def __init__(self, registry_dir: Optional[str] = None):
96
+ if getattr(self, "_initialized", False):
97
+ return
98
+ self._dir = registry_dir or _resolve_registry_dir()
99
+ self._models_file = os.path.join(self._dir, "models.json")
100
+ self._providers_file = os.path.join(self._dir, "providers.json")
101
+ self._discovery_state_file = os.path.join(self._dir, "discovery_state.json")
102
+ self._version_file = os.path.join(self._dir, "registry_version.json")
103
+
104
+ self._rw_lock = threading.RLock()
105
+ self._models: Dict[str, ModelInfo] = {} # key: canonical_model_id (provider:model_id)
106
+ self._providers: Dict[str, Dict[str, Any]] = {}
107
+ self._discovery_state: Dict[str, Any] = {}
108
+ self._new_models_queue: List[str] = []
109
+
110
+ self._load_initial_registry()
111
+ self._initialized = True
112
+
113
+ # ── Persistence & Migration ────────────────────────────────────────────────
114
+
115
+ def _load_initial_registry(self):
116
+ """Load from persistent disk, or bootstrap from bundled files."""
117
+ with self._rw_lock:
118
+ loaded = False
119
+ if os.path.isfile(self._models_file):
120
+ try:
121
+ with open(self._models_file, "r", encoding="utf-8") as f:
122
+ data = json.load(f)
123
+ for item in data.get("models", []):
124
+ info = ModelInfo.from_dict(item)
125
+ self._models[info.canonical_model_id] = info
126
+ loaded = True
127
+ except Exception as e:
128
+ _LOG.warning(f"Failed to load cached models.json: {e}")
129
+
130
+ # Bootstrap default catalog from model_metadata.json & providers.json if needed
131
+ self._bootstrap_bundled_catalog()
132
+
133
+ # Ensure flagship 2026 models like GPT-6 Astra are always registered
134
+ self._ensure_flagship_2026_models()
135
+
136
+ def _bootstrap_bundled_catalog(self):
137
+ """Ingest existing models from model_metadata.json without overwriting user data."""
138
+ pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
139
+ metadata_file = os.path.join(pkg_dir, "models", "model_metadata.json")
140
+ if os.path.isfile(metadata_file):
141
+ try:
142
+ with open(metadata_file, "r", encoding="utf-8") as f:
143
+ data = json.load(f)
144
+ for item in data.get("models", []):
145
+ res = ModelValidator.validate_raw(item, item.get("provider", "openai"))
146
+ if res.is_valid and res.model_info:
147
+ cid = res.model_info.canonical_model_id
148
+ if cid not in self._models:
149
+ self._models[cid] = res.model_info
150
+ except Exception as e:
151
+ _LOG.warning(f"Failed to bootstrap from model_metadata.json: {e}")
152
+
153
+ # Ingest providers from providers.json
154
+ providers_file = os.path.join(pkg_dir, "providers", "providers.json")
155
+ if os.path.isfile(providers_file):
156
+ try:
157
+ with open(providers_file, "r", encoding="utf-8") as f:
158
+ data = json.load(f)
159
+ for p in data.get("providers", []):
160
+ pid = p.get("id")
161
+ if pid and pid not in self._providers:
162
+ self._providers[pid] = dict(p)
163
+ # Register fallback models
164
+ for fb_model in p.get("fallback_models", []):
165
+ res = ModelValidator.validate_raw(fb_model, pid)
166
+ if res.is_valid and res.model_info:
167
+ cid = res.model_info.canonical_model_id
168
+ if cid not in self._models:
169
+ res.model_info.source = "fallback"
170
+ self._models[cid] = res.model_info
171
+ except Exception as e:
172
+ _LOG.warning(f"Failed to bootstrap from providers.json: {e}")
173
+
174
+ def _ensure_flagship_2026_models(self):
175
+ """Guarantee official 2026 flagship models (e.g. GPT-6 Astra) are seeded."""
176
+ # 1. OpenAI GPT-6 Astra
177
+ astra = ModelInfo(
178
+ provider="openai",
179
+ model_id="gpt-6-astra",
180
+ display_name="GPT-6 Astra",
181
+ family="GPT",
182
+ version="6",
183
+ canonical_model_id="openai:gpt-6-astra",
184
+ release_date="2026-03",
185
+ status=STATUS_ACTIVE,
186
+ availability=AVAILABILITY_PAID_API,
187
+ modalities=["text", "image"],
188
+ capabilities=[
189
+ "reasoning", "coding", "vision", "computer_use", "research",
190
+ "document_workflows", "tools", "streaming", "long_context"
191
+ ],
192
+ context_window=1050000,
193
+ max_output_tokens=128000,
194
+ reasoning_levels=["low", "medium", "high", "xhigh", "max"],
195
+ pricing={"input_price_per_1m": 5.0, "output_price_per_1m": 20.0},
196
+ endpoint="https://api.openai.com/v1",
197
+ authentication_required=True,
198
+ local=False,
199
+ cloud=True,
200
+ verified=True,
201
+ source="official_api",
202
+ tags=["flagship", "2026", "reasoning", "coding", "computer_use"],
203
+ recommendation_badges=["Best for Coding", "Best for Reasoning", "Best Agent Model"],
204
+ )
205
+ self._models[astra.canonical_model_id] = astra
206
+
207
+ # 2. Claude 4 Fable 5.1 & Sonnet
208
+ claude4 = ModelInfo(
209
+ provider="anthropic",
210
+ model_id="claude-4-fable-5.1",
211
+ display_name="Claude 4 Fable 5.1",
212
+ family="Claude",
213
+ version="4",
214
+ canonical_model_id="anthropic:claude-4-fable-5.1",
215
+ status=STATUS_ACTIVE,
216
+ availability=AVAILABILITY_PAID_API,
217
+ modalities=["text", "image"],
218
+ capabilities=["reasoning", "coding", "vision", "tools", "streaming", "long_context"],
219
+ context_window=500000,
220
+ max_output_tokens=64000,
221
+ reasoning_levels=["standard", "extended"],
222
+ pricing={"input_price_per_1m": 4.0, "output_price_per_1m": 16.0},
223
+ endpoint="https://api.anthropic.com/v1",
224
+ authentication_required=True,
225
+ verified=True,
226
+ source="official_api",
227
+ recommendation_badges=["Best for Research"],
228
+ )
229
+ self._models[claude4.canonical_model_id] = claude4
230
+
231
+ claude4_sonnet = ModelInfo(
232
+ provider="anthropic",
233
+ model_id="claude-4-sonnet-20260515",
234
+ display_name="Claude 4 Sonnet",
235
+ family="Claude",
236
+ version="4",
237
+ canonical_model_id="anthropic:claude-4-sonnet-20260515",
238
+ status=STATUS_ACTIVE,
239
+ availability=AVAILABILITY_PAID_API,
240
+ modalities=["text", "image"],
241
+ capabilities=["reasoning", "coding", "vision", "tools", "streaming"],
242
+ context_window=500000,
243
+ max_output_tokens=32768,
244
+ pricing={"input_price_per_1m": 3.0, "output_price_per_1m": 15.0},
245
+ endpoint="https://api.anthropic.com/v1",
246
+ authentication_required=True,
247
+ verified=True,
248
+ source="official_api",
249
+ recommendation_badges=["Best for Coding"],
250
+ )
251
+ self._models[claude4_sonnet.canonical_model_id] = claude4_sonnet
252
+
253
+ # 3. DeepSeek-R1 / DeepSeek-V3
254
+ ds_r1 = ModelInfo(
255
+ provider="deepseek",
256
+ model_id="deepseek-reasoner",
257
+ display_name="DeepSeek-R1 (Reasoner)",
258
+ family="DeepSeek",
259
+ canonical_model_id="deepseek:deepseek-reasoner",
260
+ status=STATUS_ACTIVE,
261
+ availability=AVAILABILITY_OPEN_WEIGHT,
262
+ capabilities=["reasoning", "coding", "tools", "streaming"],
263
+ context_window=128000,
264
+ max_output_tokens=32768,
265
+ reasoning_levels=["high"],
266
+ pricing={"input_price_per_1m": 0.55, "output_price_per_1m": 2.19},
267
+ endpoint="https://api.deepseek.com/v1",
268
+ authentication_required=True,
269
+ verified=True,
270
+ source="official_api",
271
+ tags=["open_weight", "reasoning"],
272
+ recommendation_badges=["Best for Reasoning", "Best Budget Model"],
273
+ )
274
+ self._models[ds_r1.canonical_model_id] = ds_r1
275
+
276
+ # 4. Moonshot Kimi K3
277
+ kimi_k3 = ModelInfo(
278
+ provider="moonshot",
279
+ model_id="kimi-k3",
280
+ display_name="Kimi K3",
281
+ family="Kimi",
282
+ canonical_model_id="moonshot:kimi-k3",
283
+ status=STATUS_ACTIVE,
284
+ availability=AVAILABILITY_PAID_API,
285
+ capabilities=["reasoning", "coding", "long_context", "tools", "streaming"],
286
+ context_window=256000,
287
+ max_output_tokens=16384,
288
+ endpoint="https://api.moonshot.cn/v1",
289
+ authentication_required=True,
290
+ verified=True,
291
+ source="official_api",
292
+ recommendation_badges=["Best for Research"],
293
+ )
294
+ self._models[kimi_k3.canonical_model_id] = kimi_k3
295
+
296
+ def save_to_disk(self):
297
+ """Persist current catalog to disk atomically."""
298
+ with self._rw_lock:
299
+ try:
300
+ os.makedirs(self._dir, exist_ok=True)
301
+ payload = {
302
+ "version": "2026.09",
303
+ "updated_at": datetime.now(timezone.utc).isoformat(),
304
+ "total_models": len(self._models),
305
+ "models": [m.to_dict() for m in self._models.values()],
306
+ }
307
+ tmp_file = self._models_file + ".tmp"
308
+ with open(tmp_file, "w", encoding="utf-8") as f:
309
+ json.dump(payload, f, indent=2)
310
+ os.replace(tmp_file, self._models_file)
311
+ except Exception as e:
312
+ _LOG.error(f"Failed to persist models to disk: {e}")
313
+
314
+ # ── Registry Mutators & Querying ───────────────────────────────────────────
315
+
316
+ def register_model(self, model_info: ModelInfo) -> bool:
317
+ """Register or update a validated ModelInfo."""
318
+ with self._rw_lock:
319
+ cid = model_info.canonical_model_id
320
+ is_new = cid not in self._models
321
+ model_info.last_seen = datetime.now(timezone.utc).isoformat()
322
+ self._models[cid] = model_info
323
+ if is_new:
324
+ self._new_models_queue.append(cid)
325
+ return is_new
326
+
327
+ def get_model(self, model_id: str, provider_id: Optional[str] = None) -> Optional[ModelInfo]:
328
+ """Look up model by ID or canonical ID."""
329
+ with self._rw_lock:
330
+ if provider_id:
331
+ cid = f"{provider_id.lower()}:{model_id}"
332
+ if cid in self._models:
333
+ return self._models[cid]
334
+ # Search by model_id directly
335
+ for m in self._models.values():
336
+ if m.model_id == model_id and (not provider_id or m.provider == provider_id.lower()):
337
+ return m
338
+ # Partial match (e.g. without provider prefix)
339
+ for m in self._models.values():
340
+ if m.model_id.lower() == model_id.lower():
341
+ return m
342
+ return None
343
+
344
+ def list_models(
345
+ self,
346
+ provider: Optional[str] = None,
347
+ category: Optional[str] = None,
348
+ status: Optional[str] = None,
349
+ state: Optional[str] = None,
350
+ include_deprecated: bool = False,
351
+ search: Optional[str] = None,
352
+ ) -> List[ModelInfo]:
353
+ """Filter and return list of models matching criteria."""
354
+ with self._rw_lock:
355
+ results = list(self._models.values())
356
+
357
+ if provider:
358
+ pid = provider.lower().strip()
359
+ results = [m for m in results if m.provider == pid]
360
+
361
+ # State-specific filters
362
+ state_val = (state or status or "").lower().strip()
363
+ if state_val == "new":
364
+ results = [m for m in results if m.canonical_model_id in self._new_models_queue or "new" in m.tags]
365
+ elif state_val == "updated":
366
+ results = [m for m in results if "updated" in m.tags]
367
+ elif state_val in ("deprecated", "retired"):
368
+ include_deprecated = True
369
+ results = [m for m in results if m.status in (STATUS_DEPRECATED, STATUS_RETIRED)]
370
+ elif state_val:
371
+ results = [m for m in results if m.status == state_val]
372
+
373
+ if not include_deprecated and state_val not in ("deprecated", "retired"):
374
+ results = [m for m in results if m.status not in (STATUS_DEPRECATED, STATUS_RETIRED, STATUS_UNAVAILABLE)]
375
+
376
+ if category and category.lower() not in ("all", "*"):
377
+ results = [m for m in results if m.matches_category(category)]
378
+
379
+ if search:
380
+ q = search.lower().strip()
381
+ results = [
382
+ m for m in results
383
+ if q in m.model_id.lower() or q in m.display_name.lower() or q in m.family.lower()
384
+ ]
385
+
386
+ return results
387
+
388
+ def reconcile_models(self, provider_id: str, discovered_models: List[ModelInfo]) -> Dict[str, List[str]]:
389
+ """Compare freshly discovered remote models against the local registry.
390
+ Calculates: added, updated, removed, deprecated, unchanged."""
391
+ with self._rw_lock:
392
+ pid = provider_id.lower().strip()
393
+ existing_for_provider = {
394
+ m.model_id: m for m in self._models.values() if m.provider == pid
395
+ }
396
+ remote_ids = {m.model_id: m for m in discovered_models}
397
+
398
+ added = []
399
+ updated = []
400
+ deprecated = []
401
+ unchanged = []
402
+
403
+ # 1. Process newly found or existing models
404
+ for mid, rem in remote_ids.items():
405
+ if mid not in existing_for_provider:
406
+ self.register_model(rem)
407
+ added.append(mid)
408
+ else:
409
+ cur = existing_for_provider[mid]
410
+ has_changes = False
411
+ # Check if model status transitioned to deprecated
412
+ if rem.status in (STATUS_DEPRECATED, STATUS_RETIRED) and cur.status not in (STATUS_DEPRECATED, STATUS_RETIRED):
413
+ cur.status = rem.status
414
+ deprecated.append(mid)
415
+ has_changes = True
416
+ # Update capabilities or context if expanded
417
+ if set(rem.capabilities) - set(cur.capabilities):
418
+ cur.capabilities = list(set(cur.capabilities) | set(rem.capabilities))
419
+ has_changes = True
420
+ if rem.context_window > cur.context_window:
421
+ cur.context_window = rem.context_window
422
+ has_changes = True
423
+ cur.last_seen = datetime.now(timezone.utc).isoformat()
424
+ cur.stale = False
425
+ if has_changes:
426
+ if mid not in deprecated:
427
+ updated.append(mid)
428
+ else:
429
+ unchanged.append(mid)
430
+
431
+ # 2. Process models missing from remote discovery
432
+ if remote_ids:
433
+ for mid, cur in existing_for_provider.items():
434
+ if mid not in remote_ids:
435
+ cur.stale = True
436
+ if cur.status in (STATUS_DEPRECATED, STATUS_RETIRED):
437
+ unchanged.append(mid)
438
+ else:
439
+ cur.status = STATUS_DEPRECATED
440
+ deprecated.append(mid)
441
+ else:
442
+ # When remote discovery returned 0 models (e.g. offline/network failure),
443
+ # preserve models as stale without marking them deprecated
444
+ for mid, cur in existing_for_provider.items():
445
+ cur.stale = True
446
+ unchanged.append(mid)
447
+
448
+ self.save_to_disk()
449
+ return {
450
+ "added": added,
451
+ "updated": updated,
452
+ "deprecated": deprecated,
453
+ "unchanged": unchanged,
454
+ }
455
+
456
+ # ── Intelligent Ranking & Categorization ──────────────────────────────────
457
+
458
+ def rank_models(
459
+ self,
460
+ models: Optional[List[ModelInfo]] = None,
461
+ task: str = "general",
462
+ ) -> List[Tuple[ModelInfo, float, List[str]]]:
463
+ """Rank models according to task-specific score and assign recommendation badges."""
464
+ with self._rw_lock:
465
+ candidates = list(models if models is not None else self._models.values())
466
+ scored = []
467
+
468
+ for m in candidates:
469
+ score = 50.0 # base score
470
+
471
+ # Context window bonus
472
+ if m.context_window >= 1000000:
473
+ score += 25.0
474
+ elif m.context_window >= 200000:
475
+ score += 15.0
476
+ elif m.context_window >= 128000:
477
+ score += 10.0
478
+
479
+ # Capability bonuses
480
+ if m.has_capability("reasoning"):
481
+ score += 20.0
482
+ if m.has_capability("coding"):
483
+ score += 15.0
484
+ if m.has_capability("vision"):
485
+ score += 10.0
486
+ if m.has_capability("computer_use"):
487
+ score += 15.0
488
+
489
+ # Verification bonus
490
+ if m.verified:
491
+ score += 10.0
492
+
493
+ # Recency / Status bonus
494
+ if m.status == STATUS_ACTIVE:
495
+ score += 5.0
496
+ elif m.status == STATUS_PREVIEW:
497
+ score += 2.0
498
+ elif m.status == STATUS_DEPRECATED:
499
+ score -= 40.0
500
+
501
+ # Dynamic Badges
502
+ badges = list(m.recommendation_badges)
503
+ if not badges:
504
+ if m.has_capability("coding") and ("astra" in m.model_id.lower() or "coder" in m.model_id.lower() or "sonnet" in m.model_id.lower()):
505
+ badges.append("Best for Coding")
506
+ if m.has_capability("reasoning") and ("reasoner" in m.model_id.lower() or "r1" in m.model_id.lower() or "o1" in m.model_id.lower() or "o3" in m.model_id.lower() or "astra" in m.model_id.lower()):
507
+ badges.append("Best for Reasoning")
508
+ if m.local:
509
+ badges.append("Best Local Model")
510
+ if m.matches_category(CATEGORY_FAST):
511
+ badges.append("Best Fast Model")
512
+ if m.availability in (AVAILABILITY_FREE_API, AVAILABILITY_OPEN_WEIGHT) or m.pricing.get("input_price_per_1m", 0.0) < 1.0:
513
+ badges.append("Best Budget Model")
514
+
515
+ scored.append((m, round(score, 1), badges))
516
+
517
+ # Sort descending by score
518
+ scored.sort(key=lambda x: x[1], reverse=True)
519
+ return scored
520
+
521
+ def get_ranked_models(
522
+ self,
523
+ task: str = "general",
524
+ limit: Optional[int] = None,
525
+ models: Optional[List[ModelInfo]] = None,
526
+ ) -> List[ModelInfo]:
527
+ """Return list of models sorted by suitability for a task."""
528
+ scored = self.rank_models(models=models, task=task)
529
+ res = [m for m, score, badges in scored]
530
+ if limit is not None:
531
+ res = res[:limit]
532
+ return res
533
+
534
+ def get_audit_summary(self) -> Dict[str, Any]:
535
+ """Return a complete audit overview of providers and models."""
536
+ with self._rw_lock:
537
+ all_m = list(self._models.values())
538
+ active_m = [m for m in all_m if m.status == STATUS_ACTIVE]
539
+ new_m = [m for m in all_m if m.canonical_model_id in self._new_models_queue]
540
+ dep_m = [m for m in all_m if m.status in (STATUS_DEPRECATED, STATUS_RETIRED)]
541
+ unavail_m = [m for m in all_m if m.status in (STATUS_UNAVAILABLE, STATUS_DISCOVERED_UNAVAILABLE)]
542
+ auth_req_m = [m for m in all_m if m.status == STATUS_AUTH_REQUIRED]
543
+
544
+ # Unique providers represented in registry
545
+ providers_seen = sorted({m.provider for m in all_m})
546
+ by_prov: Dict[str, int] = {}
547
+ for m in all_m:
548
+ by_prov[m.provider] = by_prov.get(m.provider, 0) + 1
549
+
550
+ return {
551
+ "total_models": len(all_m),
552
+ "total_providers": len(providers_seen),
553
+ "providers_discovered": len(providers_seen),
554
+ "providers_active": len([p for p in providers_seen if any(m.provider == p and m.status == STATUS_ACTIVE for m in all_m)]),
555
+ "providers_requiring_auth": len({m.provider for m in auth_req_m}),
556
+ "models_discovered": len(all_m),
557
+ "models_active": len(active_m),
558
+ "new_models": len(new_m),
559
+ "deprecated": len(dep_m),
560
+ "unavailable": len(unavail_m),
561
+ "providers_list": providers_seen,
562
+ "models_by_provider": by_prov,
563
+ "models_by_state": {
564
+ "available": len(active_m),
565
+ "new": len(new_m),
566
+ "deprecated": len(dep_m),
567
+ "unavailable": len(unavail_m),
568
+ },
569
+ }
570
+
571
+ def pop_new_models(self) -> List[ModelInfo]:
572
+ """Return and clear newly detected models since last poll."""
573
+ with self._rw_lock:
574
+ models = [self._models[cid] for cid in self._new_models_queue if cid in self._models]
575
+ self._new_models_queue.clear()
576
+ return models
577
+
578
+
579
+ # Singleton convenience accessors
580
+ def get_registry() -> DynamicModelRegistry:
581
+ return DynamicModelRegistry()
582
+
583
+ def get_dynamic_registry() -> DynamicModelRegistry:
584
+ return DynamicModelRegistry()