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,194 @@
1
+ """
2
+ CAT Model Capabilities & Profile System.
3
+
4
+ Enforces Section 13, 14, 26, 35-37 of the CAT AI Mode Reliability Contract.
5
+ Distinguishes small/base models (e.g. deepseek-coder:1.3b-base-q8_0) from
6
+ large instruction-following models, providing conservative budgets,
7
+ minimal prompting, and specialized generation parameters.
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Dict, Any, Optional
12
+ import re
13
+
14
+
15
+ @dataclass
16
+ class ModelCapabilities:
17
+ chat: bool = True
18
+ instruction_following: bool = True
19
+ tool_calling: bool = True
20
+ vision: bool = False
21
+ reasoning: bool = False
22
+ context_length: int = 4096
23
+ is_small_model: bool = False
24
+ is_base_model: bool = False
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ return {
28
+ "chat": self.chat,
29
+ "instruction_following": self.instruction_following,
30
+ "tool_calling": self.tool_calling,
31
+ "vision": self.vision,
32
+ "reasoning": self.reasoning,
33
+ "context_length": self.context_length,
34
+ "is_small_model": self.is_small_model,
35
+ "is_base_model": self.is_base_model,
36
+ }
37
+
38
+
39
+ @dataclass
40
+ class ModelProfile:
41
+ provider: str
42
+ model: str
43
+ capabilities: ModelCapabilities
44
+ prompt_template: str = "chat" # "chat", "minimal", "completion"
45
+ recommended_temperature: float = 0.7
46
+ max_context_tokens: int = 4096
47
+ reserved_output_tokens: int = 1024
48
+ generation_options: Dict[str, Any] = field(default_factory=dict)
49
+
50
+ def is_small_or_base(self) -> bool:
51
+ return self.capabilities.is_small_model or self.capabilities.is_base_model
52
+
53
+ def supports_tools(self) -> bool:
54
+ return self.capabilities.tool_calling and not self.capabilities.is_base_model
55
+
56
+ def get_effective_options(self, user_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
57
+ """Merge base generation options with model defaults and optional user overrides."""
58
+ opts = dict(self.generation_options)
59
+ if user_options:
60
+ for k, v in user_options.items():
61
+ if v is not None:
62
+ opts[k] = v
63
+ return opts
64
+
65
+
66
+ # Regex patterns identifying small or base models
67
+ _BASE_MODEL_PATTERNS = [
68
+ re.compile(r"[-:_]base($|[-:_])", re.IGNORECASE),
69
+ re.compile(r"base-q\d", re.IGNORECASE),
70
+ ]
71
+
72
+ _SMALL_MODEL_PATTERNS = [
73
+ re.compile(r"[:\-_](0\.\d+|1\.[0-8]+|2|3)b($|[:\-_])", re.IGNORECASE),
74
+ re.compile(r"(tinyllama|smollm|qwen.*0\.5b|qwen.*1\.8b|deepseek.*1\.3b)", re.IGNORECASE),
75
+ ]
76
+
77
+ _VISION_PATTERNS = [
78
+ re.compile(r"(vision|llava|gpt-4o|claude-3|gemini-1\.5|gemini-2\.0)", re.IGNORECASE),
79
+ ]
80
+
81
+ _REASONING_PATTERNS = [
82
+ re.compile(r"(o1|o3|r1|reasoning|deepseek-r1)", re.IGNORECASE),
83
+ ]
84
+
85
+ _TOOL_CAPABLE_PATTERNS = [
86
+ re.compile(r"(gpt-4|claude-3|gemini|llama-?3\.[123]|qwen2\.5.*(7b|14b|32b|72b)|mistral)", re.IGNORECASE),
87
+ ]
88
+
89
+
90
+ def detect_capabilities(provider: str, model_name: str) -> ModelCapabilities:
91
+ """Analyze provider and model string to infer capabilities safely."""
92
+ provider_str = (provider or "").lower().strip()
93
+ model_str = (model_name or "").lower().strip()
94
+
95
+ is_base = any(p.search(model_str) for p in _BASE_MODEL_PATTERNS)
96
+ is_small = any(p.search(model_str) for p in _SMALL_MODEL_PATTERNS) or ("1.3b" in model_str)
97
+
98
+ # Base models do not reliably follow complex multi-turn instruction schemas
99
+ instruction_following = not is_base
100
+ tool_calling = False if (is_base or is_small) else any(p.search(model_str) for p in _TOOL_CAPABLE_PATTERNS)
101
+
102
+ # If provider is OpenAI/Anthropic/Gemini standard models, tool calling is standard
103
+ if provider_str in ("openai", "anthropic", "gemini") and not is_small:
104
+ tool_calling = True
105
+ instruction_following = True
106
+
107
+ vision = any(p.search(model_str) for p in _VISION_PATTERNS)
108
+ reasoning = any(p.search(model_str) for p in _REASONING_PATTERNS)
109
+
110
+ if is_small:
111
+ context_length = 2048
112
+ elif is_base:
113
+ context_length = 2048
114
+ elif "16k" in model_str:
115
+ context_length = 16384
116
+ elif "32k" in model_str:
117
+ context_length = 32768
118
+ elif "128k" in model_str or "gpt-4o" in model_str or "gemini" in model_str:
119
+ context_length = 131072
120
+ else:
121
+ context_length = 4096
122
+
123
+ return ModelCapabilities(
124
+ chat=True,
125
+ instruction_following=instruction_following,
126
+ tool_calling=tool_calling,
127
+ vision=vision,
128
+ reasoning=reasoning,
129
+ context_length=context_length,
130
+ is_small_model=is_small,
131
+ is_base_model=is_base,
132
+ )
133
+
134
+
135
+ def get_model_profile(provider: str, model_name: str, config: Optional[Dict[str, Any]] = None) -> ModelProfile:
136
+ """Build a complete ModelProfile tailored to the given provider and model."""
137
+ caps = detect_capabilities(provider, model_name)
138
+
139
+ if caps.is_small_model or caps.is_base_model:
140
+ # Conservative defaults for small/base local models (e.g. deepseek-coder:1.3b-base-q8_0)
141
+ recommended_temp = 0.3
142
+ max_context = min(caps.context_length, 1536)
143
+ reserved_out = 512
144
+ prompt_template = "minimal" if caps.is_base_model else "chat"
145
+ gen_opts = {
146
+ "temperature": recommended_temp,
147
+ "top_p": 0.9,
148
+ "repeat_penalty": 1.18,
149
+ "num_predict": reserved_out,
150
+ }
151
+ else:
152
+ # Standard instruction/chat model defaults
153
+ recommended_temp = 0.7
154
+ max_context = min(caps.context_length, 8192)
155
+ reserved_out = 1024
156
+ prompt_template = "chat"
157
+ gen_opts = {
158
+ "temperature": recommended_temp,
159
+ "top_p": 0.95,
160
+ }
161
+
162
+ # Override temperature if explicitly provided in config
163
+ if config and config.get("temperature") is not None:
164
+ try:
165
+ gen_opts["temperature"] = float(config["temperature"])
166
+ except (ValueError, TypeError):
167
+ pass
168
+
169
+ # For Ollama models, check if model size exceeds GPU VRAM (e.g. 8B/9.6GB model on 4GB VRAM)
170
+ # Default to num_gpu: 0 (CPU) to prevent llama-server crashing with 0xc0000409 buffer overrun
171
+ if (provider or "").lower() == "ollama":
172
+ if config and config.get("num_gpu") is not None:
173
+ gen_opts["num_gpu"] = int(config["num_gpu"])
174
+ else:
175
+ try:
176
+ from .. import pc_specs
177
+ specs = pc_specs.get_specs()
178
+ vram = specs.get("gpu_vram_gb", 0)
179
+ is_large = any(k in (model_name or "").lower() for k in ("8b", "9b", "14b", "32b", "70b", "e4b", "gemma4", "gemma:7b", "llama3:8b"))
180
+ if vram and vram <= 4.5 and is_large:
181
+ gen_opts["num_gpu"] = 0
182
+ except Exception:
183
+ pass
184
+
185
+ return ModelProfile(
186
+ provider=provider or "ollama",
187
+ model=model_name or "default",
188
+ capabilities=caps,
189
+ prompt_template=prompt_template,
190
+ recommended_temperature=recommended_temp,
191
+ max_context_tokens=max_context,
192
+ reserved_output_tokens=reserved_out,
193
+ generation_options=gen_opts,
194
+ )
@@ -0,0 +1,265 @@
1
+ """Model registry — curated metadata + dynamic discovery merging.
2
+
3
+ Data-driven redesign (v0.7.4+): curated model metadata lives in
4
+ `models/model_metadata.json`, provider metadata + fallback model lists
5
+ live in `providers/providers.json`. No model names are hardcoded in
6
+ Python source — this module only loads and queries the JSON stores.
7
+
8
+ Dynamic model discovery, disk caching, refresh and category filtering
9
+ live in `models/manager.py`; this module keeps the classic lookup API
10
+ used by the picker UIs (get_model_info / get_badges / enrich_models /
11
+ group_by_tier / ...).
12
+ """
13
+
14
+ import json
15
+ import os
16
+ from typing import Optional
17
+
18
+ from . import manager
19
+
20
+
21
+ # ── Schema ─────────────────────────────────────────────────────────────────
22
+
23
+ class ModelInfo:
24
+ __slots__ = (
25
+ "id", "provider", "family",
26
+ "context_window", "capabilities",
27
+ "pricing_tier", "status",
28
+ "input_price_per_1m", "output_price_per_1m",
29
+ )
30
+
31
+ def __init__(self, id="", provider="", family="",
32
+ context_window=0, capabilities=None,
33
+ pricing_tier="unknown", status="stable",
34
+ input_price_per_1m=0.0, output_price_per_1m=0.0):
35
+ self.id = id
36
+ self.provider = provider
37
+ self.family = family
38
+ self.context_window = context_window
39
+ self.capabilities = capabilities or []
40
+ self.pricing_tier = pricing_tier
41
+ self.status = status
42
+ self.input_price_per_1m = input_price_per_1m
43
+ self.output_price_per_1m = output_price_per_1m
44
+
45
+ def to_dict(self):
46
+ return {
47
+ "id": self.id,
48
+ "provider": self.provider,
49
+ "family": self.family,
50
+ "context_window": self.context_window,
51
+ "capabilities": list(self.capabilities),
52
+ "pricing_tier": self.pricing_tier,
53
+ "status": self.status,
54
+ "input_price_per_1m": self.input_price_per_1m,
55
+ "output_price_per_1m": self.output_price_per_1m,
56
+ }
57
+
58
+
59
+ # ── Metadata helpers (JSON-backed) ─────────────────────────────────────────
60
+
61
+ def _meta_to_legacy(entry: dict) -> dict:
62
+ """Convert a model_metadata.json entry to the legacy ModelInfo dict."""
63
+ caps = []
64
+ for key, label in (("reasoning", "reasoning"), ("vision", "vision"),
65
+ ("coding", "coding"), ("embedding", "embeddings"),
66
+ ("image_generation", "image_gen"), ("speech", "audio"),
67
+ ("tools", "tool_use"), ("streaming", "streaming")):
68
+ if entry.get(key):
69
+ caps.append(label)
70
+ if not caps:
71
+ caps = ["chat", "streaming"]
72
+ tier = "free" if entry.get("free") else "paid" if entry.get("paid") else "unknown"
73
+ status = ("deprecated" if entry.get("deprecated")
74
+ else "preview" if entry.get("preview") else "stable")
75
+ return {
76
+ "id": entry.get("id", ""),
77
+ "provider": entry.get("provider", ""),
78
+ "family": entry.get("family", ""),
79
+ "context_window": int(entry.get("context_length", 0) or 0),
80
+ "capabilities": caps,
81
+ "pricing_tier": tier,
82
+ "status": status,
83
+ "input_price_per_1m": float(entry.get("input_price_per_1m", 0.0) or 0.0),
84
+ "output_price_per_1m": float(entry.get("output_price_per_1m", 0.0) or 0.0),
85
+ "recommendation_badges": entry.get("recommendation_badges", []),
86
+ }
87
+
88
+
89
+ def _resolve_meta(model_id: str, provider_id: str) -> Optional[dict]:
90
+ entry = manager.get_model_meta(model_id, provider_id)
91
+ if entry:
92
+ return _meta_to_legacy(entry)
93
+ return None
94
+
95
+
96
+ # ── Lookup Helpers ─────────────────────────────────────────────────────────
97
+
98
+ def get_model_info(model_id: str, provider_id: str) -> Optional[dict]:
99
+ """Look up curated metadata for a specific model+provider combination."""
100
+ return _resolve_meta(model_id, provider_id)
101
+
102
+
103
+ def get_price_tier(model_id: str, provider_id: str) -> str:
104
+ """Return 'free', 'paid', or 'unknown'."""
105
+ info = get_model_info(model_id, provider_id)
106
+ if info:
107
+ return info.get("pricing_tier", "unknown")
108
+ lower = model_id.lower()
109
+ if any(kw in lower for kw in ("free", "open-")):
110
+ return "free"
111
+ return "unknown"
112
+
113
+
114
+ def get_capabilities(model_id: str, provider_id: str) -> list[str]:
115
+ """Return list of known capabilities for this model."""
116
+ info = get_model_info(model_id, provider_id)
117
+ if info:
118
+ return list(info.get("capabilities", []))
119
+ return []
120
+
121
+
122
+ def get_badges(model_id: str, provider_id: str) -> list[str]:
123
+ """Return list of badge strings for UI display.
124
+ Each badge is a short color-tagged string like \"[FREE]\", \"[VISION]\", etc.
125
+ """
126
+ badges = []
127
+ info = get_model_info(model_id, provider_id)
128
+
129
+ # Pricing badge
130
+ tier = (info.get("pricing_tier", "unknown") if info
131
+ else get_price_tier(model_id, provider_id))
132
+ if tier == "free":
133
+ badges.append("[FREE]")
134
+ elif tier == "paid":
135
+ badges.append("[PAID]")
136
+ else:
137
+ badges.append("[?]")
138
+
139
+ # Status badge
140
+ if info:
141
+ status = info.get("status", "")
142
+ if status == "preview":
143
+ badges.append("[PREVIEW]")
144
+ elif status == "experimental":
145
+ badges.append("[EXP]")
146
+ elif status == "deprecated":
147
+ badges.append("[DEPRECATED]")
148
+
149
+ # Capability badges
150
+ caps = info.get("capabilities", []) if info else []
151
+ if "vision" in caps:
152
+ badges.append("[VISION]")
153
+ if "reasoning" in caps:
154
+ badges.append("[REASONING]")
155
+ if "coding" in caps:
156
+ badges.append("[CODING]")
157
+ if "image_gen" in caps:
158
+ badges.append("[IMAGE]")
159
+ if "audio" in caps:
160
+ badges.append("[AUDIO]")
161
+ if "embeddings" in caps:
162
+ badges.append("[EMBED]")
163
+
164
+ # Context window badge
165
+ if info:
166
+ ctx = info.get("context_window", 0)
167
+ if ctx >= 1000000:
168
+ badges.append("[1M]")
169
+ elif ctx >= 100000:
170
+ badges.append("[100K]")
171
+ elif ctx >= 32000:
172
+ badges.append("[32K]")
173
+ elif ctx >= 8000:
174
+ badges.append("[8K]")
175
+
176
+ # Recommendation badges from dynamic registry (e.g. Best for Coding)
177
+ if info:
178
+ for b in info.get("recommendation_badges", []):
179
+ b_clean = f"[{b.strip('[]').upper()}]"
180
+ if b_clean not in badges:
181
+ badges.append(b_clean)
182
+
183
+ return badges
184
+
185
+
186
+ def get_family(model_id: str, provider_id: str) -> str:
187
+ """Return the model family name, if known."""
188
+ info = get_model_info(model_id, provider_id)
189
+ if info:
190
+ return info.get("family", "")
191
+ return ""
192
+
193
+
194
+ def get_context_window(model_id: str, provider_id: str) -> int:
195
+ """Return the context window size, if known."""
196
+ info = get_model_info(model_id, provider_id)
197
+ if info:
198
+ return info.get("context_window", 0)
199
+ return 0
200
+
201
+
202
+ # ── Dynamic Merge ──────────────────────────────────────────────────────────
203
+
204
+ def enrich_models(provider_id: str, model_ids: list[str]) -> list[dict]:
205
+ """Merge dynamic model list with curated metadata.
206
+
207
+ Returns a list of dicts, one per model, sorted alphabetically:
208
+ {"id": str, "pricing_tier": str, "badges": list[str],
209
+ "capabilities": list[str], "family": str, "context_window": int}
210
+ """
211
+ result = []
212
+ for mid in model_ids:
213
+ info = get_model_info(mid, provider_id)
214
+ if info:
215
+ result.append({
216
+ "id": mid,
217
+ "pricing_tier": info.get("pricing_tier", "unknown"),
218
+ "badges": get_badges(mid, provider_id),
219
+ "capabilities": list(info.get("capabilities", [])),
220
+ "family": info.get("family", ""),
221
+ "context_window": info.get("context_window", 0),
222
+ "status": info.get("status", "stable"),
223
+ })
224
+ else:
225
+ tier = get_price_tier(mid, provider_id)
226
+ result.append({
227
+ "id": mid,
228
+ "pricing_tier": tier,
229
+ "badges": ["[?]"],
230
+ "capabilities": [],
231
+ "family": "",
232
+ "context_window": 0,
233
+ "status": "unknown",
234
+ })
235
+ result.sort(key=lambda x: x["id"].lower())
236
+ return result
237
+
238
+
239
+ def tier_sort_key(entry: dict) -> int:
240
+ """Sort key: free (0) before paid (1) before unknown (2)."""
241
+ t = entry.get("pricing_tier", "unknown")
242
+ return {"free": 0, "paid": 1, "unknown": 2}.get(t, 2)
243
+
244
+
245
+ def group_by_tier(enriched: list[dict]) -> dict[str, list[dict]]:
246
+ """Group enriched model list by pricing tier.
247
+
248
+ Returns {"free": [...], "paid": [...], "unknown": [...]}
249
+ """
250
+ groups: dict[str, list[dict]] = {"free": [], "paid": [], "unknown": []}
251
+ for entry in enriched:
252
+ tier = entry.get("pricing_tier", "unknown")
253
+ groups.setdefault(tier, []).append(entry)
254
+ return groups
255
+
256
+
257
+ # ── Re-exports (new manager API convenience) ───────────────────────────────
258
+
259
+ def get_providers() -> list[dict]:
260
+ """All provider metadata from providers.json."""
261
+ return manager.list_providers()
262
+
263
+
264
+ def get_provider(provider_id: str) -> Optional[dict]:
265
+ return manager.get_provider(provider_id)
@@ -0,0 +1,197 @@
1
+ """
2
+ calc_terminal/models/schema.py
3
+ ==============================
4
+ Standardized Model Metadata Schema for CAT's Dynamic Model Registry.
5
+
6
+ Represents AI models in a unified, provider-agnostic format supporting:
7
+ - Canonical model identity across multiple deployment endpoints.
8
+ - Granular capability detection (reasoning, coding, vision, computer use, etc.).
9
+ - Multi-level reasoning configurations (e.g. low, medium, high, xhigh, max).
10
+ - Clear access tier categorization (open_weight, free_api, paid_api, local, cloud).
11
+ - Release and verification tracking (first_seen, last_seen, last_verified).
12
+ """
13
+
14
+ from dataclasses import dataclass, field, asdict
15
+ from datetime import datetime, timezone
16
+ from typing import List, Dict, Any, Optional
17
+
18
+
19
+ # Standard Availability States
20
+ AVAILABILITY_API = "api"
21
+ AVAILABILITY_OPEN_WEIGHT = "open_weight"
22
+ AVAILABILITY_FREE_API = "free_api"
23
+ AVAILABILITY_FREE_TIER = "free_tier"
24
+ AVAILABILITY_PAID_API = "paid_api"
25
+ AVAILABILITY_LOCAL = "local"
26
+ AVAILABILITY_CLOUD = "cloud"
27
+ AVAILABILITY_SUBSCRIPTION = "subscription"
28
+
29
+ # Standard Model Statuses
30
+ STATUS_ACTIVE = "active"
31
+ STATUS_PREVIEW = "preview"
32
+ STATUS_BETA = "beta"
33
+ STATUS_DEPRECATED = "deprecated"
34
+ STATUS_RETIRED = "retired"
35
+ STATUS_UNAVAILABLE = "unavailable"
36
+ STATUS_AUTH_REQUIRED = "auth_required"
37
+ STATUS_DISCOVERED_UNAVAILABLE = "discovered_but_unavailable"
38
+
39
+ # Standard Categories
40
+ CATEGORY_GENERAL = "general"
41
+ CATEGORY_REASONING = "reasoning"
42
+ CATEGORY_CODING = "coding"
43
+ CATEGORY_FAST = "fast"
44
+ CATEGORY_CHEAP = "cheap"
45
+ CATEGORY_LONG_CONTEXT = "long_context"
46
+ CATEGORY_VISION = "vision"
47
+ CATEGORY_AUDIO = "audio"
48
+ CATEGORY_MULTIMODAL = "multimodal"
49
+ CATEGORY_AGENT = "agent"
50
+ CATEGORY_RESEARCH = "research"
51
+ CATEGORY_LOCAL = "local"
52
+ CATEGORY_OPEN_WEIGHT = "open_weight"
53
+ CATEGORY_EMBEDDING = "embedding"
54
+ CATEGORY_SPECIALIZED = "specialized"
55
+ CATEGORY_FREE_API = "free"
56
+ CATEGORY_PAID_API = "paid"
57
+ AVAILABILITY_FREE_TIER = "free_tier"
58
+
59
+
60
+ def _now_iso() -> str:
61
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
62
+
63
+
64
+ @dataclass
65
+ class ModelInfo:
66
+ """Standardized representation of an AI model in CAT."""
67
+
68
+ provider: str
69
+ model_id: str
70
+ display_name: str = ""
71
+ family: str = ""
72
+ version: str = ""
73
+ canonical_model_id: str = ""
74
+ release_date: Optional[str] = None
75
+ status: str = STATUS_ACTIVE
76
+ availability: str = AVAILABILITY_PAID_API
77
+ modalities: List[str] = field(default_factory=lambda: ["text"])
78
+ capabilities: List[str] = field(default_factory=lambda: ["chat", "streaming"])
79
+ context_window: int = 128000
80
+ max_output_tokens: int = 4096
81
+ reasoning_levels: List[str] = field(default_factory=list)
82
+ pricing: Dict[str, float] = field(default_factory=lambda: {"input_price_per_1m": 0.0, "output_price_per_1m": 0.0})
83
+ endpoint: str = ""
84
+ authentication_required: bool = True
85
+ local: bool = False
86
+ cloud: bool = True
87
+ verified: bool = False
88
+ source: str = "official_api"
89
+ first_seen: str = field(default_factory=_now_iso)
90
+ last_seen: str = field(default_factory=_now_iso)
91
+ last_verified: str = field(default_factory=_now_iso)
92
+ stale: bool = False
93
+ tags: List[str] = field(default_factory=list)
94
+ recommendation_badges: List[str] = field(default_factory=list)
95
+
96
+ def __post_init__(self):
97
+ if not self.display_name:
98
+ self.display_name = self.model_id
99
+ if not self.canonical_model_id:
100
+ self.canonical_model_id = f"{self.provider}:{self.model_id}"
101
+ if not self.family:
102
+ self.family = self._derive_family()
103
+
104
+ def _derive_family(self) -> str:
105
+ mid = self.model_id.lower()
106
+ if "gpt" in mid or "o1" in mid or "o3" in mid:
107
+ return "GPT"
108
+ if "claude" in mid:
109
+ return "Claude"
110
+ if "gemini" in mid:
111
+ return "Gemini"
112
+ if "deepseek" in mid:
113
+ return "DeepSeek"
114
+ if "qwen" in mid:
115
+ return "Qwen"
116
+ if "kimi" in mid or "moonshot" in mid:
117
+ return "Kimi"
118
+ if "glm" in mid:
119
+ return "GLM"
120
+ if "grok" in mid:
121
+ return "Grok"
122
+ if "mistral" in mid or "codestral" in mid:
123
+ return "Mistral"
124
+ if "command" in mid:
125
+ return "Command"
126
+ if "llama" in mid:
127
+ return "Llama"
128
+ if "minimax" in mid or "abab" in mid:
129
+ return "MiniMax"
130
+ if "hunyuan" in mid or "hy" in mid:
131
+ return "Hunyuan"
132
+ if "doubao" in mid or "seed" in mid:
133
+ return "Seed"
134
+ if "ernie" in mid:
135
+ return "ERNIE"
136
+ return self.provider.capitalize()
137
+
138
+ @property
139
+ def availability_state(self) -> str:
140
+ return self.status
141
+
142
+ def is_usable(self) -> bool:
143
+ """A model is usable only when it has an active/preview state,
144
+ is verified or from an official provider endpoint, and is not
145
+ marked as discovered_but_unavailable."""
146
+ return (
147
+ self.status in (STATUS_ACTIVE, STATUS_PREVIEW, STATUS_BETA)
148
+ and not self.status == STATUS_DISCOVERED_UNAVAILABLE
149
+ )
150
+
151
+ def has_capability(self, capability: str) -> bool:
152
+ cap = capability.lower().strip()
153
+ return cap in [c.lower() for c in self.capabilities]
154
+
155
+ def matches_category(self, category: str) -> bool:
156
+ cat = category.lower().strip()
157
+ if cat in ("all", "*"):
158
+ return True
159
+ if cat == CATEGORY_LOCAL:
160
+ return self.local
161
+ if cat == CATEGORY_OPEN_WEIGHT:
162
+ return self.availability == AVAILABILITY_OPEN_WEIGHT or "open_weight" in self.tags
163
+ if cat in (CATEGORY_FREE_API, "free"):
164
+ return self.availability in (AVAILABILITY_FREE_API, AVAILABILITY_FREE_TIER) or self.pricing.get("input_price_per_1m", 0.0) == 0.0
165
+ if cat in (CATEGORY_PAID_API, "paid"):
166
+ return self.pricing.get("input_price_per_1m", 0.0) > 0.0
167
+ if cat == CATEGORY_REASONING:
168
+ return self.has_capability("reasoning") or bool(self.reasoning_levels) or any(k in self.model_id.lower() for k in ("reasoner", "r1", "o1", "o3", "qwq", "k3", "thinking"))
169
+ if cat == CATEGORY_CODING:
170
+ return self.has_capability("coding") or any(k in self.model_id.lower() for k in ("coder", "codestral", "astra", "code"))
171
+ if cat == CATEGORY_VISION:
172
+ return self.has_capability("vision") or "image" in self.modalities or any(k in self.model_id.lower() for k in ("vision", "4o", "4v", "pixtral", "vl", "gemini"))
173
+ if cat == CATEGORY_FAST:
174
+ return self.has_capability("fast") or any(k in self.model_id.lower() for k in ("mini", "flash", "turbo", "instant", "haiku", "8b"))
175
+ if cat == CATEGORY_LONG_CONTEXT:
176
+ return self.context_window >= 128000
177
+ if cat == CATEGORY_AGENT:
178
+ return self.has_capability("agent") or self.has_capability("computer_use") or self.has_capability("tools")
179
+ if cat == CATEGORY_RESEARCH:
180
+ return self.has_capability("research") or self.matches_category(CATEGORY_REASONING)
181
+ return self.has_capability(cat) or cat in self.tags
182
+
183
+ def to_dict(self) -> Dict[str, Any]:
184
+ return asdict(self)
185
+
186
+ @classmethod
187
+ def from_dict(cls, data: Dict[str, Any]) -> "ModelInfo":
188
+ cleaned = dict(data)
189
+ # Handle backward-compatible fields
190
+ if "id" in cleaned and "model_id" not in cleaned:
191
+ cleaned["model_id"] = cleaned.pop("id")
192
+ if "context_length" in cleaned and "context_window" not in cleaned:
193
+ cleaned["context_window"] = int(cleaned.pop("context_length") or 128000)
194
+ # Retain only recognized dataclass keys
195
+ valid_keys = {f for f in cls.__dataclass_fields__}
196
+ filtered = {k: v for k, v in cleaned.items() if k in valid_keys}
197
+ return cls(**filtered)