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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Anthropic Messages API provider."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .base_provider import BaseProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AnthropicProvider(BaseProvider):
|
|
13
|
+
ID = "anthropic"
|
|
14
|
+
NAME = "Anthropic (Claude)"
|
|
15
|
+
NEEDS_KEY = True
|
|
16
|
+
DEFAULT_MODEL = "claude-3-5-sonnet-20240620" # fallback only — resolved from providers.json when available
|
|
17
|
+
API_STYLE = "anthropic"
|
|
18
|
+
supports_tools = True
|
|
19
|
+
supports_reasoning = True
|
|
20
|
+
supports_vision = True
|
|
21
|
+
supports_streaming = True
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _default_url(cls):
|
|
25
|
+
return "https://api.anthropic.com/v1"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def default_model(cls):
|
|
29
|
+
"""Data-driven default from providers.json; DEFAULT_MODEL as fallback."""
|
|
30
|
+
from ..models import manager as _mgr
|
|
31
|
+
return _mgr.default_model_for(cls.ID, cls.DEFAULT_MODEL)
|
|
32
|
+
|
|
33
|
+
def _headers(self):
|
|
34
|
+
return {
|
|
35
|
+
"x-api-key": self.get_api_key(),
|
|
36
|
+
"anthropic-version": "2023-06-01",
|
|
37
|
+
"content-type": "application/json",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def _build_messages(self, history, prompt):
|
|
41
|
+
messages = [{"role": role, "content": text} for role, text in (history or [])]
|
|
42
|
+
messages.append({"role": "user", "content": prompt})
|
|
43
|
+
return messages
|
|
44
|
+
|
|
45
|
+
def fetch_models(self) -> list[str]:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
def connect(self) -> tuple[bool, str, list[str]]:
|
|
49
|
+
base = self.get_base_url()
|
|
50
|
+
if not base:
|
|
51
|
+
return False, "No base URL configured", []
|
|
52
|
+
try:
|
|
53
|
+
resp = requests.get(f"{base}/models", headers=self._headers(), timeout=8)
|
|
54
|
+
if resp.status_code >= 400 and resp.status_code != 404:
|
|
55
|
+
if resp.status_code == 401:
|
|
56
|
+
return False, "Invalid API key (HTTP 401)", []
|
|
57
|
+
if resp.status_code == 403:
|
|
58
|
+
return False, "Access forbidden (HTTP 403)", []
|
|
59
|
+
return False, f"HTTP {resp.status_code}", []
|
|
60
|
+
return True, f"Connected to {base}", []
|
|
61
|
+
except requests.exceptions.ConnectionError:
|
|
62
|
+
return False, f"Cannot reach {base}", []
|
|
63
|
+
except requests.exceptions.Timeout:
|
|
64
|
+
return False, "Connection timed out", []
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return False, f"Connection failed: {e}"
|
|
67
|
+
|
|
68
|
+
def chat(self, prompt, system_prompt="", history=None) -> str:
|
|
69
|
+
base = self.get_base_url()
|
|
70
|
+
if not base:
|
|
71
|
+
return "AI not configured."
|
|
72
|
+
try:
|
|
73
|
+
payload = {
|
|
74
|
+
"model": self.get_model(),
|
|
75
|
+
"max_tokens": 1024,
|
|
76
|
+
"system": system_prompt or "",
|
|
77
|
+
"messages": self._build_messages(history, prompt),
|
|
78
|
+
}
|
|
79
|
+
temp = self.config.get("temperature")
|
|
80
|
+
if temp is not None:
|
|
81
|
+
payload["temperature"] = temp
|
|
82
|
+
top_p = self.config.get("top_p")
|
|
83
|
+
if top_p is not None:
|
|
84
|
+
payload["top_p"] = top_p
|
|
85
|
+
resp = requests.post(f"{base}/messages", headers=self._headers(),
|
|
86
|
+
json=payload, timeout=30)
|
|
87
|
+
self._raise_for_status(resp)
|
|
88
|
+
return resp.json()["content"][0]["text"]
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return f"Error: {e}"
|
|
91
|
+
|
|
92
|
+
def stream(self, prompt, system_prompt="", history=None):
|
|
93
|
+
base = self.get_base_url()
|
|
94
|
+
if not base:
|
|
95
|
+
yield "AI not configured."
|
|
96
|
+
return
|
|
97
|
+
try:
|
|
98
|
+
payload = {
|
|
99
|
+
"model": self.get_model(),
|
|
100
|
+
"max_tokens": 1024,
|
|
101
|
+
"system": system_prompt or "",
|
|
102
|
+
"messages": self._build_messages(history, prompt),
|
|
103
|
+
"stream": True,
|
|
104
|
+
}
|
|
105
|
+
temp = self.config.get("temperature")
|
|
106
|
+
if temp is not None:
|
|
107
|
+
payload["temperature"] = temp
|
|
108
|
+
top_p = self.config.get("top_p")
|
|
109
|
+
if top_p is not None:
|
|
110
|
+
payload["top_p"] = top_p
|
|
111
|
+
resp = requests.post(f"{base}/messages", headers=self._headers(),
|
|
112
|
+
json=payload, timeout=60, stream=True)
|
|
113
|
+
self._raise_for_status(resp)
|
|
114
|
+
for line in resp.iter_lines(decode_unicode=True):
|
|
115
|
+
if not line or not line.startswith("data:"):
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
obj = json.loads(line[5:].strip())
|
|
119
|
+
except ValueError:
|
|
120
|
+
continue
|
|
121
|
+
if obj.get("type") == "content_block_delta":
|
|
122
|
+
piece = (obj.get("delta") or {}).get("text")
|
|
123
|
+
if piece:
|
|
124
|
+
yield piece
|
|
125
|
+
except Exception as e:
|
|
126
|
+
yield f"Error: {e}"
|
|
127
|
+
|
|
128
|
+
def vision(self, prompt, image_b64, mime) -> str:
|
|
129
|
+
base = self.get_base_url()
|
|
130
|
+
if not base:
|
|
131
|
+
return "AI not configured."
|
|
132
|
+
try:
|
|
133
|
+
payload = {
|
|
134
|
+
"model": self.get_model(),
|
|
135
|
+
"max_tokens": 1024,
|
|
136
|
+
"system": ("You are CAT AI. Describe and analyze "
|
|
137
|
+
"the attached image precisely."),
|
|
138
|
+
"messages": [{"role": "user", "content": [
|
|
139
|
+
{"type": "image", "source": {
|
|
140
|
+
"type": "base64", "media_type": mime, "data": image_b64}},
|
|
141
|
+
{"type": "text", "text": prompt},
|
|
142
|
+
]}],
|
|
143
|
+
}
|
|
144
|
+
resp = requests.post(f"{base}/messages", headers=self._headers(),
|
|
145
|
+
json=payload, timeout=60)
|
|
146
|
+
self._raise_for_status(resp)
|
|
147
|
+
return resp.json()["content"][0]["text"]
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return f"Vision error: {e}"
|
|
150
|
+
|
|
151
|
+
def health_check(self) -> dict:
|
|
152
|
+
start = time.time()
|
|
153
|
+
ok, msg, models = self.connect()
|
|
154
|
+
elapsed = time.time() - start
|
|
155
|
+
return {
|
|
156
|
+
"provider": self.ID,
|
|
157
|
+
"status": "ok" if ok else "error",
|
|
158
|
+
"latency_ms": round(elapsed * 1000),
|
|
159
|
+
"message": msg,
|
|
160
|
+
"base_url": self.get_base_url(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _raise_for_status(resp):
|
|
165
|
+
if resp.status_code >= 400:
|
|
166
|
+
try:
|
|
167
|
+
body = resp.json()
|
|
168
|
+
err = (body.get("error", {}).get("message")
|
|
169
|
+
or str(body)[:300])
|
|
170
|
+
except Exception:
|
|
171
|
+
err = resp.text[:300] if resp.text else resp.reason
|
|
172
|
+
raise RuntimeError(f"HTTP {resp.status_code}: {err}")
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Provider Auto-Update System
|
|
3
|
+
|
|
4
|
+
This module automatically fetches and updates AI provider information
|
|
5
|
+
from provider APIs and a central registry. It ensures CAT always has
|
|
6
|
+
the latest models and providers available.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Auto-discovery of new models from provider APIs
|
|
10
|
+
- Periodic background updates
|
|
11
|
+
- Fallback to cached data when offline
|
|
12
|
+
- Support for custom provider endpoints
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import threading
|
|
19
|
+
import logging
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
from typing import Dict, List, Optional, Any
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
_LOG = logging.getLogger("cct.auto_update")
|
|
25
|
+
|
|
26
|
+
# Configuration
|
|
27
|
+
UPDATE_INTERVAL_HOURS = 24 # Check for updates every 24 hours
|
|
28
|
+
CACHE_DIR = os.path.join(os.path.expanduser("~"), ".cct", "cache", "providers")
|
|
29
|
+
PROVIDERS_CACHE_FILE = os.path.join(CACHE_DIR, "providers_cache.json")
|
|
30
|
+
LAST_UPDATE_FILE = os.path.join(CACHE_DIR, "last_update.json")
|
|
31
|
+
CUSTOM_PROVIDERS_FILE = os.path.join(os.path.expanduser("~"), ".cct", "custom_providers.json")
|
|
32
|
+
|
|
33
|
+
# Central registry URL (could be a GitHub raw URL or custom server)
|
|
34
|
+
CENTRAL_REGISTRY_URL = "https://raw.githubusercontent.com/cct-ai/providers/main/providers.json"
|
|
35
|
+
|
|
36
|
+
# Ensure cache directory exists
|
|
37
|
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProviderAutoUpdater:
|
|
41
|
+
"""Handles automatic updates of AI provider information."""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self._update_lock = threading.Lock()
|
|
45
|
+
self._background_thread = None
|
|
46
|
+
self._stop_event = threading.Event()
|
|
47
|
+
self._providers_data = None
|
|
48
|
+
self._last_update = None
|
|
49
|
+
|
|
50
|
+
def start_background_updates(self):
|
|
51
|
+
"""Start background update thread."""
|
|
52
|
+
if self._background_thread and self._background_thread.is_alive():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
self._stop_event.clear()
|
|
56
|
+
self._background_thread = threading.Thread(
|
|
57
|
+
target=self._update_loop,
|
|
58
|
+
daemon=True,
|
|
59
|
+
name="cct-provider-updater"
|
|
60
|
+
)
|
|
61
|
+
self._background_thread.start()
|
|
62
|
+
_LOG.info("Started background provider updater")
|
|
63
|
+
|
|
64
|
+
def stop_background_updates(self):
|
|
65
|
+
"""Stop background update thread."""
|
|
66
|
+
self._stop_event.set()
|
|
67
|
+
if self._background_thread:
|
|
68
|
+
self._background_thread.join(timeout=5)
|
|
69
|
+
_LOG.info("Stopped background provider updater")
|
|
70
|
+
|
|
71
|
+
def _update_loop(self):
|
|
72
|
+
"""Background loop that checks for updates periodically."""
|
|
73
|
+
while not self._stop_event.is_set():
|
|
74
|
+
try:
|
|
75
|
+
self.check_and_update()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
_LOG.error(f"Error in update loop: {e}")
|
|
78
|
+
|
|
79
|
+
# Wait for next update interval or stop event
|
|
80
|
+
self._stop_event.wait(UPDATE_INTERVAL_HOURS * 3600)
|
|
81
|
+
|
|
82
|
+
def check_and_update(self, force: bool = False) -> bool:
|
|
83
|
+
"""Check for updates and apply if available.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
force: Force update even if recently checked
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if updates were applied, False otherwise
|
|
90
|
+
"""
|
|
91
|
+
with self._update_lock:
|
|
92
|
+
try:
|
|
93
|
+
# Check if we need to update
|
|
94
|
+
if not force and not self._needs_update():
|
|
95
|
+
_LOG.info("Provider data is up to date")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
_LOG.info("Checking for provider updates...")
|
|
99
|
+
|
|
100
|
+
# Try to fetch from central registry
|
|
101
|
+
new_data = self._fetch_central_registry()
|
|
102
|
+
|
|
103
|
+
if new_data:
|
|
104
|
+
# Merge with local custom providers
|
|
105
|
+
custom_providers = self._load_custom_providers()
|
|
106
|
+
if custom_providers:
|
|
107
|
+
new_data = self._merge_providers(new_data, custom_providers)
|
|
108
|
+
|
|
109
|
+
# Save to cache
|
|
110
|
+
self._save_to_cache(new_data)
|
|
111
|
+
|
|
112
|
+
# Update in-memory data
|
|
113
|
+
self._providers_data = new_data
|
|
114
|
+
self._last_update = datetime.now()
|
|
115
|
+
|
|
116
|
+
# Save last update timestamp
|
|
117
|
+
self._save_last_update()
|
|
118
|
+
|
|
119
|
+
_LOG.info(f"Updated providers: {len(new_data.get('providers', []))} providers")
|
|
120
|
+
return True
|
|
121
|
+
else:
|
|
122
|
+
_LOG.warning("Failed to fetch central registry, using cached data")
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
_LOG.error(f"Error checking for updates: {e}")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def _needs_update(self) -> bool:
|
|
130
|
+
"""Check if we need to update based on last update time."""
|
|
131
|
+
try:
|
|
132
|
+
if os.path.exists(LAST_UPDATE_FILE):
|
|
133
|
+
with open(LAST_UPDATE_FILE, "r") as f:
|
|
134
|
+
data = json.load(f)
|
|
135
|
+
last_str = data.get("last_update")
|
|
136
|
+
if last_str:
|
|
137
|
+
last_time = datetime.fromisoformat(last_str)
|
|
138
|
+
if datetime.now() - last_time < timedelta(hours=UPDATE_INTERVAL_HOURS):
|
|
139
|
+
return False
|
|
140
|
+
return True
|
|
141
|
+
except Exception:
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
def _fetch_central_registry(self) -> Optional[Dict]:
|
|
145
|
+
"""Fetch provider data from central registry."""
|
|
146
|
+
try:
|
|
147
|
+
import requests
|
|
148
|
+
|
|
149
|
+
# Try GitHub raw URL first
|
|
150
|
+
resp = requests.get(CENTRAL_REGISTRY_URL, timeout=10)
|
|
151
|
+
if resp.status_code == 200:
|
|
152
|
+
return resp.json()
|
|
153
|
+
|
|
154
|
+
# Fallback to other sources
|
|
155
|
+
fallback_urls = [
|
|
156
|
+
"https://raw.githubusercontent.com/cct-ai/providers/main/providers.json",
|
|
157
|
+
"https://cdn.jsdelivr.net/gh/cct-ai/providers@main/providers.json",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
for url in fallback_urls:
|
|
161
|
+
try:
|
|
162
|
+
resp = requests.get(url, timeout=10)
|
|
163
|
+
if resp.status_code == 200:
|
|
164
|
+
return resp.json()
|
|
165
|
+
except Exception:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
_LOG.error(f"Error fetching central registry: {e}")
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def _load_custom_providers(self) -> List[Dict]:
|
|
175
|
+
"""Load custom providers from local file."""
|
|
176
|
+
try:
|
|
177
|
+
if os.path.exists(CUSTOM_PROVIDERS_FILE):
|
|
178
|
+
with open(CUSTOM_PROVIDERS_FILE, "r", encoding="utf-8") as f:
|
|
179
|
+
data = json.load(f)
|
|
180
|
+
return data.get("providers", [])
|
|
181
|
+
except Exception as e:
|
|
182
|
+
_LOG.error(f"Error loading custom providers: {e}")
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
def _merge_providers(self, central_data: Dict, custom_providers: List[Dict]) -> Dict:
|
|
186
|
+
"""Merge central registry data with custom providers."""
|
|
187
|
+
providers = central_data.get("providers", [])
|
|
188
|
+
existing_ids = {p.get("id") for p in providers}
|
|
189
|
+
|
|
190
|
+
for custom in custom_providers:
|
|
191
|
+
if custom.get("id") not in existing_ids:
|
|
192
|
+
providers.append(custom)
|
|
193
|
+
|
|
194
|
+
central_data["providers"] = providers
|
|
195
|
+
return central_data
|
|
196
|
+
|
|
197
|
+
def _save_to_cache(self, data: Dict):
|
|
198
|
+
"""Save provider data to cache file."""
|
|
199
|
+
try:
|
|
200
|
+
with open(PROVIDERS_CACHE_FILE, "w", encoding="utf-8") as f:
|
|
201
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
_LOG.error(f"Error saving to cache: {e}")
|
|
204
|
+
|
|
205
|
+
def _save_last_update(self):
|
|
206
|
+
"""Save last update timestamp."""
|
|
207
|
+
try:
|
|
208
|
+
with open(LAST_UPDATE_FILE, "w") as f:
|
|
209
|
+
json.dump({"last_update": datetime.now().isoformat()}, f)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
_LOG.error(f"Error saving last update: {e}")
|
|
212
|
+
|
|
213
|
+
def get_providers(self, force_refresh: bool = False) -> Dict:
|
|
214
|
+
"""Get provider data, loading from cache if needed.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
force_refresh: Force refresh from cache/network
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dictionary with providers data
|
|
221
|
+
"""
|
|
222
|
+
if self._providers_data and not force_refresh:
|
|
223
|
+
return self._providers_data
|
|
224
|
+
|
|
225
|
+
# Try to load from cache first
|
|
226
|
+
try:
|
|
227
|
+
if os.path.exists(PROVIDERS_CACHE_FILE):
|
|
228
|
+
with open(PROVIDERS_CACHE_FILE, "r", encoding="utf-8") as f:
|
|
229
|
+
self._providers_data = json.load(f)
|
|
230
|
+
return self._providers_data
|
|
231
|
+
except Exception as e:
|
|
232
|
+
_LOG.error(f"Error loading from cache: {e}")
|
|
233
|
+
|
|
234
|
+
# Return empty data if nothing available
|
|
235
|
+
return {"version": 1, "providers": []}
|
|
236
|
+
|
|
237
|
+
def add_custom_provider(self, provider: Dict) -> bool:
|
|
238
|
+
"""Add a custom provider to the local configuration.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
provider: Provider configuration dictionary
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if added successfully, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
custom_providers = self._load_custom_providers()
|
|
248
|
+
|
|
249
|
+
# Check if provider already exists
|
|
250
|
+
existing_ids = {p.get("id") for p in custom_providers}
|
|
251
|
+
if provider.get("id") in existing_ids:
|
|
252
|
+
_LOG.warning(f"Provider {provider.get('id')} already exists")
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# Add timestamp
|
|
256
|
+
provider["last_updated"] = datetime.now().isoformat()
|
|
257
|
+
custom_providers.append(provider)
|
|
258
|
+
|
|
259
|
+
# Save to file
|
|
260
|
+
with open(CUSTOM_PROVIDERS_FILE, "w", encoding="utf-8") as f:
|
|
261
|
+
json.dump({"providers": custom_providers}, f, indent=2, ensure_ascii=False)
|
|
262
|
+
|
|
263
|
+
_LOG.info(f"Added custom provider: {provider.get('id')}")
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
_LOG.error(f"Error adding custom provider: {e}")
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
def remove_custom_provider(self, provider_id: str) -> bool:
|
|
271
|
+
"""Remove a custom provider from the local configuration.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
provider_id: ID of the provider to remove
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
True if removed successfully, False otherwise
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
custom_providers = self._load_custom_providers()
|
|
281
|
+
original_count = len(custom_providers)
|
|
282
|
+
|
|
283
|
+
# Remove provider
|
|
284
|
+
custom_providers = [p for p in custom_providers if p.get("id") != provider_id]
|
|
285
|
+
|
|
286
|
+
if len(custom_providers) == original_count:
|
|
287
|
+
_LOG.warning(f"Provider {provider_id} not found")
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
# Save to file
|
|
291
|
+
with open(CUSTOM_PROVIDERS_FILE, "w", encoding="utf-8") as f:
|
|
292
|
+
json.dump({"providers": custom_providers}, f, indent=2, ensure_ascii=False)
|
|
293
|
+
|
|
294
|
+
_LOG.info(f"Removed custom provider: {provider_id}")
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
_LOG.error(f"Error removing custom provider: {e}")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def discover_provider_models(self, provider_id: str, base_url: str,
|
|
302
|
+
api_style: str = "openai") -> List[str]:
|
|
303
|
+
"""Discover available models from a provider's API.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
provider_id: Provider identifier
|
|
307
|
+
base_url: API base URL
|
|
308
|
+
api_style: API style (openai, anthropic, gemini, ollama)
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of available model names
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
import requests
|
|
315
|
+
|
|
316
|
+
models = []
|
|
317
|
+
|
|
318
|
+
if api_style == "ollama":
|
|
319
|
+
# Ollama uses /api/tags
|
|
320
|
+
resp = requests.get(f"{base_url}/api/tags", timeout=10)
|
|
321
|
+
if resp.status_code == 200:
|
|
322
|
+
data = resp.json()
|
|
323
|
+
models = [m.get("name", "") for m in data.get("models", [])]
|
|
324
|
+
|
|
325
|
+
elif api_style == "openai":
|
|
326
|
+
# OpenAI-compatible uses /v1/models
|
|
327
|
+
resp = requests.get(f"{base_url}/v1/models", timeout=10)
|
|
328
|
+
if resp.status_code == 200:
|
|
329
|
+
data = resp.json()
|
|
330
|
+
models = [m.get("id", "") for m in data.get("data", [])]
|
|
331
|
+
|
|
332
|
+
elif api_style == "anthropic":
|
|
333
|
+
# Anthropic doesn't have a public models endpoint
|
|
334
|
+
# Use known models list
|
|
335
|
+
models = [
|
|
336
|
+
"claude-4-fable-5.1",
|
|
337
|
+
"claude-sonnet-4-20250514",
|
|
338
|
+
"claude-opus-4-20250514",
|
|
339
|
+
"claude-3-7-sonnet-20250219",
|
|
340
|
+
"claude-3-5-sonnet-20241022",
|
|
341
|
+
"claude-3-5-haiku-20241022",
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
elif api_style == "gemini":
|
|
345
|
+
# Gemini uses specific endpoint
|
|
346
|
+
resp = requests.get(
|
|
347
|
+
f"{base_url}/models",
|
|
348
|
+
timeout=10
|
|
349
|
+
)
|
|
350
|
+
if resp.status_code == 200:
|
|
351
|
+
data = resp.json()
|
|
352
|
+
models = [m.get("name", "") for m in data.get("models", [])]
|
|
353
|
+
|
|
354
|
+
_LOG.info(f"Discovered {len(models)} models for {provider_id}")
|
|
355
|
+
return sorted(set(models))
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
_LOG.error(f"Error discovering models for {provider_id}: {e}")
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# Global updater instance
|
|
363
|
+
_updater = None
|
|
364
|
+
_updater_lock = threading.Lock()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def get_updater() -> ProviderAutoUpdater:
|
|
368
|
+
"""Get the global provider auto-updater instance."""
|
|
369
|
+
global _updater
|
|
370
|
+
with _updater_lock:
|
|
371
|
+
if _updater is None:
|
|
372
|
+
_updater = ProviderAutoUpdater()
|
|
373
|
+
return _updater
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def start_auto_updates():
|
|
377
|
+
"""Start the auto-update system."""
|
|
378
|
+
updater = get_updater()
|
|
379
|
+
updater.start_background_updates()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def stop_auto_updates():
|
|
383
|
+
"""Stop the auto-update system."""
|
|
384
|
+
updater = get_updater()
|
|
385
|
+
updater.stop_background_updates()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def force_update() -> bool:
|
|
389
|
+
"""Force an immediate update check."""
|
|
390
|
+
updater = get_updater()
|
|
391
|
+
return updater.check_and_update(force=True)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def get_latest_providers() -> Dict:
|
|
395
|
+
"""Get the latest provider data."""
|
|
396
|
+
updater = get_updater()
|
|
397
|
+
return updater.get_providers(force_refresh=True)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def add_custom_provider(provider: Dict) -> bool:
|
|
401
|
+
"""Add a custom provider."""
|
|
402
|
+
updater = get_updater()
|
|
403
|
+
return updater.add_custom_provider(provider)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def remove_custom_provider(provider_id: str) -> bool:
|
|
407
|
+
"""Remove a custom provider."""
|
|
408
|
+
updater = get_updater()
|
|
409
|
+
return updater.remove_custom_provider(provider_id)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def discover_models(provider_id: str, base_url: str,
|
|
413
|
+
api_style: str = "openai") -> List[str]:
|
|
414
|
+
"""Discover models from a provider."""
|
|
415
|
+
updater = get_updater()
|
|
416
|
+
return updater.discover_provider_models(provider_id, base_url, api_style)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Abstract base class for all AI providers in CCT."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseProvider(ABC):
|
|
8
|
+
"""Every provider implements this interface. Add a new one by
|
|
9
|
+
subclassing and registering it in ProviderManager."""
|
|
10
|
+
|
|
11
|
+
ID: str = ""
|
|
12
|
+
NAME: str = ""
|
|
13
|
+
NEEDS_KEY: bool = True
|
|
14
|
+
DEFAULT_MODEL: str = ""
|
|
15
|
+
API_STYLE: str = "openai"
|
|
16
|
+
|
|
17
|
+
# ── Capability flags (override in subclasses) ─────────────────────
|
|
18
|
+
supports_model_listing: bool = True
|
|
19
|
+
supports_streaming: bool = True
|
|
20
|
+
supports_tools: bool = False
|
|
21
|
+
supports_reasoning: bool = False
|
|
22
|
+
supports_vision: bool = False
|
|
23
|
+
supports_audio: bool = False
|
|
24
|
+
supports_images: bool = False
|
|
25
|
+
supports_embeddings: bool = False
|
|
26
|
+
supports_reranking: bool = False
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: dict):
|
|
29
|
+
self.config = config
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def connect(self) -> tuple[bool, str, list[str]]:
|
|
33
|
+
"""Test connectivity. Returns (ok, message, available_models)."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def fetch_models(self) -> list[str]:
|
|
37
|
+
"""GET /models or equivalent. Returns list of model IDs or []."""
|
|
38
|
+
|
|
39
|
+
def fetch_models_paginated(self, cancel_event=None) -> list[str]:
|
|
40
|
+
"""Fetch models with pagination support. Override for providers
|
|
41
|
+
with paginated APIs. Default implementation delegates to fetch_models().
|
|
42
|
+
`cancel_event` is a threading.Event; check periodically and return
|
|
43
|
+
early if set."""
|
|
44
|
+
if cancel_event and cancel_event.is_set():
|
|
45
|
+
return []
|
|
46
|
+
return self.fetch_models()
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def chat(self, prompt: str, system_prompt: str = "",
|
|
50
|
+
history: Optional[list] = None) -> str:
|
|
51
|
+
"""Non-streaming chat completion."""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def stream(self, prompt: str, system_prompt: str = "",
|
|
55
|
+
history: Optional[list] = None):
|
|
56
|
+
"""Streaming generator yielding text fragments."""
|
|
57
|
+
|
|
58
|
+
def embeddings(self, texts: list[str]) -> list[list[float]]:
|
|
59
|
+
raise NotImplementedError(f"{self.ID} does not support embeddings")
|
|
60
|
+
|
|
61
|
+
def vision(self, prompt: str, image_b64: str, mime: str) -> str:
|
|
62
|
+
raise NotImplementedError(f"{self.ID} does not support vision")
|
|
63
|
+
|
|
64
|
+
def health_check(self) -> dict:
|
|
65
|
+
"""Return status info: latency, version, etc."""
|
|
66
|
+
return {"provider": self.ID, "status": "unknown"}
|
|
67
|
+
|
|
68
|
+
def get_base_url(self) -> str:
|
|
69
|
+
return (self.config.get("base_url") or
|
|
70
|
+
self.config.get("api_url") or "").rstrip("/")
|
|
71
|
+
|
|
72
|
+
def get_api_key(self) -> str:
|
|
73
|
+
return self.config.get("api_key", "")
|
|
74
|
+
|
|
75
|
+
def get_model(self) -> str:
|
|
76
|
+
return self.config.get("model") or self.default_model()
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def default_model(cls) -> str:
|
|
80
|
+
"""Data-driven default; overridden by provider classes that resolve
|
|
81
|
+
from providers.json. Falls back to the hardcoded DEFAULT_MODEL."""
|
|
82
|
+
return cls.DEFAULT_MODEL
|
|
83
|
+
|
|
84
|
+
def get_extra_headers(self) -> dict:
|
|
85
|
+
return dict(self.config.get("extra_headers", {}))
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def to_provider_dict(cls) -> dict:
|
|
89
|
+
"""Return the standard provider info dict for this class."""
|
|
90
|
+
return {
|
|
91
|
+
"id": cls.ID,
|
|
92
|
+
"name": cls.NAME,
|
|
93
|
+
"url": cls._default_url() if hasattr(cls, "_default_url") else "",
|
|
94
|
+
"api_style": cls.API_STYLE,
|
|
95
|
+
"needs_key": cls.NEEDS_KEY,
|
|
96
|
+
"default_model": cls.default_model(),
|
|
97
|
+
"models": [],
|
|
98
|
+
"models_cache": [],
|
|
99
|
+
"features": [],
|
|
100
|
+
"openai_compat": cls.API_STYLE == "openai",
|
|
101
|
+
"desc": "",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def dispose(self):
|
|
105
|
+
"""Cleanup hook when provider is removed."""
|