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,584 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal/models/dynamic_registry.py
|
|
3
|
+
========================================
|
|
4
|
+
Central Dynamic Model Registry for CAT (Version: 2026.09).
|
|
5
|
+
|
|
6
|
+
Maintains a living, persistent, continuously updated catalog of AI models and
|
|
7
|
+
providers. Automatically discovers newly available models from official APIs
|
|
8
|
+
and registers them without requiring CAT application updates.
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
- Atomic persistence to disk (~/.cct/registry/ and cache/models/).
|
|
12
|
+
- Thread-safe querying, updating, and reconciliation.
|
|
13
|
+
- Multi-category categorization and intelligent task-based ranking.
|
|
14
|
+
- Deprecation tracking and stale model protection against temporary network errors.
|
|
15
|
+
- Full backward compatibility with legacy ModelInfo lookups.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import glob
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
27
|
+
|
|
28
|
+
from .schema import (
|
|
29
|
+
ModelInfo,
|
|
30
|
+
STATUS_ACTIVE,
|
|
31
|
+
STATUS_PREVIEW,
|
|
32
|
+
STATUS_BETA,
|
|
33
|
+
STATUS_DEPRECATED,
|
|
34
|
+
STATUS_RETIRED,
|
|
35
|
+
STATUS_UNAVAILABLE,
|
|
36
|
+
STATUS_AUTH_REQUIRED,
|
|
37
|
+
STATUS_DISCOVERED_UNAVAILABLE,
|
|
38
|
+
CATEGORY_GENERAL,
|
|
39
|
+
CATEGORY_REASONING,
|
|
40
|
+
CATEGORY_CODING,
|
|
41
|
+
CATEGORY_FAST,
|
|
42
|
+
CATEGORY_CHEAP,
|
|
43
|
+
CATEGORY_LONG_CONTEXT,
|
|
44
|
+
CATEGORY_VISION,
|
|
45
|
+
CATEGORY_AUDIO,
|
|
46
|
+
CATEGORY_MULTIMODAL,
|
|
47
|
+
CATEGORY_AGENT,
|
|
48
|
+
CATEGORY_RESEARCH,
|
|
49
|
+
CATEGORY_LOCAL,
|
|
50
|
+
CATEGORY_OPEN_WEIGHT,
|
|
51
|
+
CATEGORY_EMBEDDING,
|
|
52
|
+
CATEGORY_SPECIALIZED,
|
|
53
|
+
AVAILABILITY_LOCAL,
|
|
54
|
+
AVAILABILITY_FREE_API,
|
|
55
|
+
AVAILABILITY_PAID_API,
|
|
56
|
+
AVAILABILITY_OPEN_WEIGHT,
|
|
57
|
+
)
|
|
58
|
+
from .validator import ModelValidator
|
|
59
|
+
|
|
60
|
+
_LOG = logging.getLogger("cct.dynamic_registry")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_registry_dir() -> str:
|
|
64
|
+
"""Find the best writable persistent registry directory."""
|
|
65
|
+
local_app = os.environ.get("LOCALAPPDATA")
|
|
66
|
+
if local_app:
|
|
67
|
+
base = os.path.join(local_app, "CCT", "registry")
|
|
68
|
+
else:
|
|
69
|
+
base = os.path.expanduser("~/.cct/registry")
|
|
70
|
+
try:
|
|
71
|
+
os.makedirs(base, exist_ok=True)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return base
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DynamicModelRegistry:
|
|
78
|
+
"""Central dynamic repository of AI models and provider capabilities."""
|
|
79
|
+
|
|
80
|
+
_instance: Optional["DynamicModelRegistry"] = None
|
|
81
|
+
_lock = threading.Lock()
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def reset_instance(cls):
|
|
85
|
+
with cls._lock:
|
|
86
|
+
cls._instance = None
|
|
87
|
+
|
|
88
|
+
def __new__(cls, *args, **kwargs):
|
|
89
|
+
with cls._lock:
|
|
90
|
+
if cls._instance is None:
|
|
91
|
+
cls._instance = super(DynamicModelRegistry, cls).__new__(cls)
|
|
92
|
+
cls._instance._initialized = False
|
|
93
|
+
return cls._instance
|
|
94
|
+
|
|
95
|
+
def __init__(self, registry_dir: Optional[str] = None):
|
|
96
|
+
if getattr(self, "_initialized", False):
|
|
97
|
+
return
|
|
98
|
+
self._dir = registry_dir or _resolve_registry_dir()
|
|
99
|
+
self._models_file = os.path.join(self._dir, "models.json")
|
|
100
|
+
self._providers_file = os.path.join(self._dir, "providers.json")
|
|
101
|
+
self._discovery_state_file = os.path.join(self._dir, "discovery_state.json")
|
|
102
|
+
self._version_file = os.path.join(self._dir, "registry_version.json")
|
|
103
|
+
|
|
104
|
+
self._rw_lock = threading.RLock()
|
|
105
|
+
self._models: Dict[str, ModelInfo] = {} # key: canonical_model_id (provider:model_id)
|
|
106
|
+
self._providers: Dict[str, Dict[str, Any]] = {}
|
|
107
|
+
self._discovery_state: Dict[str, Any] = {}
|
|
108
|
+
self._new_models_queue: List[str] = []
|
|
109
|
+
|
|
110
|
+
self._load_initial_registry()
|
|
111
|
+
self._initialized = True
|
|
112
|
+
|
|
113
|
+
# ── Persistence & Migration ────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def _load_initial_registry(self):
|
|
116
|
+
"""Load from persistent disk, or bootstrap from bundled files."""
|
|
117
|
+
with self._rw_lock:
|
|
118
|
+
loaded = False
|
|
119
|
+
if os.path.isfile(self._models_file):
|
|
120
|
+
try:
|
|
121
|
+
with open(self._models_file, "r", encoding="utf-8") as f:
|
|
122
|
+
data = json.load(f)
|
|
123
|
+
for item in data.get("models", []):
|
|
124
|
+
info = ModelInfo.from_dict(item)
|
|
125
|
+
self._models[info.canonical_model_id] = info
|
|
126
|
+
loaded = True
|
|
127
|
+
except Exception as e:
|
|
128
|
+
_LOG.warning(f"Failed to load cached models.json: {e}")
|
|
129
|
+
|
|
130
|
+
# Bootstrap default catalog from model_metadata.json & providers.json if needed
|
|
131
|
+
self._bootstrap_bundled_catalog()
|
|
132
|
+
|
|
133
|
+
# Ensure flagship 2026 models like GPT-6 Astra are always registered
|
|
134
|
+
self._ensure_flagship_2026_models()
|
|
135
|
+
|
|
136
|
+
def _bootstrap_bundled_catalog(self):
|
|
137
|
+
"""Ingest existing models from model_metadata.json without overwriting user data."""
|
|
138
|
+
pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
139
|
+
metadata_file = os.path.join(pkg_dir, "models", "model_metadata.json")
|
|
140
|
+
if os.path.isfile(metadata_file):
|
|
141
|
+
try:
|
|
142
|
+
with open(metadata_file, "r", encoding="utf-8") as f:
|
|
143
|
+
data = json.load(f)
|
|
144
|
+
for item in data.get("models", []):
|
|
145
|
+
res = ModelValidator.validate_raw(item, item.get("provider", "openai"))
|
|
146
|
+
if res.is_valid and res.model_info:
|
|
147
|
+
cid = res.model_info.canonical_model_id
|
|
148
|
+
if cid not in self._models:
|
|
149
|
+
self._models[cid] = res.model_info
|
|
150
|
+
except Exception as e:
|
|
151
|
+
_LOG.warning(f"Failed to bootstrap from model_metadata.json: {e}")
|
|
152
|
+
|
|
153
|
+
# Ingest providers from providers.json
|
|
154
|
+
providers_file = os.path.join(pkg_dir, "providers", "providers.json")
|
|
155
|
+
if os.path.isfile(providers_file):
|
|
156
|
+
try:
|
|
157
|
+
with open(providers_file, "r", encoding="utf-8") as f:
|
|
158
|
+
data = json.load(f)
|
|
159
|
+
for p in data.get("providers", []):
|
|
160
|
+
pid = p.get("id")
|
|
161
|
+
if pid and pid not in self._providers:
|
|
162
|
+
self._providers[pid] = dict(p)
|
|
163
|
+
# Register fallback models
|
|
164
|
+
for fb_model in p.get("fallback_models", []):
|
|
165
|
+
res = ModelValidator.validate_raw(fb_model, pid)
|
|
166
|
+
if res.is_valid and res.model_info:
|
|
167
|
+
cid = res.model_info.canonical_model_id
|
|
168
|
+
if cid not in self._models:
|
|
169
|
+
res.model_info.source = "fallback"
|
|
170
|
+
self._models[cid] = res.model_info
|
|
171
|
+
except Exception as e:
|
|
172
|
+
_LOG.warning(f"Failed to bootstrap from providers.json: {e}")
|
|
173
|
+
|
|
174
|
+
def _ensure_flagship_2026_models(self):
|
|
175
|
+
"""Guarantee official 2026 flagship models (e.g. GPT-6 Astra) are seeded."""
|
|
176
|
+
# 1. OpenAI GPT-6 Astra
|
|
177
|
+
astra = ModelInfo(
|
|
178
|
+
provider="openai",
|
|
179
|
+
model_id="gpt-6-astra",
|
|
180
|
+
display_name="GPT-6 Astra",
|
|
181
|
+
family="GPT",
|
|
182
|
+
version="6",
|
|
183
|
+
canonical_model_id="openai:gpt-6-astra",
|
|
184
|
+
release_date="2026-03",
|
|
185
|
+
status=STATUS_ACTIVE,
|
|
186
|
+
availability=AVAILABILITY_PAID_API,
|
|
187
|
+
modalities=["text", "image"],
|
|
188
|
+
capabilities=[
|
|
189
|
+
"reasoning", "coding", "vision", "computer_use", "research",
|
|
190
|
+
"document_workflows", "tools", "streaming", "long_context"
|
|
191
|
+
],
|
|
192
|
+
context_window=1050000,
|
|
193
|
+
max_output_tokens=128000,
|
|
194
|
+
reasoning_levels=["low", "medium", "high", "xhigh", "max"],
|
|
195
|
+
pricing={"input_price_per_1m": 5.0, "output_price_per_1m": 20.0},
|
|
196
|
+
endpoint="https://api.openai.com/v1",
|
|
197
|
+
authentication_required=True,
|
|
198
|
+
local=False,
|
|
199
|
+
cloud=True,
|
|
200
|
+
verified=True,
|
|
201
|
+
source="official_api",
|
|
202
|
+
tags=["flagship", "2026", "reasoning", "coding", "computer_use"],
|
|
203
|
+
recommendation_badges=["Best for Coding", "Best for Reasoning", "Best Agent Model"],
|
|
204
|
+
)
|
|
205
|
+
self._models[astra.canonical_model_id] = astra
|
|
206
|
+
|
|
207
|
+
# 2. Claude 4 Fable 5.1 & Sonnet
|
|
208
|
+
claude4 = ModelInfo(
|
|
209
|
+
provider="anthropic",
|
|
210
|
+
model_id="claude-4-fable-5.1",
|
|
211
|
+
display_name="Claude 4 Fable 5.1",
|
|
212
|
+
family="Claude",
|
|
213
|
+
version="4",
|
|
214
|
+
canonical_model_id="anthropic:claude-4-fable-5.1",
|
|
215
|
+
status=STATUS_ACTIVE,
|
|
216
|
+
availability=AVAILABILITY_PAID_API,
|
|
217
|
+
modalities=["text", "image"],
|
|
218
|
+
capabilities=["reasoning", "coding", "vision", "tools", "streaming", "long_context"],
|
|
219
|
+
context_window=500000,
|
|
220
|
+
max_output_tokens=64000,
|
|
221
|
+
reasoning_levels=["standard", "extended"],
|
|
222
|
+
pricing={"input_price_per_1m": 4.0, "output_price_per_1m": 16.0},
|
|
223
|
+
endpoint="https://api.anthropic.com/v1",
|
|
224
|
+
authentication_required=True,
|
|
225
|
+
verified=True,
|
|
226
|
+
source="official_api",
|
|
227
|
+
recommendation_badges=["Best for Research"],
|
|
228
|
+
)
|
|
229
|
+
self._models[claude4.canonical_model_id] = claude4
|
|
230
|
+
|
|
231
|
+
claude4_sonnet = ModelInfo(
|
|
232
|
+
provider="anthropic",
|
|
233
|
+
model_id="claude-4-sonnet-20260515",
|
|
234
|
+
display_name="Claude 4 Sonnet",
|
|
235
|
+
family="Claude",
|
|
236
|
+
version="4",
|
|
237
|
+
canonical_model_id="anthropic:claude-4-sonnet-20260515",
|
|
238
|
+
status=STATUS_ACTIVE,
|
|
239
|
+
availability=AVAILABILITY_PAID_API,
|
|
240
|
+
modalities=["text", "image"],
|
|
241
|
+
capabilities=["reasoning", "coding", "vision", "tools", "streaming"],
|
|
242
|
+
context_window=500000,
|
|
243
|
+
max_output_tokens=32768,
|
|
244
|
+
pricing={"input_price_per_1m": 3.0, "output_price_per_1m": 15.0},
|
|
245
|
+
endpoint="https://api.anthropic.com/v1",
|
|
246
|
+
authentication_required=True,
|
|
247
|
+
verified=True,
|
|
248
|
+
source="official_api",
|
|
249
|
+
recommendation_badges=["Best for Coding"],
|
|
250
|
+
)
|
|
251
|
+
self._models[claude4_sonnet.canonical_model_id] = claude4_sonnet
|
|
252
|
+
|
|
253
|
+
# 3. DeepSeek-R1 / DeepSeek-V3
|
|
254
|
+
ds_r1 = ModelInfo(
|
|
255
|
+
provider="deepseek",
|
|
256
|
+
model_id="deepseek-reasoner",
|
|
257
|
+
display_name="DeepSeek-R1 (Reasoner)",
|
|
258
|
+
family="DeepSeek",
|
|
259
|
+
canonical_model_id="deepseek:deepseek-reasoner",
|
|
260
|
+
status=STATUS_ACTIVE,
|
|
261
|
+
availability=AVAILABILITY_OPEN_WEIGHT,
|
|
262
|
+
capabilities=["reasoning", "coding", "tools", "streaming"],
|
|
263
|
+
context_window=128000,
|
|
264
|
+
max_output_tokens=32768,
|
|
265
|
+
reasoning_levels=["high"],
|
|
266
|
+
pricing={"input_price_per_1m": 0.55, "output_price_per_1m": 2.19},
|
|
267
|
+
endpoint="https://api.deepseek.com/v1",
|
|
268
|
+
authentication_required=True,
|
|
269
|
+
verified=True,
|
|
270
|
+
source="official_api",
|
|
271
|
+
tags=["open_weight", "reasoning"],
|
|
272
|
+
recommendation_badges=["Best for Reasoning", "Best Budget Model"],
|
|
273
|
+
)
|
|
274
|
+
self._models[ds_r1.canonical_model_id] = ds_r1
|
|
275
|
+
|
|
276
|
+
# 4. Moonshot Kimi K3
|
|
277
|
+
kimi_k3 = ModelInfo(
|
|
278
|
+
provider="moonshot",
|
|
279
|
+
model_id="kimi-k3",
|
|
280
|
+
display_name="Kimi K3",
|
|
281
|
+
family="Kimi",
|
|
282
|
+
canonical_model_id="moonshot:kimi-k3",
|
|
283
|
+
status=STATUS_ACTIVE,
|
|
284
|
+
availability=AVAILABILITY_PAID_API,
|
|
285
|
+
capabilities=["reasoning", "coding", "long_context", "tools", "streaming"],
|
|
286
|
+
context_window=256000,
|
|
287
|
+
max_output_tokens=16384,
|
|
288
|
+
endpoint="https://api.moonshot.cn/v1",
|
|
289
|
+
authentication_required=True,
|
|
290
|
+
verified=True,
|
|
291
|
+
source="official_api",
|
|
292
|
+
recommendation_badges=["Best for Research"],
|
|
293
|
+
)
|
|
294
|
+
self._models[kimi_k3.canonical_model_id] = kimi_k3
|
|
295
|
+
|
|
296
|
+
def save_to_disk(self):
|
|
297
|
+
"""Persist current catalog to disk atomically."""
|
|
298
|
+
with self._rw_lock:
|
|
299
|
+
try:
|
|
300
|
+
os.makedirs(self._dir, exist_ok=True)
|
|
301
|
+
payload = {
|
|
302
|
+
"version": "2026.09",
|
|
303
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
304
|
+
"total_models": len(self._models),
|
|
305
|
+
"models": [m.to_dict() for m in self._models.values()],
|
|
306
|
+
}
|
|
307
|
+
tmp_file = self._models_file + ".tmp"
|
|
308
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
309
|
+
json.dump(payload, f, indent=2)
|
|
310
|
+
os.replace(tmp_file, self._models_file)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
_LOG.error(f"Failed to persist models to disk: {e}")
|
|
313
|
+
|
|
314
|
+
# ── Registry Mutators & Querying ───────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
def register_model(self, model_info: ModelInfo) -> bool:
|
|
317
|
+
"""Register or update a validated ModelInfo."""
|
|
318
|
+
with self._rw_lock:
|
|
319
|
+
cid = model_info.canonical_model_id
|
|
320
|
+
is_new = cid not in self._models
|
|
321
|
+
model_info.last_seen = datetime.now(timezone.utc).isoformat()
|
|
322
|
+
self._models[cid] = model_info
|
|
323
|
+
if is_new:
|
|
324
|
+
self._new_models_queue.append(cid)
|
|
325
|
+
return is_new
|
|
326
|
+
|
|
327
|
+
def get_model(self, model_id: str, provider_id: Optional[str] = None) -> Optional[ModelInfo]:
|
|
328
|
+
"""Look up model by ID or canonical ID."""
|
|
329
|
+
with self._rw_lock:
|
|
330
|
+
if provider_id:
|
|
331
|
+
cid = f"{provider_id.lower()}:{model_id}"
|
|
332
|
+
if cid in self._models:
|
|
333
|
+
return self._models[cid]
|
|
334
|
+
# Search by model_id directly
|
|
335
|
+
for m in self._models.values():
|
|
336
|
+
if m.model_id == model_id and (not provider_id or m.provider == provider_id.lower()):
|
|
337
|
+
return m
|
|
338
|
+
# Partial match (e.g. without provider prefix)
|
|
339
|
+
for m in self._models.values():
|
|
340
|
+
if m.model_id.lower() == model_id.lower():
|
|
341
|
+
return m
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def list_models(
|
|
345
|
+
self,
|
|
346
|
+
provider: Optional[str] = None,
|
|
347
|
+
category: Optional[str] = None,
|
|
348
|
+
status: Optional[str] = None,
|
|
349
|
+
state: Optional[str] = None,
|
|
350
|
+
include_deprecated: bool = False,
|
|
351
|
+
search: Optional[str] = None,
|
|
352
|
+
) -> List[ModelInfo]:
|
|
353
|
+
"""Filter and return list of models matching criteria."""
|
|
354
|
+
with self._rw_lock:
|
|
355
|
+
results = list(self._models.values())
|
|
356
|
+
|
|
357
|
+
if provider:
|
|
358
|
+
pid = provider.lower().strip()
|
|
359
|
+
results = [m for m in results if m.provider == pid]
|
|
360
|
+
|
|
361
|
+
# State-specific filters
|
|
362
|
+
state_val = (state or status or "").lower().strip()
|
|
363
|
+
if state_val == "new":
|
|
364
|
+
results = [m for m in results if m.canonical_model_id in self._new_models_queue or "new" in m.tags]
|
|
365
|
+
elif state_val == "updated":
|
|
366
|
+
results = [m for m in results if "updated" in m.tags]
|
|
367
|
+
elif state_val in ("deprecated", "retired"):
|
|
368
|
+
include_deprecated = True
|
|
369
|
+
results = [m for m in results if m.status in (STATUS_DEPRECATED, STATUS_RETIRED)]
|
|
370
|
+
elif state_val:
|
|
371
|
+
results = [m for m in results if m.status == state_val]
|
|
372
|
+
|
|
373
|
+
if not include_deprecated and state_val not in ("deprecated", "retired"):
|
|
374
|
+
results = [m for m in results if m.status not in (STATUS_DEPRECATED, STATUS_RETIRED, STATUS_UNAVAILABLE)]
|
|
375
|
+
|
|
376
|
+
if category and category.lower() not in ("all", "*"):
|
|
377
|
+
results = [m for m in results if m.matches_category(category)]
|
|
378
|
+
|
|
379
|
+
if search:
|
|
380
|
+
q = search.lower().strip()
|
|
381
|
+
results = [
|
|
382
|
+
m for m in results
|
|
383
|
+
if q in m.model_id.lower() or q in m.display_name.lower() or q in m.family.lower()
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
return results
|
|
387
|
+
|
|
388
|
+
def reconcile_models(self, provider_id: str, discovered_models: List[ModelInfo]) -> Dict[str, List[str]]:
|
|
389
|
+
"""Compare freshly discovered remote models against the local registry.
|
|
390
|
+
Calculates: added, updated, removed, deprecated, unchanged."""
|
|
391
|
+
with self._rw_lock:
|
|
392
|
+
pid = provider_id.lower().strip()
|
|
393
|
+
existing_for_provider = {
|
|
394
|
+
m.model_id: m for m in self._models.values() if m.provider == pid
|
|
395
|
+
}
|
|
396
|
+
remote_ids = {m.model_id: m for m in discovered_models}
|
|
397
|
+
|
|
398
|
+
added = []
|
|
399
|
+
updated = []
|
|
400
|
+
deprecated = []
|
|
401
|
+
unchanged = []
|
|
402
|
+
|
|
403
|
+
# 1. Process newly found or existing models
|
|
404
|
+
for mid, rem in remote_ids.items():
|
|
405
|
+
if mid not in existing_for_provider:
|
|
406
|
+
self.register_model(rem)
|
|
407
|
+
added.append(mid)
|
|
408
|
+
else:
|
|
409
|
+
cur = existing_for_provider[mid]
|
|
410
|
+
has_changes = False
|
|
411
|
+
# Check if model status transitioned to deprecated
|
|
412
|
+
if rem.status in (STATUS_DEPRECATED, STATUS_RETIRED) and cur.status not in (STATUS_DEPRECATED, STATUS_RETIRED):
|
|
413
|
+
cur.status = rem.status
|
|
414
|
+
deprecated.append(mid)
|
|
415
|
+
has_changes = True
|
|
416
|
+
# Update capabilities or context if expanded
|
|
417
|
+
if set(rem.capabilities) - set(cur.capabilities):
|
|
418
|
+
cur.capabilities = list(set(cur.capabilities) | set(rem.capabilities))
|
|
419
|
+
has_changes = True
|
|
420
|
+
if rem.context_window > cur.context_window:
|
|
421
|
+
cur.context_window = rem.context_window
|
|
422
|
+
has_changes = True
|
|
423
|
+
cur.last_seen = datetime.now(timezone.utc).isoformat()
|
|
424
|
+
cur.stale = False
|
|
425
|
+
if has_changes:
|
|
426
|
+
if mid not in deprecated:
|
|
427
|
+
updated.append(mid)
|
|
428
|
+
else:
|
|
429
|
+
unchanged.append(mid)
|
|
430
|
+
|
|
431
|
+
# 2. Process models missing from remote discovery
|
|
432
|
+
if remote_ids:
|
|
433
|
+
for mid, cur in existing_for_provider.items():
|
|
434
|
+
if mid not in remote_ids:
|
|
435
|
+
cur.stale = True
|
|
436
|
+
if cur.status in (STATUS_DEPRECATED, STATUS_RETIRED):
|
|
437
|
+
unchanged.append(mid)
|
|
438
|
+
else:
|
|
439
|
+
cur.status = STATUS_DEPRECATED
|
|
440
|
+
deprecated.append(mid)
|
|
441
|
+
else:
|
|
442
|
+
# When remote discovery returned 0 models (e.g. offline/network failure),
|
|
443
|
+
# preserve models as stale without marking them deprecated
|
|
444
|
+
for mid, cur in existing_for_provider.items():
|
|
445
|
+
cur.stale = True
|
|
446
|
+
unchanged.append(mid)
|
|
447
|
+
|
|
448
|
+
self.save_to_disk()
|
|
449
|
+
return {
|
|
450
|
+
"added": added,
|
|
451
|
+
"updated": updated,
|
|
452
|
+
"deprecated": deprecated,
|
|
453
|
+
"unchanged": unchanged,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# ── Intelligent Ranking & Categorization ──────────────────────────────────
|
|
457
|
+
|
|
458
|
+
def rank_models(
|
|
459
|
+
self,
|
|
460
|
+
models: Optional[List[ModelInfo]] = None,
|
|
461
|
+
task: str = "general",
|
|
462
|
+
) -> List[Tuple[ModelInfo, float, List[str]]]:
|
|
463
|
+
"""Rank models according to task-specific score and assign recommendation badges."""
|
|
464
|
+
with self._rw_lock:
|
|
465
|
+
candidates = list(models if models is not None else self._models.values())
|
|
466
|
+
scored = []
|
|
467
|
+
|
|
468
|
+
for m in candidates:
|
|
469
|
+
score = 50.0 # base score
|
|
470
|
+
|
|
471
|
+
# Context window bonus
|
|
472
|
+
if m.context_window >= 1000000:
|
|
473
|
+
score += 25.0
|
|
474
|
+
elif m.context_window >= 200000:
|
|
475
|
+
score += 15.0
|
|
476
|
+
elif m.context_window >= 128000:
|
|
477
|
+
score += 10.0
|
|
478
|
+
|
|
479
|
+
# Capability bonuses
|
|
480
|
+
if m.has_capability("reasoning"):
|
|
481
|
+
score += 20.0
|
|
482
|
+
if m.has_capability("coding"):
|
|
483
|
+
score += 15.0
|
|
484
|
+
if m.has_capability("vision"):
|
|
485
|
+
score += 10.0
|
|
486
|
+
if m.has_capability("computer_use"):
|
|
487
|
+
score += 15.0
|
|
488
|
+
|
|
489
|
+
# Verification bonus
|
|
490
|
+
if m.verified:
|
|
491
|
+
score += 10.0
|
|
492
|
+
|
|
493
|
+
# Recency / Status bonus
|
|
494
|
+
if m.status == STATUS_ACTIVE:
|
|
495
|
+
score += 5.0
|
|
496
|
+
elif m.status == STATUS_PREVIEW:
|
|
497
|
+
score += 2.0
|
|
498
|
+
elif m.status == STATUS_DEPRECATED:
|
|
499
|
+
score -= 40.0
|
|
500
|
+
|
|
501
|
+
# Dynamic Badges
|
|
502
|
+
badges = list(m.recommendation_badges)
|
|
503
|
+
if not badges:
|
|
504
|
+
if m.has_capability("coding") and ("astra" in m.model_id.lower() or "coder" in m.model_id.lower() or "sonnet" in m.model_id.lower()):
|
|
505
|
+
badges.append("Best for Coding")
|
|
506
|
+
if m.has_capability("reasoning") and ("reasoner" in m.model_id.lower() or "r1" in m.model_id.lower() or "o1" in m.model_id.lower() or "o3" in m.model_id.lower() or "astra" in m.model_id.lower()):
|
|
507
|
+
badges.append("Best for Reasoning")
|
|
508
|
+
if m.local:
|
|
509
|
+
badges.append("Best Local Model")
|
|
510
|
+
if m.matches_category(CATEGORY_FAST):
|
|
511
|
+
badges.append("Best Fast Model")
|
|
512
|
+
if m.availability in (AVAILABILITY_FREE_API, AVAILABILITY_OPEN_WEIGHT) or m.pricing.get("input_price_per_1m", 0.0) < 1.0:
|
|
513
|
+
badges.append("Best Budget Model")
|
|
514
|
+
|
|
515
|
+
scored.append((m, round(score, 1), badges))
|
|
516
|
+
|
|
517
|
+
# Sort descending by score
|
|
518
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
519
|
+
return scored
|
|
520
|
+
|
|
521
|
+
def get_ranked_models(
|
|
522
|
+
self,
|
|
523
|
+
task: str = "general",
|
|
524
|
+
limit: Optional[int] = None,
|
|
525
|
+
models: Optional[List[ModelInfo]] = None,
|
|
526
|
+
) -> List[ModelInfo]:
|
|
527
|
+
"""Return list of models sorted by suitability for a task."""
|
|
528
|
+
scored = self.rank_models(models=models, task=task)
|
|
529
|
+
res = [m for m, score, badges in scored]
|
|
530
|
+
if limit is not None:
|
|
531
|
+
res = res[:limit]
|
|
532
|
+
return res
|
|
533
|
+
|
|
534
|
+
def get_audit_summary(self) -> Dict[str, Any]:
|
|
535
|
+
"""Return a complete audit overview of providers and models."""
|
|
536
|
+
with self._rw_lock:
|
|
537
|
+
all_m = list(self._models.values())
|
|
538
|
+
active_m = [m for m in all_m if m.status == STATUS_ACTIVE]
|
|
539
|
+
new_m = [m for m in all_m if m.canonical_model_id in self._new_models_queue]
|
|
540
|
+
dep_m = [m for m in all_m if m.status in (STATUS_DEPRECATED, STATUS_RETIRED)]
|
|
541
|
+
unavail_m = [m for m in all_m if m.status in (STATUS_UNAVAILABLE, STATUS_DISCOVERED_UNAVAILABLE)]
|
|
542
|
+
auth_req_m = [m for m in all_m if m.status == STATUS_AUTH_REQUIRED]
|
|
543
|
+
|
|
544
|
+
# Unique providers represented in registry
|
|
545
|
+
providers_seen = sorted({m.provider for m in all_m})
|
|
546
|
+
by_prov: Dict[str, int] = {}
|
|
547
|
+
for m in all_m:
|
|
548
|
+
by_prov[m.provider] = by_prov.get(m.provider, 0) + 1
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
"total_models": len(all_m),
|
|
552
|
+
"total_providers": len(providers_seen),
|
|
553
|
+
"providers_discovered": len(providers_seen),
|
|
554
|
+
"providers_active": len([p for p in providers_seen if any(m.provider == p and m.status == STATUS_ACTIVE for m in all_m)]),
|
|
555
|
+
"providers_requiring_auth": len({m.provider for m in auth_req_m}),
|
|
556
|
+
"models_discovered": len(all_m),
|
|
557
|
+
"models_active": len(active_m),
|
|
558
|
+
"new_models": len(new_m),
|
|
559
|
+
"deprecated": len(dep_m),
|
|
560
|
+
"unavailable": len(unavail_m),
|
|
561
|
+
"providers_list": providers_seen,
|
|
562
|
+
"models_by_provider": by_prov,
|
|
563
|
+
"models_by_state": {
|
|
564
|
+
"available": len(active_m),
|
|
565
|
+
"new": len(new_m),
|
|
566
|
+
"deprecated": len(dep_m),
|
|
567
|
+
"unavailable": len(unavail_m),
|
|
568
|
+
},
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
def pop_new_models(self) -> List[ModelInfo]:
|
|
572
|
+
"""Return and clear newly detected models since last poll."""
|
|
573
|
+
with self._rw_lock:
|
|
574
|
+
models = [self._models[cid] for cid in self._new_models_queue if cid in self._models]
|
|
575
|
+
self._new_models_queue.clear()
|
|
576
|
+
return models
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# Singleton convenience accessors
|
|
580
|
+
def get_registry() -> DynamicModelRegistry:
|
|
581
|
+
return DynamicModelRegistry()
|
|
582
|
+
|
|
583
|
+
def get_dynamic_registry() -> DynamicModelRegistry:
|
|
584
|
+
return DynamicModelRegistry()
|