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,254 @@
1
+ """OpenAI-compatible provider — works with OpenAI, Groq, OpenRouter,
2
+ DeepSeek, Together AI, Perplexity, and any other /v1 endpoint."""
3
+
4
+ import json
5
+ import time
6
+ from typing import Optional
7
+
8
+ import requests
9
+
10
+ from .base_provider import BaseProvider
11
+
12
+
13
+ class OpenAIProvider(BaseProvider):
14
+ ID = "openai"
15
+ NAME = "OpenAI"
16
+ NEEDS_KEY = True
17
+ DEFAULT_MODEL = "gpt-4o-mini" # fallback only — resolved from providers.json when available
18
+ API_STYLE = "openai"
19
+ supports_tools = True
20
+ supports_reasoning = True
21
+ supports_vision = True
22
+ supports_audio = True
23
+ supports_images = True
24
+ supports_embeddings = True
25
+ supports_streaming = True
26
+
27
+ @classmethod
28
+ def _default_url(cls):
29
+ return "https://api.openai.com/v1"
30
+
31
+ @classmethod
32
+ def default_model(cls):
33
+ """Data-driven default from providers.json; DEFAULT_MODEL as fallback."""
34
+ from ..models import manager as _mgr
35
+ return _mgr.default_model_for(cls.ID, cls.DEFAULT_MODEL)
36
+
37
+ def _headers(self):
38
+ h = {"Content-Type": "application/json"}
39
+ key = self.get_api_key()
40
+ if key:
41
+ h["Authorization"] = f"Bearer {key}"
42
+ h.update(self.get_extra_headers())
43
+ return h
44
+
45
+ def _build_messages(self, system_prompt, history, prompt):
46
+ messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
47
+ for role, text in (history or []):
48
+ messages.append({"role": role, "content": text})
49
+ messages.append({"role": "user", "content": prompt})
50
+ return messages
51
+
52
+ def fetch_models(self) -> list[str]:
53
+ return self._fetch_models_paginated()
54
+
55
+ def fetch_models_paginated(self, cancel_event=None) -> list[str]:
56
+ return self._fetch_models_paginated(cancel_event)
57
+
58
+ def _fetch_models_paginated(self, cancel_event=None) -> list[str]:
59
+ base = self.get_base_url()
60
+ if not base:
61
+ return []
62
+ all_models = []
63
+ after = None
64
+ limit = 100
65
+ try:
66
+ while True:
67
+ if cancel_event and cancel_event.is_set():
68
+ return all_models or []
69
+ url = f"{base}/models"
70
+ params = {"limit": limit}
71
+ if after:
72
+ params["after"] = after
73
+ resp = requests.get(url, headers=self._headers(),
74
+ params=params, timeout=10)
75
+ if resp.status_code >= 400:
76
+ return all_models or []
77
+ data = resp.json()
78
+ items = data.get("data", [])
79
+ for m in items:
80
+ if isinstance(m, dict) and m.get("id"):
81
+ all_models.append(m["id"])
82
+ # Check for pagination
83
+ if "has_more" in data:
84
+ if not data.get("has_more"):
85
+ break
86
+ after = None
87
+ if items:
88
+ after = items[-1].get("id")
89
+ if not after:
90
+ break
91
+ return sorted(set(all_models))
92
+ except Exception:
93
+ return all_models or []
94
+
95
+ def connect(self) -> tuple[bool, str, list[str]]:
96
+ models = self.fetch_models()
97
+ if models:
98
+ return True, f"Connected \u2014 {len(models)} model(s) visible", models
99
+ base = self.get_base_url()
100
+ if not base:
101
+ return False, "No base URL configured", []
102
+ try:
103
+ resp = requests.get(f"{base}/models", headers=self._headers(), timeout=8)
104
+ if resp.status_code == 401:
105
+ return False, "Invalid or missing API key (HTTP 401)", []
106
+ if resp.status_code == 403:
107
+ return False, "Access forbidden (HTTP 403)", []
108
+ if resp.status_code >= 400:
109
+ return False, f"Server returned HTTP {resp.status_code}", []
110
+ return True, f"Connected to {base}", []
111
+ except requests.exceptions.ConnectionError:
112
+ return False, f"Cannot reach {base} \u2014 check URL or internet", []
113
+ except requests.exceptions.Timeout:
114
+ return False, "Connection timed out", []
115
+ except Exception as e:
116
+ return False, f"Connection failed: {e}", []
117
+
118
+ def chat(self, prompt, system_prompt="", history=None) -> str:
119
+ base = self.get_base_url()
120
+ if not base:
121
+ return "AI not configured \u2014 no base URL set."
122
+ try:
123
+ payload = {
124
+ "model": self.get_model(),
125
+ "messages": self._build_messages(system_prompt, history, prompt),
126
+ }
127
+ temp = self.config.get("temperature")
128
+ if temp is not None:
129
+ payload["temperature"] = temp
130
+ top_p = self.config.get("top_p")
131
+ if top_p is not None:
132
+ payload["top_p"] = top_p
133
+ resp = requests.post(f"{base}/chat/completions",
134
+ headers=self._headers(), json=payload, timeout=30)
135
+ self._raise_for_status(resp)
136
+ data = resp.json()
137
+ return data["choices"][0]["message"]["content"]
138
+ except requests.exceptions.ConnectionError:
139
+ return "Could not reach the AI server."
140
+ except requests.exceptions.Timeout:
141
+ return "The AI request timed out."
142
+ except Exception as e:
143
+ return f"Error: {e}"
144
+
145
+ def stream(self, prompt, system_prompt="", history=None):
146
+ base = self.get_base_url()
147
+ if not base:
148
+ yield "AI not configured \u2014 no base URL set."
149
+ return
150
+ try:
151
+ payload = {
152
+ "model": self.get_model(),
153
+ "messages": self._build_messages(system_prompt, history, prompt),
154
+ "stream": True,
155
+ }
156
+ temp = self.config.get("temperature")
157
+ if temp is not None:
158
+ payload["temperature"] = temp
159
+ top_p = self.config.get("top_p")
160
+ if top_p is not None:
161
+ payload["top_p"] = top_p
162
+ resp = requests.post(f"{base}/chat/completions",
163
+ headers=self._headers(), json=payload,
164
+ timeout=60, stream=True)
165
+ self._raise_for_status(resp)
166
+ for line in resp.iter_lines(decode_unicode=True):
167
+ if not line or not line.startswith("data:"):
168
+ continue
169
+ data_str = line[5:].strip()
170
+ if data_str == "[DONE]":
171
+ break
172
+ try:
173
+ obj = json.loads(data_str)
174
+ except ValueError:
175
+ continue
176
+ choices = obj.get("choices") or []
177
+ if choices:
178
+ piece = (choices[0].get("delta") or {}).get("content")
179
+ if piece:
180
+ yield piece
181
+ except requests.exceptions.ConnectionError:
182
+ yield "Could not reach the AI server."
183
+ except requests.exceptions.Timeout:
184
+ yield "The AI request timed out."
185
+ except Exception as e:
186
+ yield f"Error: {e}"
187
+
188
+ def vision(self, prompt, image_b64, mime) -> str:
189
+ base = self.get_base_url()
190
+ if not base:
191
+ return "AI not configured."
192
+ try:
193
+ payload = {
194
+ "model": self.get_model(),
195
+ "messages": [
196
+ {"role": "system",
197
+ "content": ("You are CAT AI. Describe and analyze "
198
+ "the attached image precisely.")},
199
+ {"role": "user", "content": [
200
+ {"type": "text", "text": prompt},
201
+ {"type": "image_url",
202
+ "image_url": {"url": f"data:{mime};base64,{image_b64}"}},
203
+ ]},
204
+ ],
205
+ }
206
+ resp = requests.post(f"{base}/chat/completions",
207
+ headers=self._headers(), json=payload, timeout=60)
208
+ self._raise_for_status(resp)
209
+ return resp.json()["choices"][0]["message"]["content"]
210
+ except Exception as e:
211
+ return f"Vision error: {e}"
212
+
213
+ def health_check(self) -> dict:
214
+ start = time.time()
215
+ models = self.fetch_models()
216
+ elapsed = time.time() - start
217
+ return {
218
+ "provider": self.ID,
219
+ "status": "ok" if models else "error",
220
+ "latency_ms": round(elapsed * 1000),
221
+ "models_count": len(models),
222
+ "base_url": self.get_base_url(),
223
+ }
224
+
225
+ @staticmethod
226
+ def _raise_for_status(resp):
227
+ if resp.status_code >= 400:
228
+ try:
229
+ body = resp.json()
230
+ err = (body.get("error", {}).get("message")
231
+ or body.get("error", {}).get("status")
232
+ or str(body)[:300])
233
+ except Exception:
234
+ err = resp.text[:300] if resp.text else resp.reason
235
+ raise RuntimeError(f"HTTP {resp.status_code}: {err}")
236
+
237
+
238
+ class CustomOpenAIProvider(OpenAIProvider):
239
+ """For user-defined OpenAI-compatible endpoints."""
240
+
241
+ ID = "custom_openai"
242
+ NAME = "OpenAI Compatible"
243
+ NEEDS_KEY = True
244
+ DEFAULT_MODEL = ""
245
+ API_STYLE = "openai"
246
+ supports_tools = True
247
+ supports_vision = True
248
+ supports_streaming = True
249
+ supports_embeddings = True
250
+ supports_reasoning = False # depends on backend
251
+
252
+ @classmethod
253
+ def _default_url(cls):
254
+ return "https://api.openai.com/v1"