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,767 @@
1
+ """Model lifecycle management — validation, deprecation handling,
2
+ alias resolution, background refresh, and health monitoring.
3
+
4
+ Architecture:
5
+ ┌─────────────────────────┐
6
+ │ Application/Chat │
7
+ │ pre_request_check() │
8
+ │ validate_and_chat() │
9
+ └────────┬────────────────┘
10
+ │ validate_model()
11
+ ▼
12
+ ┌─────────────────────────┐
13
+ │ ModelLifecycleManager │
14
+ │ ─ cache check │
15
+ │ ─ deprecation check │
16
+ │ ─ alias resolution │
17
+ │ ─ refresh on stale │
18
+ └────────┬────────────────┘
19
+ │ fetch_models()
20
+ ▼
21
+ ┌─────────────────────────┐
22
+ │ Provider Adapter │
23
+ │ (generic fallback or │
24
+ │ registered class) │
25
+ └─────────────────────────┘
26
+ """
27
+
28
+ import os
29
+ import re
30
+ import threading
31
+ import time
32
+ from typing import Optional
33
+ from dataclasses import dataclass, field
34
+
35
+
36
+ # ── Logging ────────────────────────────────────────────────────────────────
37
+ # User app-data logs dir first (pip-installed site-packages is read-only);
38
+ # legacy repo-root startup.log as fallback. Best-effort only.
39
+
40
+ def _resolve_log_file() -> str:
41
+ try:
42
+ from calc_terminal.first_run import logs_dir as _logs_dir
43
+
44
+ import os as _os
45
+
46
+ _d = _logs_dir()
47
+ _os.makedirs(_d, exist_ok=True)
48
+ return _os.path.join(_d, "startup.log")
49
+ except Exception:
50
+ pass
51
+ return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
52
+ os.path.abspath(__file__)))), "startup.log")
53
+
54
+
55
+ _LOG_FILE = _resolve_log_file()
56
+
57
+
58
+ def _log(msg: str, exc_info: bool = False):
59
+ try:
60
+ ts = time.strftime("%H:%M:%S")
61
+ with open(_LOG_FILE, "a", encoding="utf-8") as f:
62
+ f.write(f"[{ts}] [Lifecycle] {msg}\n")
63
+ if exc_info:
64
+ import traceback
65
+ traceback.print_exc(file=f)
66
+ except Exception:
67
+ pass
68
+
69
+
70
+ # ── Result Types ───────────────────────────────────────────────────────────
71
+
72
+ @dataclass
73
+ class ValidationResult:
74
+ """Result of a model validation check."""
75
+ valid: bool
76
+ model: str
77
+ message: str = ""
78
+ replacement: Optional[str] = None
79
+ retry_after_refresh: bool = False
80
+
81
+
82
+ @dataclass
83
+ class RecoveryResult:
84
+ """Result of a deprecation recovery attempt."""
85
+ recovered: bool
86
+ model: str
87
+ message: str
88
+ new_model: Optional[str] = None
89
+
90
+
91
+ @dataclass
92
+ class ProviderCapabilities:
93
+ """Capability flags for a provider adapter."""
94
+ supports_model_listing: bool = True
95
+ supports_streaming: bool = True
96
+ supports_tools: bool = False
97
+ supports_reasoning: bool = False
98
+ supports_vision: bool = False
99
+ supports_audio: bool = False
100
+ supports_images: bool = False
101
+ supports_embeddings: bool = False
102
+ supports_reranking: bool = False
103
+
104
+
105
+ @dataclass
106
+ class ProviderHealth:
107
+ """Health snapshot for a provider."""
108
+ provider_id: str = ""
109
+ online: bool = False
110
+ api_reachable: bool = False
111
+ authenticated: bool = False
112
+ model_list_available: bool = False
113
+ chat_supported: bool = False
114
+ latency_ms: float = 0.0
115
+ last_checked: float = 0.0
116
+ error: str = ""
117
+
118
+
119
+ # ── Model Alias / Replacement Map ──────────────────────────────────────────
120
+ # Known model renames/replacements published by providers.
121
+ # Format: (provider_id, old_model) -> new_model
122
+ # Data-driven (v0.7.4+): the authoritative alias list lives in
123
+ # models/model_metadata.json ("aliases"); the dict below is the fallback
124
+ # used only when the JSON store is unreadable.
125
+
126
+ _MODEL_ALIASES: dict[tuple[str, str], str] = {
127
+ # OpenAI
128
+ ("openai", "gpt-4"): "gpt-4o",
129
+ ("openai", "gpt-4-turbo-preview"): "gpt-4-turbo",
130
+ ("openai", "gpt-3.5-turbo-0125"): "gpt-4o-mini",
131
+
132
+ # Anthropic
133
+ ("anthropic", "claude-3-opus-20240229"): "claude-opus-4-20250514",
134
+ ("anthropic", "claude-3-sonnet-20240229"): "claude-sonnet-4-20250514",
135
+ ("anthropic", "claude-3-haiku-20240307"): "claude-3-5-haiku-20241022",
136
+ ("anthropic", "claude-instant-1"): "claude-3-haiku-20240307",
137
+ ("anthropic", "claude-2.1"): "claude-3-5-sonnet-20241022",
138
+ ("anthropic", "claude-2.0"): "claude-3-5-sonnet-20241022",
139
+ ("anthropic", "claude-1"): "claude-3-5-sonnet-20241022",
140
+ ("anthropic", "claude-1.3"): "claude-3-5-sonnet-20241022",
141
+ ("anthropic", "claude-1.2"): "claude-3-5-sonnet-20241022",
142
+ ("anthropic", "claude-1.1"): "claude-3-5-sonnet-20241022",
143
+
144
+ # DeepSeek
145
+ ("deepseek", "deepseek-chat"): "deepseek-chat",
146
+ ("deepseek", "deepseek-v2"): "deepseek-chat",
147
+
148
+ # Mistral
149
+ ("mistral", "mistral-tiny"): "open-mistral-nemo",
150
+ ("mistral", "mistral-small-latest"): "mistral-small-2402",
151
+ ("mistral", "mistral-medium-latest"): "mistral-large-2407",
152
+
153
+ # Google
154
+ ("gemini", "gemini-pro"): "gemini-1.5-pro",
155
+ ("gemini", "gemini-pro-vision"): "gemini-1.5-pro",
156
+ ("gemini", "gemini-ultra"): "gemini-2.0-flash",
157
+
158
+ # xAI
159
+ ("xai", "grok-1"): "grok-2-1212",
160
+
161
+ # Perplexity
162
+ ("perplexity", "pplx-70b-online"): "sonar-pro",
163
+ ("perplexity", "pplx-7b-online"): "sonar",
164
+
165
+ # Cohere
166
+ ("cohere", "command-xlarge"): "command",
167
+ ("cohere", "command-xlarge-beta"): "command",
168
+ ("cohere", "command-medium"): "command-r",
169
+ ("cohere", "command-medium-beta"): "command-r",
170
+ }
171
+
172
+
173
+ def _load_aliases() -> dict[tuple[str, str], str]:
174
+ """Authoritative alias map from model_metadata.json; falls back to the
175
+ embedded dict if the JSON store is missing or unreadable."""
176
+ try:
177
+ from ..models import manager as _mgr
178
+ with open(_mgr.MODEL_METADATA_FILE, "r", encoding="utf-8") as f:
179
+ import json as _json
180
+ data = _json.load(f)
181
+ out = {}
182
+ for a in data.get("aliases", []):
183
+ if a.get("provider") and a.get("from") and a.get("to"):
184
+ out[(str(a["provider"]), str(a["from"]))] = str(a["to"])
185
+ if out:
186
+ return out
187
+ except Exception:
188
+ pass
189
+ return dict(_MODEL_ALIASES)
190
+
191
+ # Patterns that indicate a model-not-found error in the response
192
+ _MODEL_NOT_FOUND_PATTERNS = [
193
+ "model.*not found",
194
+ "model.*unavailable",
195
+ "model.*deprecated",
196
+ "model.*decommissioned",
197
+ "model.*removed",
198
+ "model.*retired",
199
+ "model.*no longer",
200
+ "model.*unknown",
201
+ "model.*invalid",
202
+ "model.*unsupported",
203
+ "unknown model",
204
+ "not found",
205
+ "not a valid model",
206
+ "does not exist",
207
+ "has been decommissioned",
208
+ "has been deprecated",
209
+ "has been removed",
210
+ "is not supported",
211
+ "is not available",
212
+ ]
213
+
214
+ # ── Refresh Tracking ───────────────────────────────────────────────────────
215
+ # {provider_id: {"last_refresh": timestamp, "refresh_count": int, "daily": bool}}
216
+
217
+ _refresh_tracker: dict[str, dict] = {}
218
+ _refresh_lock = threading.Lock()
219
+ _REFRESH_COOLDOWN = 300 # 5 minutes minimum between refreshes
220
+ _DAILY_REFRESH_SECONDS = 86400 # 24 hours
221
+
222
+
223
+ def _get_refresh_state(provider_id: str) -> dict:
224
+ with _refresh_lock:
225
+ if provider_id not in _refresh_tracker:
226
+ _refresh_tracker[provider_id] = {
227
+ "last_refresh": 0.0,
228
+ "refresh_count": 0,
229
+ "daily_refreshed": False,
230
+ }
231
+ return _refresh_tracker[provider_id]
232
+
233
+
234
+ def _update_refresh_time(provider_id: str):
235
+ with _refresh_lock:
236
+ state = _refresh_tracker.get(provider_id, {})
237
+ state["last_refresh"] = time.time()
238
+ state["refresh_count"] = state.get("refresh_count", 0) + 1
239
+ _refresh_tracker[provider_id] = state
240
+
241
+
242
+ def should_refresh(provider_id: str, force: bool = False) -> bool:
243
+ """Check whether a model refresh is needed for this provider.
244
+
245
+ Returns True when:
246
+ - force=True
247
+ - never refreshed before
248
+ - last refresh older than cooldown (for on-error immediate refresh)
249
+ - last refresh older than 24h (daily refresh)
250
+ """
251
+ if force:
252
+ return True
253
+ state = _get_refresh_state(provider_id)
254
+ now = time.time()
255
+ last = state.get("last_refresh", 0.0)
256
+ if last == 0.0:
257
+ return True # never refreshed
258
+ if now - last > _DAILY_REFRESH_SECONDS:
259
+ return True # stale daily refresh
260
+ return False
261
+
262
+
263
+ # ── Model Lifecycle Manager ────────────────────────────────────────────────
264
+
265
+ class ModelLifecycleManager:
266
+ """Central manager for model lifecycle operations.
267
+
268
+ Provides model validation, deprecation detection, alias resolution,
269
+ background refresh coordination, and health monitoring.
270
+ """
271
+
272
+ def __init__(self):
273
+ self._deprecated: dict[str, dict[str, str]] = {}
274
+ # {provider_id: {bad_model: replacement_model_or_none}}
275
+
276
+ # ── Validation ─────────────────────────────────────────────────────
277
+
278
+ def validate_model(self, provider_id: str, model: str) -> ValidationResult:
279
+ """Check whether a model is valid, current, and available.
280
+
281
+ Returns ValidationResult with:
282
+ - valid=True if model is good to use
283
+ - replacement set if an alias or upgrade path exists
284
+ - retry_after_refresh=True if models are stale
285
+ """
286
+ if not model:
287
+ return ValidationResult(
288
+ valid=False, model=model,
289
+ message="No model selected")
290
+
291
+ # 1. Check if model is actively tracked as deprecated
292
+ dep_map = self._deprecated.get(provider_id, {})
293
+ if model in dep_map:
294
+ replacement = dep_map[model]
295
+ msg = (f"Model \"{model}\" has been deprecated"
296
+ + (f" \u2014 use \"{replacement}\" instead" if replacement else ""))
297
+ _log(f"validate_model [{provider_id}] DEPRECATED: {model}"
298
+ + (f" -> {replacement}" if replacement else ""))
299
+ return ValidationResult(
300
+ valid=False, model=model, message=msg,
301
+ replacement=replacement, retry_after_refresh=False)
302
+
303
+ # 2. Check alias map (data-driven: model_metadata.json)
304
+ alias_key = (provider_id, model)
305
+ alias_map = _load_aliases()
306
+ if alias_key in alias_map:
307
+ replacement = alias_map[alias_key]
308
+ _log(f"validate_model [{provider_id}] ALIAS: {model} -> {replacement}")
309
+ return ValidationResult(
310
+ valid=True, model=replacement,
311
+ message=f"Model \"{model}\" aliased to \"{replacement}\"",
312
+ replacement=replacement, retry_after_refresh=False)
313
+
314
+ # 3. Check cache freshness
315
+ try:
316
+ from .provider_manager import get_cached_models
317
+ cached = get_cached_models(provider_id)
318
+ if cached is not None:
319
+ if model in cached:
320
+ return ValidationResult(valid=True, model=model)
321
+ # Model not in cached list — might be new or removed
322
+ # Try refreshing if cache is old
323
+ state = _get_refresh_state(provider_id)
324
+ if time.time() - state.get("last_refresh", 0) > _REFRESH_COOLDOWN:
325
+ return ValidationResult(
326
+ valid=False, model=model,
327
+ message="Model not found in current registry",
328
+ retry_after_refresh=True)
329
+ except Exception:
330
+ pass
331
+
332
+ # 4. No cache — model is unknown but could still work
333
+ return ValidationResult(valid=True, model=model)
334
+
335
+ def resolve_alias(self, provider_id: str, model: str) -> str:
336
+ """Resolve model alias to its current name. Returns current model."""
337
+ alias_key = (provider_id, model)
338
+ return _load_aliases().get(alias_key, model)
339
+
340
+ # ── Deprecation Handling ───────────────────────────────────────────
341
+
342
+ def is_model_not_found_error(self, status_code: int, response_text: str) -> bool:
343
+ """Heuristic check if an error response indicates a model lifecycle issue."""
344
+ if status_code in (400, 404, 410):
345
+ lower = response_text.lower()
346
+ for pattern in _MODEL_NOT_FOUND_PATTERNS:
347
+ if re.search(pattern, lower):
348
+ return True
349
+ return False
350
+
351
+ def handle_deprecation_error(
352
+ self, provider_id: str, model: str,
353
+ status_code: int, response_text: str
354
+ ) -> RecoveryResult:
355
+ """Handle a model-not-found error with automatic recovery.
356
+
357
+ Steps:
358
+ 1. Refresh model list from provider
359
+ 2. If model reappears, keep it
360
+ 3. If model is gone, check alias map
361
+ 4. If alias exists, switch to alias
362
+ 5. If same-family model exists, suggest it
363
+ 6. Mark model as deprecated for future checks
364
+ """
365
+ _log(f"handle_deprecation [{provider_id}] model={model} "
366
+ f"HTTP {status_code}: {response_text[:200]}")
367
+
368
+ # Step 1: Refresh models
369
+ try:
370
+ from .provider_manager import discover_models, get_cached_models
371
+ config = {"provider": provider_id, "base_url": "", "api_style": "openai"}
372
+ refreshed = discover_models(provider_id, config)
373
+ _log(f"handle_deprecation [{provider_id}] refreshed: {len(refreshed)} models")
374
+ except Exception as e:
375
+ _log(f"handle_deprecation [{provider_id}] refresh failed: {e}")
376
+ refreshed = []
377
+
378
+ # Step 2: Check if model reappeared
379
+ if model in refreshed:
380
+ _log(f"handle_deprecation [{provider_id}] {model} still available after refresh")
381
+ return RecoveryResult(
382
+ recovered=True, model=model,
383
+ message=f"Model \"{model}\" confirmed available")
384
+
385
+ # Step 3: Check alias
386
+ replacement = self.resolve_alias(provider_id, model)
387
+ if replacement != model:
388
+ _log(f"handle_deprecation [{provider_id}] alias: {model} -> {replacement}")
389
+ self._mark_deprecated(provider_id, model, replacement)
390
+ return RecoveryResult(
391
+ recovered=True, model=replacement,
392
+ message=f"Model \"{model}\" aliased to \"{replacement}\"",
393
+ new_model=replacement)
394
+
395
+ # Step 4: Find same-family replacement from refreshed list
396
+ from calc_terminal.models.registry import get_family
397
+ family = get_family(model, provider_id)
398
+ if family and refreshed:
399
+ same_family = sorted([m for m in refreshed
400
+ if get_family(m, provider_id) == family])
401
+ if same_family:
402
+ replacement = same_family[-1] # pick newest in family
403
+ _log(f"handle_deprecation [{provider_id}] family match: "
404
+ f"{family} -> {replacement}")
405
+ self._mark_deprecated(provider_id, model, replacement)
406
+ return RecoveryResult(
407
+ recovered=True, model=replacement,
408
+ message=f"Switched to \"{replacement}\" (same family: {family})",
409
+ new_model=replacement)
410
+
411
+ # Step 5: Try provider default
412
+ try:
413
+ from .provider_manager import get_provider_class
414
+ cls = get_provider_class(provider_id)
415
+ replacement = cls.default_model() if cls else ""
416
+ if cls and replacement:
417
+ _log(f"handle_deprecation [{provider_id}] using default: {replacement}")
418
+ self._mark_deprecated(provider_id, model, replacement)
419
+ return RecoveryResult(
420
+ recovered=True, model=replacement,
421
+ message=f"Fell back to default model \"{replacement}\"",
422
+ new_model=replacement)
423
+ except Exception:
424
+ pass
425
+
426
+ # Step 6: Pick first available model
427
+ if refreshed:
428
+ replacement = sorted(refreshed)[0]
429
+ _log(f"handle_deprecation [{provider_id}] first available: {replacement}")
430
+ self._mark_deprecated(provider_id, model, replacement)
431
+ return RecoveryResult(
432
+ recovered=True, model=replacement,
433
+ message=f"Using first available model \"{replacement}\"",
434
+ new_model=replacement)
435
+
436
+ # Step 7: No recovery possible
437
+ _log(f"handle_deprecation [{provider_id}] no recovery possible")
438
+ return RecoveryResult(
439
+ recovered=False, model=model,
440
+ message=f"Model \"{model}\" is no longer available and no replacement found")
441
+
442
+ def _mark_deprecated(self, provider_id: str, model: str, replacement: Optional[str] = None):
443
+ """Track a deprecated model so future validate_model calls catch it fast."""
444
+ if provider_id not in self._deprecated:
445
+ self._deprecated[provider_id] = {}
446
+ self._deprecated[provider_id][model] = replacement
447
+ _log(f"deprecated [{provider_id}] tracked: {model}"
448
+ + (f" -> {replacement}" if replacement else ""))
449
+
450
+ def get_deprecated(self, provider_id: str) -> dict[str, Optional[str]]:
451
+ """Get known deprecated models for a provider."""
452
+ return dict(self._deprecated.get(provider_id, {}))
453
+
454
+ # ── Pre-Request Check ─────────────────────────────────────────────
455
+
456
+ def pre_request_check(self, provider_id: str, model: str) -> ValidationResult:
457
+ """Complete pre-request validation with auto-recovery.
458
+
459
+ Called before every chat/stream request.
460
+ Validates the model, resolves aliases, triggers refresh if stale.
461
+ """
462
+ result = self.validate_model(provider_id, model)
463
+ if result.valid:
464
+ return result
465
+
466
+ # If stale cache, refresh and re-validate
467
+ if result.retry_after_refresh:
468
+ _log(f"pre_request_check [{provider_id}] refreshing stale models")
469
+ try:
470
+ from .provider_manager import discover_models, set_cached_models
471
+ config = {"provider": provider_id, "base_url": "", "api_style": "openai"}
472
+ fresh = discover_models(provider_id, config)
473
+ if fresh and model in fresh:
474
+ _update_refresh_time(provider_id)
475
+ return ValidationResult(valid=True, model=model,
476
+ message="Model confirmed after refresh")
477
+ if fresh and len(fresh) > 0:
478
+ # Model still not found — try deprecation recovery
479
+ recovery = self.handle_deprecation_error(
480
+ provider_id, model, 400,
481
+ "Model not found in refreshed registry")
482
+ if recovery.recovered and recovery.new_model:
483
+ _update_refresh_time(provider_id)
484
+ return ValidationResult(
485
+ valid=True, model=recovery.new_model,
486
+ message=recovery.message,
487
+ replacement=recovery.new_model)
488
+ except Exception as e:
489
+ _log(f"pre_request_check [{provider_id}] error during refresh: {e}")
490
+
491
+ return result
492
+
493
+ # ── Chat/Stream Wrappers ───────────────────────────────────────────
494
+
495
+ def validate_and_chat(
496
+ self, config: dict,
497
+ prompt: str, system_prompt: str = "",
498
+ history: Optional[list] = None
499
+ ) -> str:
500
+ """Wrapper around provider chat() that validates model first.
501
+
502
+ If the model is deprecated/aliased, resolves it automatically.
503
+ If the request fails with model-not-found, attempts recovery.
504
+ """
505
+ provider_id = config.get("provider", "")
506
+ model = config.get("model", "")
507
+
508
+ # Phase 1: Pre-request validation
509
+ validation = self.pre_request_check(provider_id, model)
510
+ if not validation.valid:
511
+ return (f"[Model Error] {validation.message}")
512
+
513
+ # Use resolved model
514
+ resolved = validation.replacement or validation.model
515
+ if resolved != model:
516
+ config["model"] = resolved
517
+ _log(f"validate_and_chat [{provider_id}] model resolved: {model} -> {resolved}")
518
+
519
+ # Phase 2: Send request
520
+ try:
521
+ from .provider_manager import get_provider
522
+ inst = get_provider(config)
523
+ if not inst:
524
+ return "AI not configured."
525
+ response = inst.chat(prompt, system_prompt, history)
526
+ return response
527
+ except Exception as e:
528
+ error_text = str(e)
529
+ _log(f"validate_and_chat [{provider_id}] chat failed: {error_text}",
530
+ exc_info=True)
531
+
532
+ # Phase 3: Check for model-not-found in exception
533
+ if self.is_model_not_found_error(0, error_text):
534
+ recovery = self.handle_deprecation_error(
535
+ provider_id, resolved, 0, error_text)
536
+ if recovery.recovered and recovery.new_model:
537
+ config["model"] = recovery.new_model
538
+ _log(f"validate_and_chat [{provider_id}] retry with {recovery.new_model}")
539
+ try:
540
+ inst = get_provider(config)
541
+ if inst:
542
+ return (f"[Auto-recovered] {recovery.message}\n\n"
543
+ + inst.chat(prompt, system_prompt, history))
544
+ except Exception:
545
+ pass
546
+ return (f"[Model Error] {recovery.message}")
547
+
548
+ return f"Error: {error_text}"
549
+
550
+ def validate_and_stream(
551
+ self, config: dict,
552
+ prompt: str, system_prompt: str = "",
553
+ history: Optional[list] = None
554
+ ):
555
+ """Wrapper around provider stream() that validates model first.
556
+
557
+ Same lifecycle logic as validate_and_chat but yields text fragments.
558
+ """
559
+ provider_id = config.get("provider", "")
560
+ model = config.get("model", "")
561
+
562
+ validation = self.pre_request_check(provider_id, model)
563
+ if not validation.valid:
564
+ yield f"[Model Error] {validation.message}"
565
+ return
566
+
567
+ resolved = validation.replacement or validation.model
568
+ if resolved != model:
569
+ config["model"] = resolved
570
+ _log(f"validate_and_stream [{provider_id}] model resolved: {model} -> {resolved}")
571
+
572
+ try:
573
+ from .provider_manager import get_provider
574
+ inst = get_provider(config)
575
+ if not inst:
576
+ yield "AI not configured."
577
+ return
578
+ yield from inst.stream(prompt, system_prompt, history)
579
+ except Exception as e:
580
+ error_text = str(e)
581
+ _log(f"validate_and_stream [{provider_id}] stream failed: {error_text}",
582
+ exc_info=True)
583
+
584
+ if self.is_model_not_found_error(0, error_text):
585
+ recovery = self.handle_deprecation_error(
586
+ provider_id, resolved, 0, error_text)
587
+ if recovery.recovered and recovery.new_model:
588
+ config["model"] = recovery.new_model
589
+ yield f"\n[Auto-recovered] {recovery.message}\n"
590
+ try:
591
+ inst = get_provider(config)
592
+ if inst:
593
+ yield from inst.stream(prompt, system_prompt, history)
594
+ return
595
+ except Exception:
596
+ pass
597
+ yield f"\n[Model Error] {recovery.message}"
598
+ else:
599
+ yield f"Error: {error_text}"
600
+
601
+ # ── Health Monitoring ─────────────────────────────────────────────
602
+
603
+ def check_health(self, provider_id: str,
604
+ config: Optional[dict] = None) -> ProviderHealth:
605
+ """Check provider health: reachability, auth, model listing, chat.
606
+
607
+ Returns a ProviderHealth dataclass with detailed status.
608
+ """
609
+ health = ProviderHealth(provider_id=provider_id, last_checked=time.time())
610
+
611
+ if not config:
612
+ try:
613
+ from .provider_manager import load_config
614
+ cfg = load_config()
615
+ config = cfg
616
+ except Exception:
617
+ health.error = "No config available"
618
+ return health
619
+
620
+ cfg = dict(config) if config else {}
621
+ cfg["provider"] = provider_id
622
+ start = time.time()
623
+
624
+ try:
625
+ from .provider_manager import get_provider, get_provider_class
626
+ from .provider_manager import get_cached_models, discover_models
627
+
628
+ cls = get_provider_class(provider_id)
629
+ has_class = cls is not None
630
+
631
+ # Try to instantiate provider
632
+ inst = get_provider(cfg)
633
+ if inst:
634
+ health.api_reachable = True
635
+ health.chat_supported = True
636
+
637
+ # Auth check via model fetch
638
+ try:
639
+ models = inst.fetch_models()
640
+ if models:
641
+ health.authenticated = True
642
+ health.model_list_available = True
643
+ else:
644
+ health.authenticated = False
645
+ health.error = "Model list empty (auth may be missing)"
646
+ except Exception as auth_e:
647
+ health.error = str(auth_e)
648
+ health.authenticated = False
649
+
650
+ elif has_class:
651
+ health.error = "Provider class found but instantiation failed"
652
+ else:
653
+ # Generic provider — try HTTP reachability
654
+ base_url = cfg.get("base_url", "")
655
+ if base_url:
656
+ import requests
657
+ try:
658
+ resp = requests.get(f"{base_url.rstrip('/')}/models",
659
+ timeout=5)
660
+ health.api_reachable = resp.status_code < 500
661
+ health.authenticated = resp.status_code not in (401, 403)
662
+ health.model_list_available = resp.status_code == 200
663
+ except Exception as e:
664
+ health.error = str(e)
665
+ health.api_reachable = False
666
+
667
+ health.online = health.api_reachable
668
+ health.latency_ms = round((time.time() - start) * 1000)
669
+
670
+ except Exception as e:
671
+ health.error = str(e)
672
+ _log(f"check_health [{provider_id}] failed: {e}", exc_info=True)
673
+
674
+ # Update refresh tracker
675
+ _update_refresh_time(provider_id)
676
+ return health
677
+
678
+ # ── Background Refresh ────────────────────────────────────────────
679
+
680
+ def ensure_fresh_models(self, provider_id: str,
681
+ config: Optional[dict] = None,
682
+ force: bool = False) -> list[str]:
683
+ """Ensure models are fresh — refresh if needed.
684
+
685
+ Called on provider creation, first use, or manually.
686
+ """
687
+ if not should_refresh(provider_id, force=force):
688
+ try:
689
+ from .provider_manager import get_cached_models
690
+ cached = get_cached_models(provider_id)
691
+ if cached is not None:
692
+ return cached
693
+ except Exception:
694
+ pass
695
+
696
+ cfg = config or {"provider": provider_id, "base_url": "", "api_style": "openai"}
697
+ cfg["provider"] = provider_id
698
+
699
+ try:
700
+ from .provider_manager import discover_models
701
+ models = discover_models(provider_id, cfg)
702
+ _update_refresh_time(provider_id)
703
+ _log(f"ensure_fresh_models [{provider_id}]: {len(models)} models "
704
+ f"({'cached' if not force else 'forced'})")
705
+ return models or []
706
+ except Exception as e:
707
+ _log(f"ensure_fresh_models [{provider_id}] failed: {e}")
708
+ return []
709
+
710
+
711
+ # ── Singleton ──────────────────────────────────────────────────────────────
712
+
713
+ _instance: Optional[ModelLifecycleManager] = None
714
+ _lock = threading.Lock()
715
+
716
+
717
+ def get_lifecycle() -> ModelLifecycleManager:
718
+ """Get the shared ModelLifecycleManager singleton."""
719
+ global _instance
720
+ if _instance is None:
721
+ with _lock:
722
+ if _instance is None:
723
+ _instance = ModelLifecycleManager()
724
+ return _instance
725
+
726
+
727
+ # ── Convenience Functions ──────────────────────────────────────────────────
728
+
729
+ def validate_model(provider_id: str, model: str) -> ValidationResult:
730
+ return get_lifecycle().validate_model(provider_id, model)
731
+
732
+
733
+ def pre_request_check(provider_id: str, model: str) -> ValidationResult:
734
+ return get_lifecycle().pre_request_check(provider_id, model)
735
+
736
+
737
+ def resolve_alias(provider_id: str, model: str) -> str:
738
+ return get_lifecycle().resolve_alias(provider_id, model)
739
+
740
+
741
+ def handle_deprecation_error(provider_id: str, model: str,
742
+ status_code: int, response_text: str) -> RecoveryResult:
743
+ return get_lifecycle().handle_deprecation_error(
744
+ provider_id, model, status_code, response_text)
745
+
746
+
747
+ def validate_and_chat(config: dict, prompt: str, system_prompt: str = "",
748
+ history: Optional[list] = None) -> str:
749
+ return get_lifecycle().validate_and_chat(config, prompt, system_prompt, history)
750
+
751
+
752
+ def validate_and_stream(config: dict, prompt: str, system_prompt: str = "",
753
+ history: Optional[list] = None):
754
+ return get_lifecycle().validate_and_stream(config, prompt, system_prompt, history)
755
+
756
+
757
+ def check_health(provider_id: str, config: Optional[dict] = None) -> ProviderHealth:
758
+ return get_lifecycle().check_health(provider_id, config)
759
+
760
+
761
+ def ensure_fresh_models(provider_id: str, config: Optional[dict] = None,
762
+ force: bool = False) -> list[str]:
763
+ return get_lifecycle().ensure_fresh_models(provider_id, config, force=force)
764
+
765
+
766
+ def get_deprecated_models(provider_id: str) -> dict[str, Optional[str]]:
767
+ return get_lifecycle().get_deprecated(provider_id)