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,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal/providers/discovery_manager.py
|
|
3
|
+
===========================================
|
|
4
|
+
Provider Discovery Manager for Dynamic Model and Provider Discovery.
|
|
5
|
+
|
|
6
|
+
Coordinates adapter-driven live discovery across all supported providers,
|
|
7
|
+
reconciles results against DynamicModelRegistry, and notifies users when
|
|
8
|
+
new models are detected.
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
- Non-blocking asynchronous discovery loops.
|
|
12
|
+
- Fail-safe timeout bounding and graceful error recovery.
|
|
13
|
+
- Offline mode detection and stale cache preservation.
|
|
14
|
+
- Live health status tracking (ONLINE, DEGRADED, OFFLINE, UNKNOWN).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import concurrent.futures
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from typing import Dict, List, Optional, Any, Callable
|
|
24
|
+
|
|
25
|
+
from .adapters import get_adapter, list_supported_adapters
|
|
26
|
+
from ..models.dynamic_registry import get_registry, DynamicModelRegistry
|
|
27
|
+
from ..models.schema import ModelInfo
|
|
28
|
+
|
|
29
|
+
_LOG = logging.getLogger("cct.discovery_manager")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProviderDiscoveryManager:
|
|
33
|
+
"""Manages continuous background model discovery and reconciliation."""
|
|
34
|
+
|
|
35
|
+
_instance: Optional["ProviderDiscoveryManager"] = None
|
|
36
|
+
_lock = threading.Lock()
|
|
37
|
+
|
|
38
|
+
def __new__(cls, *args, **kwargs):
|
|
39
|
+
with cls._lock:
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
cls._instance = super(ProviderDiscoveryManager, cls).__new__(cls)
|
|
42
|
+
cls._instance._initialized = False
|
|
43
|
+
return cls._instance
|
|
44
|
+
|
|
45
|
+
def __init__(self, registry: Optional[DynamicModelRegistry] = None):
|
|
46
|
+
if getattr(self, "_initialized", False):
|
|
47
|
+
return
|
|
48
|
+
self.registry = registry or get_registry()
|
|
49
|
+
self._provider_status: Dict[str, Dict[str, Any]] = {}
|
|
50
|
+
self._notification_hooks: List[Callable[[List[ModelInfo]], None]] = []
|
|
51
|
+
self._bg_thread: Optional[threading.Thread] = None
|
|
52
|
+
self._stop_event = threading.Event()
|
|
53
|
+
self._initialized = True
|
|
54
|
+
|
|
55
|
+
# ── Notification Hooks ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def add_notification_hook(self, hook: Callable[[List[ModelInfo]], None]):
|
|
58
|
+
"""Register a callback for when new models are detected."""
|
|
59
|
+
if hook not in self._notification_hooks:
|
|
60
|
+
self._notification_hooks.append(hook)
|
|
61
|
+
|
|
62
|
+
register_notification_hook = add_notification_hook
|
|
63
|
+
|
|
64
|
+
def _notify_new_models(self, new_models: List[ModelInfo]):
|
|
65
|
+
for hook in self._notification_hooks:
|
|
66
|
+
try:
|
|
67
|
+
hook(new_models)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
_LOG.warning(f"Error executing discovery notification hook: {e}")
|
|
70
|
+
|
|
71
|
+
# ── Single Provider Discovery ──────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def discover_provider(
|
|
74
|
+
self,
|
|
75
|
+
provider_id: str,
|
|
76
|
+
config: Optional[Dict[str, Any]] = None,
|
|
77
|
+
timeout: float = 6.0,
|
|
78
|
+
) -> List[ModelInfo]:
|
|
79
|
+
"""Run discovery for a single provider and reconcile with registry."""
|
|
80
|
+
pid = provider_id.lower().strip()
|
|
81
|
+
adapter = get_adapter(pid, config)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
discovered = adapter.discover_models(timeout=timeout)
|
|
85
|
+
if discovered:
|
|
86
|
+
diff = self.registry.reconcile_models(pid, discovered)
|
|
87
|
+
self._provider_status[pid] = {
|
|
88
|
+
"status": "online",
|
|
89
|
+
"models_count": len(discovered),
|
|
90
|
+
"last_checked": time.time(),
|
|
91
|
+
"diff": diff,
|
|
92
|
+
}
|
|
93
|
+
# Check for newly discovered models
|
|
94
|
+
new_models = self.registry.pop_new_models()
|
|
95
|
+
if new_models:
|
|
96
|
+
self._notify_new_models(new_models)
|
|
97
|
+
return discovered
|
|
98
|
+
else:
|
|
99
|
+
self._provider_status[pid] = {
|
|
100
|
+
"status": "degraded",
|
|
101
|
+
"models_count": 0,
|
|
102
|
+
"last_checked": time.time(),
|
|
103
|
+
"message": "Zero models returned",
|
|
104
|
+
}
|
|
105
|
+
except Exception as e:
|
|
106
|
+
_LOG.warning(f"Discovery failed for {pid}: {e}")
|
|
107
|
+
self._provider_status[pid] = {
|
|
108
|
+
"status": "offline",
|
|
109
|
+
"last_checked": time.time(),
|
|
110
|
+
"error": str(e),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Fallback to previously cached registry models
|
|
114
|
+
return self.registry.list_models(provider=pid)
|
|
115
|
+
|
|
116
|
+
# ── Parallel Discovery Across All Providers ────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def discover_all(
|
|
119
|
+
self,
|
|
120
|
+
provider_configs: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
121
|
+
timeout_per_provider: float = 6.0,
|
|
122
|
+
max_workers: int = 8,
|
|
123
|
+
) -> Dict[str, List[ModelInfo]]:
|
|
124
|
+
"""Discover models across all supported providers in parallel."""
|
|
125
|
+
supported = list_supported_adapters()
|
|
126
|
+
configs = provider_configs or {}
|
|
127
|
+
results: Dict[str, List[ModelInfo]] = {}
|
|
128
|
+
|
|
129
|
+
# Prioritize key providers first
|
|
130
|
+
priority_pids = [
|
|
131
|
+
"openai", "anthropic", "gemini", "deepseek", "alibaba",
|
|
132
|
+
"moonshot", "zhipu", "ollama", "xai", "mistral", "minimax"
|
|
133
|
+
]
|
|
134
|
+
all_pids = list(priority_pids) + [p for p in supported.keys() if p not in priority_pids]
|
|
135
|
+
|
|
136
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
137
|
+
future_to_pid = {
|
|
138
|
+
executor.submit(
|
|
139
|
+
self.discover_provider, pid, configs.get(pid), timeout_per_provider
|
|
140
|
+
): pid
|
|
141
|
+
for pid in all_pids
|
|
142
|
+
}
|
|
143
|
+
for future in concurrent.futures.as_completed(future_to_pid):
|
|
144
|
+
pid = future_to_pid[future]
|
|
145
|
+
try:
|
|
146
|
+
results[pid] = future.result()
|
|
147
|
+
except Exception as e:
|
|
148
|
+
results[pid] = []
|
|
149
|
+
_LOG.warning(f"Exception during discovery for {pid}: {e}")
|
|
150
|
+
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
# ── Background Worker ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
def start_background_discovery(self, interval_hours: float = 24.0, delay_startup: float = 1.0):
|
|
156
|
+
"""Launch background discovery thread."""
|
|
157
|
+
if self._bg_thread and self._bg_thread.is_alive():
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
self._stop_event.clear()
|
|
161
|
+
|
|
162
|
+
def _loop():
|
|
163
|
+
# Initial brief delay so CAT startup finishes instantly
|
|
164
|
+
self._stop_event.wait(delay_startup)
|
|
165
|
+
if self._stop_event.is_set():
|
|
166
|
+
return
|
|
167
|
+
try:
|
|
168
|
+
self.discover_all()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
_LOG.warning(f"Background initial discovery error: {e}")
|
|
171
|
+
|
|
172
|
+
# Periodic loop
|
|
173
|
+
while not self._stop_event.is_set():
|
|
174
|
+
self._stop_event.wait(interval_hours * 3600.0)
|
|
175
|
+
if self._stop_event.is_set():
|
|
176
|
+
break
|
|
177
|
+
try:
|
|
178
|
+
self.discover_all()
|
|
179
|
+
except Exception as e:
|
|
180
|
+
_LOG.warning(f"Background periodic discovery error: {e}")
|
|
181
|
+
|
|
182
|
+
self._bg_thread = threading.Thread(
|
|
183
|
+
target=_loop,
|
|
184
|
+
daemon=True,
|
|
185
|
+
name="cct-discovery-manager",
|
|
186
|
+
)
|
|
187
|
+
self._bg_thread.start()
|
|
188
|
+
|
|
189
|
+
def stop_background_discovery(self):
|
|
190
|
+
"""Stop background discovery thread."""
|
|
191
|
+
self._stop_event.set()
|
|
192
|
+
|
|
193
|
+
# ── Status Inspection ──────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def get_provider_status(self, provider_id: str) -> Dict[str, Any]:
|
|
196
|
+
pid = provider_id.lower().strip()
|
|
197
|
+
return self._provider_status.get(pid, {"status": "unknown", "provider": pid})
|
|
198
|
+
|
|
199
|
+
def get_all_provider_statuses(self) -> Dict[str, Dict[str, Any]]:
|
|
200
|
+
return dict(self._provider_status)
|
|
201
|
+
|
|
202
|
+
get_all_provider_status = get_all_provider_statuses
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Singleton convenience accessor
|
|
206
|
+
def get_discovery_manager() -> ProviderDiscoveryManager:
|
|
207
|
+
return ProviderDiscoveryManager()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Google Gemini 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 GeminiProvider(BaseProvider):
|
|
13
|
+
ID = "gemini"
|
|
14
|
+
NAME = "Google Gemini"
|
|
15
|
+
NEEDS_KEY = True
|
|
16
|
+
DEFAULT_MODEL = "gemini-1.5-flash-latest" # fallback only — resolved from providers.json when available
|
|
17
|
+
API_STYLE = "gemini"
|
|
18
|
+
supports_tools = True
|
|
19
|
+
supports_reasoning = True
|
|
20
|
+
supports_vision = True
|
|
21
|
+
supports_streaming = True
|
|
22
|
+
supports_audio = True
|
|
23
|
+
supports_embeddings = True
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def _default_url(cls):
|
|
27
|
+
return "https://generativelanguage.googleapis.com/v1beta"
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def default_model(cls):
|
|
31
|
+
"""Data-driven default from providers.json; DEFAULT_MODEL as fallback."""
|
|
32
|
+
from ..models import manager as _mgr
|
|
33
|
+
return _mgr.default_model_for(cls.ID, cls.DEFAULT_MODEL)
|
|
34
|
+
|
|
35
|
+
def _key_suffix(self):
|
|
36
|
+
return f"?key={self.get_api_key()}"
|
|
37
|
+
|
|
38
|
+
def _build_contents(self, history, prompt):
|
|
39
|
+
contents = []
|
|
40
|
+
for role, text in (history or []):
|
|
41
|
+
r = "model" if role == "assistant" else "user"
|
|
42
|
+
contents.append({"role": r, "parts": [{"text": text}]})
|
|
43
|
+
contents.append({"role": "user", "parts": [{"text": prompt}]})
|
|
44
|
+
return contents
|
|
45
|
+
|
|
46
|
+
def fetch_models(self) -> list[str]:
|
|
47
|
+
base = self.get_base_url()
|
|
48
|
+
key = self.get_api_key()
|
|
49
|
+
if not base or not key:
|
|
50
|
+
return []
|
|
51
|
+
try:
|
|
52
|
+
resp = requests.get(f"{base}/models{self._key_suffix()}", timeout=10)
|
|
53
|
+
if resp.status_code >= 400:
|
|
54
|
+
return []
|
|
55
|
+
out = []
|
|
56
|
+
for m in resp.json().get("models", []):
|
|
57
|
+
methods = m.get("supportedGenerationMethods", [])
|
|
58
|
+
if not methods or "generateContent" in methods:
|
|
59
|
+
out.append(m.get("name", "").split("/")[-1])
|
|
60
|
+
return sorted(set(n for n in out if n))
|
|
61
|
+
except Exception:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
def connect(self) -> tuple[bool, str, list[str]]:
|
|
65
|
+
models = self.fetch_models()
|
|
66
|
+
if models:
|
|
67
|
+
return True, f"Connected \u2014 {len(models)} model(s) visible", models
|
|
68
|
+
return False, "Could not connect \u2014 check API key", []
|
|
69
|
+
|
|
70
|
+
def chat(self, prompt, system_prompt="", history=None) -> str:
|
|
71
|
+
base = self.get_base_url()
|
|
72
|
+
if not base:
|
|
73
|
+
return "AI not configured."
|
|
74
|
+
model = self.get_model()
|
|
75
|
+
try:
|
|
76
|
+
url = f"{base}/models/{model}:generateContent{self._key_suffix()}"
|
|
77
|
+
payload = {
|
|
78
|
+
"contents": self._build_contents(history, prompt),
|
|
79
|
+
"systemInstruction": {"parts": [{"text": system_prompt or ""}]},
|
|
80
|
+
}
|
|
81
|
+
temp = self.config.get("temperature")
|
|
82
|
+
top_p = self.config.get("top_p")
|
|
83
|
+
if temp is not None or top_p is not None:
|
|
84
|
+
gen = {}
|
|
85
|
+
if temp is not None:
|
|
86
|
+
gen["temperature"] = temp
|
|
87
|
+
if top_p is not None:
|
|
88
|
+
gen["topP"] = top_p
|
|
89
|
+
payload["generationConfig"] = gen
|
|
90
|
+
resp = requests.post(url, json=payload, timeout=30)
|
|
91
|
+
self._raise_for_status(resp)
|
|
92
|
+
data = resp.json()
|
|
93
|
+
return data["candidates"][0]["content"]["parts"][0]["text"]
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return f"Error: {e}"
|
|
96
|
+
|
|
97
|
+
def stream(self, prompt, system_prompt="", history=None):
|
|
98
|
+
base = self.get_base_url()
|
|
99
|
+
if not base:
|
|
100
|
+
yield "AI not configured."
|
|
101
|
+
return
|
|
102
|
+
model = self.get_model()
|
|
103
|
+
try:
|
|
104
|
+
url = (f"{base}/models/{model}:streamGenerateContent"
|
|
105
|
+
f"?alt=sse{self._key_suffix()}")
|
|
106
|
+
payload = {
|
|
107
|
+
"contents": self._build_contents(history, prompt),
|
|
108
|
+
"systemInstruction": {"parts": [{"text": system_prompt or ""}]},
|
|
109
|
+
}
|
|
110
|
+
temp = self.config.get("temperature")
|
|
111
|
+
top_p = self.config.get("top_p")
|
|
112
|
+
if temp is not None or top_p is not None:
|
|
113
|
+
gen = {}
|
|
114
|
+
if temp is not None:
|
|
115
|
+
gen["temperature"] = temp
|
|
116
|
+
if top_p is not None:
|
|
117
|
+
gen["topP"] = top_p
|
|
118
|
+
payload["generationConfig"] = gen
|
|
119
|
+
resp = requests.post(url, json=payload, timeout=60, stream=True)
|
|
120
|
+
self._raise_for_status(resp)
|
|
121
|
+
for line in resp.iter_lines(decode_unicode=True):
|
|
122
|
+
if not line or not line.startswith("data:"):
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
obj = json.loads(line[5:].strip())
|
|
126
|
+
except ValueError:
|
|
127
|
+
continue
|
|
128
|
+
candidates = obj.get("candidates") or []
|
|
129
|
+
if candidates:
|
|
130
|
+
parts = (candidates[0].get("content") or {}).get("parts") or []
|
|
131
|
+
for part in parts:
|
|
132
|
+
piece = part.get("text")
|
|
133
|
+
if piece:
|
|
134
|
+
yield piece
|
|
135
|
+
except Exception as e:
|
|
136
|
+
yield f"Error: {e}"
|
|
137
|
+
|
|
138
|
+
def vision(self, prompt, image_b64, mime) -> str:
|
|
139
|
+
base = self.get_base_url()
|
|
140
|
+
if not base:
|
|
141
|
+
return "AI not configured."
|
|
142
|
+
model = self.get_model()
|
|
143
|
+
try:
|
|
144
|
+
url = f"{base}/models/{model}:generateContent{self._key_suffix()}"
|
|
145
|
+
payload = {
|
|
146
|
+
"contents": [{"role": "user", "parts": [
|
|
147
|
+
{"text": prompt},
|
|
148
|
+
{"inline_data": {"mime_type": mime, "data": image_b64}},
|
|
149
|
+
]}],
|
|
150
|
+
}
|
|
151
|
+
resp = requests.post(url, json=payload, timeout=60)
|
|
152
|
+
self._raise_for_status(resp)
|
|
153
|
+
return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return f"Vision error: {e}"
|
|
156
|
+
|
|
157
|
+
def health_check(self) -> dict:
|
|
158
|
+
start = time.time()
|
|
159
|
+
models = self.fetch_models()
|
|
160
|
+
elapsed = time.time() - start
|
|
161
|
+
return {
|
|
162
|
+
"provider": self.ID,
|
|
163
|
+
"status": "ok" if models else "error",
|
|
164
|
+
"latency_ms": round(elapsed * 1000),
|
|
165
|
+
"models_count": len(models),
|
|
166
|
+
"base_url": self.get_base_url(),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _raise_for_status(resp):
|
|
171
|
+
if resp.status_code >= 400:
|
|
172
|
+
try:
|
|
173
|
+
body = resp.json()
|
|
174
|
+
err = (body.get("error", {}).get("message")
|
|
175
|
+
or str(body)[:300])
|
|
176
|
+
except Exception:
|
|
177
|
+
err = resp.text[:300] if resp.text else resp.reason
|
|
178
|
+
raise RuntimeError(f"HTTP {resp.status_code}: {err}")
|