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,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal/models/validator.py
|
|
3
|
+
=================================
|
|
4
|
+
8-Stage Model Validation Pipeline for CAT's Dynamic Model Registry.
|
|
5
|
+
|
|
6
|
+
Pipeline:
|
|
7
|
+
1. IDENTITY CHECK — Clean identifier, no control chars or invalid formats.
|
|
8
|
+
2. PROVIDER CHECK — Provider exists and is recognized in CAT ecosystem.
|
|
9
|
+
3. MODEL-ID CHECK — Matches provider naming conventions; canonical deduplication.
|
|
10
|
+
4. ENDPOINT CHECK — Verifies valid, safe API URL structure.
|
|
11
|
+
5. AUTH CHECK — Determines authentication requirements safely (no key logging).
|
|
12
|
+
6. CAPABILITY CHECK— Discovers and assigns verified capabilities.
|
|
13
|
+
7. AVAILABILITY CHECK — Confirms deployment availability state.
|
|
14
|
+
8. REGISTRATION GATE — Produces clean ModelInfo or marks DISCOVERED_BUT_UNAVAILABLE.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import urllib.parse
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Optional, List, Tuple, Dict, Any
|
|
21
|
+
|
|
22
|
+
from .schema import (
|
|
23
|
+
ModelInfo,
|
|
24
|
+
STATUS_ACTIVE,
|
|
25
|
+
STATUS_PREVIEW,
|
|
26
|
+
STATUS_BETA,
|
|
27
|
+
STATUS_DEPRECATED,
|
|
28
|
+
STATUS_UNAVAILABLE,
|
|
29
|
+
STATUS_AUTH_REQUIRED,
|
|
30
|
+
STATUS_DISCOVERED_UNAVAILABLE,
|
|
31
|
+
AVAILABILITY_PAID_API,
|
|
32
|
+
AVAILABILITY_FREE_API,
|
|
33
|
+
AVAILABILITY_LOCAL,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ValidationResult:
|
|
39
|
+
is_valid: bool
|
|
40
|
+
status: str
|
|
41
|
+
rejection_reason: Optional[str] = None
|
|
42
|
+
model_info: Optional[ModelInfo] = None
|
|
43
|
+
warnings: List[str] = None
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
if self.warnings is None:
|
|
47
|
+
self.warnings = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ModelValidator:
|
|
51
|
+
"""Validates discovered models before they enter CAT's active registry."""
|
|
52
|
+
|
|
53
|
+
# Disallowed patterns (malformed IDs, binary garbage, HTML injections)
|
|
54
|
+
INVALID_ID_CHARS = re.compile(r"[\x00-\x1f\x7f<>\"'`;|&$]")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def validate_raw(
|
|
58
|
+
cls,
|
|
59
|
+
raw_model: Any,
|
|
60
|
+
provider_id: str,
|
|
61
|
+
provider_config: Optional[Dict[str, Any]] = None,
|
|
62
|
+
) -> ValidationResult:
|
|
63
|
+
"""Run the full 8-stage validation pipeline on a model candidate."""
|
|
64
|
+
config = provider_config or {}
|
|
65
|
+
warnings = []
|
|
66
|
+
|
|
67
|
+
# ── Stage 1: IDENTITY CHECK ──────────────────────────────────────────
|
|
68
|
+
if not raw_model:
|
|
69
|
+
return ValidationResult(
|
|
70
|
+
is_valid=False,
|
|
71
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
72
|
+
rejection_reason="Empty or null model object.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if isinstance(raw_model, str):
|
|
76
|
+
model_id = raw_model.strip()
|
|
77
|
+
raw_dict = {"model_id": model_id}
|
|
78
|
+
elif isinstance(raw_model, dict):
|
|
79
|
+
raw_dict = dict(raw_model)
|
|
80
|
+
model_id = str(raw_dict.get("id") or raw_dict.get("model_id") or "").strip()
|
|
81
|
+
elif hasattr(raw_model, "to_dict"):
|
|
82
|
+
raw_dict = raw_model.to_dict()
|
|
83
|
+
model_id = str(raw_dict.get("model_id") or raw_dict.get("id") or "").strip()
|
|
84
|
+
else:
|
|
85
|
+
return ValidationResult(
|
|
86
|
+
is_valid=False,
|
|
87
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
88
|
+
rejection_reason=f"Unsupported raw model type: {type(raw_model).__name__}",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not model_id:
|
|
92
|
+
return ValidationResult(
|
|
93
|
+
is_valid=False,
|
|
94
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
95
|
+
rejection_reason="Model has empty identifier.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if model_id in ("*", "default", "none"):
|
|
99
|
+
return ValidationResult(
|
|
100
|
+
is_valid=False,
|
|
101
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
102
|
+
rejection_reason=f"Model ID is an invalid placeholder or wildcard: '{model_id}'",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if cls.INVALID_ID_CHARS.search(model_id):
|
|
106
|
+
return ValidationResult(
|
|
107
|
+
is_valid=False,
|
|
108
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
109
|
+
rejection_reason=f"Model ID contains prohibited characters: '{model_id}'",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# ── Stage 2: PROVIDER CHECK ──────────────────────────────────────────
|
|
113
|
+
pid = str(provider_id or raw_dict.get("provider") or "").strip().lower()
|
|
114
|
+
if not pid:
|
|
115
|
+
return ValidationResult(
|
|
116
|
+
is_valid=False,
|
|
117
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
118
|
+
rejection_reason="Missing provider identifier.",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ── Stage 3: MODEL-ID CHECK & CANONICAL MAPPING ───────────────────────
|
|
122
|
+
canonical_id = cls._derive_canonical_id(model_id, pid)
|
|
123
|
+
|
|
124
|
+
# ── Stage 4: ENDPOINT CHECK ──────────────────────────────────────────
|
|
125
|
+
endpoint = str(
|
|
126
|
+
raw_dict.get("endpoint")
|
|
127
|
+
or config.get("base_url")
|
|
128
|
+
or config.get("api_url")
|
|
129
|
+
or ""
|
|
130
|
+
).strip()
|
|
131
|
+
if endpoint:
|
|
132
|
+
try:
|
|
133
|
+
parsed = urllib.parse.urlparse(endpoint)
|
|
134
|
+
if parsed.scheme not in ("http", "https"):
|
|
135
|
+
return ValidationResult(
|
|
136
|
+
is_valid=False,
|
|
137
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
138
|
+
rejection_reason=f"Insecure endpoint scheme '{parsed.scheme}': must be http or https",
|
|
139
|
+
)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return ValidationResult(
|
|
142
|
+
is_valid=False,
|
|
143
|
+
status=STATUS_DISCOVERED_UNAVAILABLE,
|
|
144
|
+
rejection_reason=f"Malformed endpoint URL: {e}",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ── Stage 5: AUTH CHECK ──────────────────────────────────────────────
|
|
148
|
+
needs_auth = config.get("needs_key", True)
|
|
149
|
+
if pid == "ollama":
|
|
150
|
+
needs_auth = False
|
|
151
|
+
api_key = config.get("api_key")
|
|
152
|
+
|
|
153
|
+
model_status = STATUS_ACTIVE
|
|
154
|
+
if needs_auth and not api_key:
|
|
155
|
+
# Model is known and documented, but user has not configured auth yet
|
|
156
|
+
model_status = STATUS_AUTH_REQUIRED
|
|
157
|
+
|
|
158
|
+
# ── Stage 6: CAPABILITY CHECK ────────────────────────────────────────
|
|
159
|
+
capabilities = cls._infer_capabilities(model_id, pid, raw_dict)
|
|
160
|
+
context_window = int(raw_dict.get("context_window") or raw_dict.get("context_length") or 128000)
|
|
161
|
+
max_output = int(raw_dict.get("max_output_tokens") or 4096)
|
|
162
|
+
reasoning_levels = raw_dict.get("reasoning_levels") or []
|
|
163
|
+
|
|
164
|
+
# Special recognition for 2026 flagship models
|
|
165
|
+
if pid == "openai" and "gpt-6-astra" in model_id.lower():
|
|
166
|
+
if not reasoning_levels:
|
|
167
|
+
reasoning_levels = ["low", "medium", "high", "xhigh", "max"]
|
|
168
|
+
context_window = max(context_window, 1050000)
|
|
169
|
+
max_output = max(max_output, 128000)
|
|
170
|
+
for cap in ("reasoning", "coding", "vision", "computer_use", "research", "document_workflows"):
|
|
171
|
+
if cap not in capabilities:
|
|
172
|
+
capabilities.append(cap)
|
|
173
|
+
|
|
174
|
+
# ── Stage 7: AVAILABILITY CHECK ──────────────────────────────────────
|
|
175
|
+
availability = raw_dict.get("availability") or (
|
|
176
|
+
AVAILABILITY_LOCAL if pid == "ollama" else AVAILABILITY_PAID_API
|
|
177
|
+
)
|
|
178
|
+
if raw_dict.get("free") or raw_dict.get("is_free"):
|
|
179
|
+
availability = AVAILABILITY_FREE_API
|
|
180
|
+
|
|
181
|
+
if raw_dict.get("deprecated") or "deprecated" in model_id.lower():
|
|
182
|
+
model_status = STATUS_DEPRECATED
|
|
183
|
+
elif raw_dict.get("preview") or "preview" in model_id.lower() or "beta" in model_id.lower():
|
|
184
|
+
model_status = STATUS_PREVIEW
|
|
185
|
+
|
|
186
|
+
# ── Stage 8: REGISTRATION GATE ───────────────────────────────────────
|
|
187
|
+
display_name = raw_dict.get("display_name") or cls._clean_display_name(model_id)
|
|
188
|
+
|
|
189
|
+
model_info = ModelInfo(
|
|
190
|
+
provider=pid,
|
|
191
|
+
model_id=model_id,
|
|
192
|
+
display_name=display_name,
|
|
193
|
+
canonical_model_id=canonical_id,
|
|
194
|
+
family=raw_dict.get("family", ""),
|
|
195
|
+
version=raw_dict.get("version", ""),
|
|
196
|
+
release_date=raw_dict.get("release_date"),
|
|
197
|
+
status=model_status,
|
|
198
|
+
availability=availability,
|
|
199
|
+
modalities=raw_dict.get("modalities") or ["text"],
|
|
200
|
+
capabilities=capabilities,
|
|
201
|
+
context_window=context_window,
|
|
202
|
+
max_output_tokens=max_output,
|
|
203
|
+
reasoning_levels=reasoning_levels,
|
|
204
|
+
pricing=raw_dict.get("pricing") or {"input_price_per_1m": 0.0, "output_price_per_1m": 0.0},
|
|
205
|
+
endpoint=endpoint,
|
|
206
|
+
authentication_required=needs_auth,
|
|
207
|
+
local=pid == "ollama",
|
|
208
|
+
cloud=pid != "ollama",
|
|
209
|
+
verified=bool(raw_dict.get("verified", False)),
|
|
210
|
+
source=raw_dict.get("source", "official_api"),
|
|
211
|
+
tags=raw_dict.get("tags") or [],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return ValidationResult(
|
|
215
|
+
is_valid=True,
|
|
216
|
+
status=model_status,
|
|
217
|
+
model_info=model_info,
|
|
218
|
+
warnings=warnings,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _derive_canonical_id(model_id: str, provider_id: str) -> str:
|
|
223
|
+
"""Normalize model identifier across multi-endpoint routers."""
|
|
224
|
+
clean = model_id.strip()
|
|
225
|
+
# Remove routing prefix (e.g. 'openai/gpt-4o-mini' -> 'gpt-4o-mini')
|
|
226
|
+
if "/" in clean and not clean.startswith("http"):
|
|
227
|
+
parts = clean.split("/")
|
|
228
|
+
if len(parts) == 2 and parts[0].lower() in (
|
|
229
|
+
"openai", "anthropic", "google", "meta-llama", "mistralai", "deepseek", "qwen"
|
|
230
|
+
):
|
|
231
|
+
clean = parts[1]
|
|
232
|
+
return f"{provider_id}:{clean}"
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _clean_display_name(model_id: str) -> str:
|
|
236
|
+
# Convert raw ids like 'gpt-6-astra' -> 'GPT-6 Astra'
|
|
237
|
+
clean = model_id.split("/")[-1]
|
|
238
|
+
words = clean.replace("-", " ").replace("_", " ").split()
|
|
239
|
+
capitalized = []
|
|
240
|
+
for w in words:
|
|
241
|
+
if w.lower() in ("gpt", "glm", "api", "r1", "v3", "hy3", "mimo"):
|
|
242
|
+
capitalized.append(w.upper())
|
|
243
|
+
else:
|
|
244
|
+
capitalized.append(w.capitalize())
|
|
245
|
+
return " ".join(capitalized)
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _infer_capabilities(model_id: str, provider_id: str, raw_dict: dict) -> List[str]:
|
|
249
|
+
caps = list(raw_dict.get("capabilities", []))
|
|
250
|
+
if not caps:
|
|
251
|
+
caps = ["chat", "streaming"]
|
|
252
|
+
|
|
253
|
+
# Normalize capability synonyms in input list
|
|
254
|
+
if any(c in ("cot", "chain_of_thought", "reasoning") for c in caps):
|
|
255
|
+
if "reasoning" not in caps:
|
|
256
|
+
caps.append("reasoning")
|
|
257
|
+
if any(c in ("code", "coder", "coding") for c in caps):
|
|
258
|
+
if "coding" not in caps:
|
|
259
|
+
caps.append("coding")
|
|
260
|
+
|
|
261
|
+
mid = model_id.lower()
|
|
262
|
+
if any(k in mid for k in ("reasoner", "r1", "o1", "o3", "k3", "qwq", "thinking")):
|
|
263
|
+
if "reasoning" not in caps:
|
|
264
|
+
caps.append("reasoning")
|
|
265
|
+
if any(k in mid for k in ("coder", "codestral", "astra", "code", "dev")):
|
|
266
|
+
if "coding" not in caps:
|
|
267
|
+
caps.append("coding")
|
|
268
|
+
if any(k in mid for k in ("vision", "4o", "4v", "pixtral", "vl", "gemini")):
|
|
269
|
+
if "vision" not in caps:
|
|
270
|
+
caps.append("vision")
|
|
271
|
+
if any(k in mid for k in ("mini", "flash", "turbo", "instant", "haiku", "8b")):
|
|
272
|
+
if "fast" not in caps:
|
|
273
|
+
caps.append("fast")
|
|
274
|
+
if "tools" in raw_dict or "function" in raw_dict:
|
|
275
|
+
if "tools" not in caps:
|
|
276
|
+
caps.append("tools")
|
|
277
|
+
return list(set(caps))
|
|
278
|
+
|
|
279
|
+
def validate(self, model: ModelInfo) -> Tuple[bool, str]:
|
|
280
|
+
"""Validate a ModelInfo instance directly."""
|
|
281
|
+
res = self.validate_raw(model, model.provider)
|
|
282
|
+
if not res.is_valid:
|
|
283
|
+
return False, res.rejection_reason or "Validation failed"
|
|
284
|
+
# Check if capabilities were normalized
|
|
285
|
+
if res.model_info:
|
|
286
|
+
model.capabilities = res.model_info.capabilities
|
|
287
|
+
return True, "Valid"
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT CLI — ModelVerificationEngine: Automated Verification for Unknown & Custom Models.
|
|
3
|
+
|
|
4
|
+
Verifies:
|
|
5
|
+
1. Endpoint reachability
|
|
6
|
+
2. Protocol & Auth handshake
|
|
7
|
+
3. Model ID validity
|
|
8
|
+
4. Live request/response generation
|
|
9
|
+
5. Capability & metadata coherence
|
|
10
|
+
|
|
11
|
+
Statuses:
|
|
12
|
+
✓ VERIFIED
|
|
13
|
+
⚠ PARTIALLY_VERIFIED
|
|
14
|
+
? UNKNOWN
|
|
15
|
+
✗ FAILED
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
import json
|
|
20
|
+
import urllib.parse
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Dict, List, Optional, Callable, Tuple, Any, Union
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VerificationStatus:
|
|
26
|
+
VERIFIED = "VERIFIED"
|
|
27
|
+
PARTIALLY_VERIFIED = "PARTIALLY_VERIFIED"
|
|
28
|
+
UNKNOWN = "UNKNOWN"
|
|
29
|
+
FAILED = "FAILED"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def mask_secret(secret: str) -> str:
|
|
33
|
+
"""Safely mask API key/token for display or logging."""
|
|
34
|
+
if not secret:
|
|
35
|
+
return ""
|
|
36
|
+
secret = str(secret).strip()
|
|
37
|
+
if len(secret) <= 8:
|
|
38
|
+
return "********"
|
|
39
|
+
return secret[:3] + "..." + secret[-4:]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class VerificationStep:
|
|
44
|
+
name: str
|
|
45
|
+
status: str # "pending", "running", "success", "warning", "failed"
|
|
46
|
+
message: str = ""
|
|
47
|
+
elapsed_ms: float = 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class VerificationResult:
|
|
52
|
+
status: str = VerificationStatus.UNKNOWN
|
|
53
|
+
success: bool = False
|
|
54
|
+
message: str = ""
|
|
55
|
+
error: str = ""
|
|
56
|
+
steps: List[VerificationStep] = field(default_factory=list)
|
|
57
|
+
model_metadata: Dict = field(default_factory=dict)
|
|
58
|
+
models_found: List[str] = field(default_factory=list)
|
|
59
|
+
timestamp: float = field(default_factory=time.time)
|
|
60
|
+
|
|
61
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
62
|
+
return getattr(self, key, default)
|
|
63
|
+
|
|
64
|
+
def __getitem__(self, key: str) -> Any:
|
|
65
|
+
if hasattr(self, key):
|
|
66
|
+
return getattr(self, key)
|
|
67
|
+
raise KeyError(key)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict:
|
|
70
|
+
return {
|
|
71
|
+
"status": self.status,
|
|
72
|
+
"success": self.success,
|
|
73
|
+
"message": self.message,
|
|
74
|
+
"error": self.error,
|
|
75
|
+
"steps": [
|
|
76
|
+
{"name": s.name, "status": s.status, "message": s.message, "elapsed_ms": s.elapsed_ms}
|
|
77
|
+
for s in self.steps
|
|
78
|
+
],
|
|
79
|
+
"model_metadata": self.model_metadata,
|
|
80
|
+
"models_found": self.models_found,
|
|
81
|
+
"timestamp": self.timestamp,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ModelVerificationEngine:
|
|
86
|
+
"""Executes safe, non-destructive verification against AI model endpoints."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, timeout: float = 8.0):
|
|
89
|
+
self.timeout = timeout
|
|
90
|
+
|
|
91
|
+
def verify_custom_model(
|
|
92
|
+
self,
|
|
93
|
+
provider_id: Any = "",
|
|
94
|
+
model_id: str = "",
|
|
95
|
+
endpoint: str = "",
|
|
96
|
+
protocol: str = "openai",
|
|
97
|
+
api_key: str = "",
|
|
98
|
+
context_window: int = 32000,
|
|
99
|
+
capabilities: Optional[Dict] = None,
|
|
100
|
+
on_step_update: Optional[Callable[[VerificationStep], None]] = None,
|
|
101
|
+
) -> VerificationResult:
|
|
102
|
+
"""Run complete 5-stage verification workflow.
|
|
103
|
+
|
|
104
|
+
Calls on_step_update(step) as each phase progresses.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(provider_id, dict):
|
|
107
|
+
cfg = provider_id
|
|
108
|
+
model_id = cfg.get("model_id") or cfg.get("model") or model_id or "default"
|
|
109
|
+
endpoint = cfg.get("base_url") or cfg.get("endpoint") or cfg.get("url") or endpoint
|
|
110
|
+
protocol = cfg.get("api_style") or cfg.get("protocol") or protocol or "openai"
|
|
111
|
+
api_key = cfg.get("api_key") or api_key
|
|
112
|
+
context_window = cfg.get("context_window", context_window)
|
|
113
|
+
capabilities = cfg.get("capabilities", capabilities)
|
|
114
|
+
provider_id = cfg.get("provider_id") or cfg.get("provider") or "custom"
|
|
115
|
+
|
|
116
|
+
result = VerificationResult()
|
|
117
|
+
steps: List[VerificationStep] = [
|
|
118
|
+
VerificationStep("Input Validation", "pending"),
|
|
119
|
+
VerificationStep("Endpoint Reachability", "pending"),
|
|
120
|
+
VerificationStep("Authentication & Protocol Handshake", "pending"),
|
|
121
|
+
VerificationStep("Model Existence & Response Test", "pending"),
|
|
122
|
+
VerificationStep("Metadata Coherence", "pending"),
|
|
123
|
+
]
|
|
124
|
+
result.steps = steps
|
|
125
|
+
|
|
126
|
+
def update_step(idx: int, status: str, msg: str = "", t0: Optional[float] = None):
|
|
127
|
+
elapsed = (time.perf_counter() - t0) * 1000.0 if t0 else 0.0
|
|
128
|
+
steps[idx].status = status
|
|
129
|
+
steps[idx].message = msg
|
|
130
|
+
if t0:
|
|
131
|
+
steps[idx].elapsed_ms = round(elapsed, 1)
|
|
132
|
+
if on_step_update:
|
|
133
|
+
try:
|
|
134
|
+
on_step_update(steps[idx])
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
# ---------------- Stage 1: Input Validation ----------------
|
|
139
|
+
t0 = time.perf_counter()
|
|
140
|
+
update_step(0, "running", "Validating model parameters...", t0)
|
|
141
|
+
provider_id = (provider_id or "").strip().lower()
|
|
142
|
+
model_id = (model_id or "").strip()
|
|
143
|
+
endpoint = (endpoint or "").strip()
|
|
144
|
+
protocol = (protocol or "openai").strip().lower()
|
|
145
|
+
|
|
146
|
+
if provider_id == "ollama":
|
|
147
|
+
if not endpoint:
|
|
148
|
+
endpoint = "http://localhost:11434"
|
|
149
|
+
if protocol in ("openai", "custom"):
|
|
150
|
+
protocol = "ollama"
|
|
151
|
+
|
|
152
|
+
if not model_id:
|
|
153
|
+
update_step(0, "failed", "Model ID cannot be empty", t0)
|
|
154
|
+
result.status = VerificationStatus.FAILED
|
|
155
|
+
result.error = "Model ID cannot be empty"
|
|
156
|
+
result.message = "Validation failed: model ID is required"
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
if protocol not in ("openai", "anthropic", "gemini", "ollama", "custom"):
|
|
160
|
+
update_step(0, "failed", f"Unsupported protocol: {protocol}", t0)
|
|
161
|
+
result.status = VerificationStatus.FAILED
|
|
162
|
+
result.error = f"Unsupported protocol: {protocol}"
|
|
163
|
+
result.message = "Validation failed: invalid protocol"
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
if endpoint:
|
|
167
|
+
parsed = urllib.parse.urlparse(endpoint)
|
|
168
|
+
if parsed.scheme not in ("http", "https"):
|
|
169
|
+
update_step(0, "failed", f"Invalid URL scheme '{parsed.scheme}'; expected http or https", t0)
|
|
170
|
+
result.status = VerificationStatus.FAILED
|
|
171
|
+
result.error = f"Invalid URL scheme '{parsed.scheme}'. Endpoint must begin with http:// or https://"
|
|
172
|
+
result.message = "Validation failed: invalid endpoint URL"
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
update_step(0, "success", f"Validated: {provider_id}/{model_id} ({protocol})", t0)
|
|
176
|
+
|
|
177
|
+
# ---------------- Stage 2: Endpoint Reachability ----------------
|
|
178
|
+
t0 = time.perf_counter()
|
|
179
|
+
update_step(1, "running", "Checking endpoint reachability...", t0)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
import requests
|
|
183
|
+
HAS_REQUESTS = True
|
|
184
|
+
except ImportError:
|
|
185
|
+
HAS_REQUESTS = False
|
|
186
|
+
|
|
187
|
+
if not HAS_REQUESTS:
|
|
188
|
+
update_step(1, "warning", "requests library not installed; skipping network probe", t0)
|
|
189
|
+
else:
|
|
190
|
+
reach_url = endpoint or "http://localhost:11434"
|
|
191
|
+
try:
|
|
192
|
+
# Lightweight probe with short timeout
|
|
193
|
+
resp = requests.get(reach_url, timeout=self.timeout)
|
|
194
|
+
update_step(1, "success", f"Endpoint reachable (HTTP {resp.status_code})", t0)
|
|
195
|
+
except requests.exceptions.SSLError as e:
|
|
196
|
+
update_step(1, "failed", f"SSL Certificate Error: {e}", t0)
|
|
197
|
+
result.status = VerificationStatus.FAILED
|
|
198
|
+
result.error = "SSL validation failed. Check endpoint certificate."
|
|
199
|
+
return result
|
|
200
|
+
except requests.exceptions.ConnectionError:
|
|
201
|
+
# Try base host
|
|
202
|
+
parsed = urllib.parse.urlparse(reach_url)
|
|
203
|
+
base = f"{parsed.scheme}://{parsed.netloc}"
|
|
204
|
+
try:
|
|
205
|
+
resp = requests.get(base, timeout=4.0)
|
|
206
|
+
update_step(1, "success", f"Host reachable (HTTP {resp.status_code})", t0)
|
|
207
|
+
except Exception as ex:
|
|
208
|
+
update_step(1, "failed", f"Could not connect to {base}: {ex}", t0)
|
|
209
|
+
result.status = VerificationStatus.FAILED
|
|
210
|
+
result.error = f"Connection refused or unreachable: {base}"
|
|
211
|
+
return result
|
|
212
|
+
except Exception as e:
|
|
213
|
+
update_step(1, "warning", f"Probe warning: {e}", t0)
|
|
214
|
+
|
|
215
|
+
# ---------------- Stage 3: Auth & Protocol Handshake ----------------
|
|
216
|
+
t0 = time.perf_counter()
|
|
217
|
+
update_step(2, "running", "Performing authentication handshake...", t0)
|
|
218
|
+
|
|
219
|
+
config = {
|
|
220
|
+
"provider": provider_id or "custom",
|
|
221
|
+
"model": model_id,
|
|
222
|
+
"api_key": api_key,
|
|
223
|
+
"base_url": endpoint,
|
|
224
|
+
"api_url": endpoint,
|
|
225
|
+
"api_style": protocol if protocol != "custom" else "openai",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
from .. import aicore
|
|
230
|
+
ok, msg, models = aicore.verify_connection(config)
|
|
231
|
+
result.models_found = list(models or [])
|
|
232
|
+
except Exception as ex:
|
|
233
|
+
ok, msg, models = False, str(ex), []
|
|
234
|
+
|
|
235
|
+
if ok:
|
|
236
|
+
update_step(2, "success", f"Auth & Protocol verified ({msg})", t0)
|
|
237
|
+
elif "authentication" in msg.lower() or "401" in msg or "invalid api key" in msg.lower():
|
|
238
|
+
update_step(2, "failed", f"Authentication failed: {msg}", t0)
|
|
239
|
+
result.status = VerificationStatus.FAILED
|
|
240
|
+
result.error = msg
|
|
241
|
+
result.message = "Authentication rejected by provider"
|
|
242
|
+
return result
|
|
243
|
+
else:
|
|
244
|
+
# Maybe model endpoint doesn't support /models list endpoint, but accepts chat completions
|
|
245
|
+
update_step(2, "warning", f"Handshake returned: {msg}", t0)
|
|
246
|
+
|
|
247
|
+
# ---------------- Stage 4: Live Generation Response Test ----------------
|
|
248
|
+
t0 = time.perf_counter()
|
|
249
|
+
update_step(3, "running", f"Testing generation query on '{model_id}'...", t0)
|
|
250
|
+
|
|
251
|
+
test_prompt = "Say hello in one word."
|
|
252
|
+
gen_ok = False
|
|
253
|
+
gen_text = ""
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
from .. import aicore
|
|
257
|
+
# Test direct query
|
|
258
|
+
reply = aicore.query_ai(test_prompt, config=config)
|
|
259
|
+
if aicore.is_error_response(reply):
|
|
260
|
+
gen_ok = False
|
|
261
|
+
gen_text = reply
|
|
262
|
+
else:
|
|
263
|
+
gen_ok = True
|
|
264
|
+
gen_text = (reply or "").strip()[:50]
|
|
265
|
+
except Exception as ex:
|
|
266
|
+
gen_ok = False
|
|
267
|
+
gen_text = str(ex)
|
|
268
|
+
|
|
269
|
+
if gen_ok:
|
|
270
|
+
update_step(3, "success", f"Model responded: '{gen_text}'", t0)
|
|
271
|
+
else:
|
|
272
|
+
if ok:
|
|
273
|
+
# Handshake worked but prompt failed
|
|
274
|
+
update_step(3, "warning", f"Model prompt returned warning: {gen_text}", t0)
|
|
275
|
+
else:
|
|
276
|
+
update_step(3, "failed", f"Test request failed: {gen_text}", t0)
|
|
277
|
+
result.status = VerificationStatus.FAILED
|
|
278
|
+
result.error = gen_text
|
|
279
|
+
result.message = f"Model '{model_id}' did not respond successfully"
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
# ---------------- Stage 5: Metadata Coherence ----------------
|
|
283
|
+
t0 = time.perf_counter()
|
|
284
|
+
update_step(4, "running", "Validating metadata & capability coherence...", t0)
|
|
285
|
+
|
|
286
|
+
# Normalize capabilities
|
|
287
|
+
inferred_caps = capabilities or {}
|
|
288
|
+
if not inferred_caps:
|
|
289
|
+
try:
|
|
290
|
+
from ..model_router import capabilities_for
|
|
291
|
+
c_obj = capabilities_for(provider_id, model_id)
|
|
292
|
+
inferred_caps = c_obj.to_dict() if hasattr(c_obj, "to_dict") else dict(c_obj.__dict__)
|
|
293
|
+
except Exception:
|
|
294
|
+
inferred_caps = {"text": True, "streaming": True}
|
|
295
|
+
|
|
296
|
+
result.model_metadata = {
|
|
297
|
+
"model_id": model_id,
|
|
298
|
+
"provider_id": provider_id,
|
|
299
|
+
"endpoint": endpoint,
|
|
300
|
+
"protocol": protocol,
|
|
301
|
+
"context_window": max(512, int(context_window or 32000)),
|
|
302
|
+
"capabilities": inferred_caps,
|
|
303
|
+
"verified_at": time.time(),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
update_step(4, "success", "Metadata coherent and verified", t0)
|
|
307
|
+
|
|
308
|
+
# Final status determination
|
|
309
|
+
if gen_ok and ok:
|
|
310
|
+
result.status = VerificationStatus.VERIFIED
|
|
311
|
+
result.success = True
|
|
312
|
+
result.message = f"Model '{model_id}' successfully verified!"
|
|
313
|
+
elif gen_ok or ok:
|
|
314
|
+
result.status = VerificationStatus.PARTIALLY_VERIFIED
|
|
315
|
+
result.success = True
|
|
316
|
+
result.message = f"Model '{model_id}' partially verified (connection verified)"
|
|
317
|
+
else:
|
|
318
|
+
result.status = VerificationStatus.FAILED
|
|
319
|
+
result.success = False
|
|
320
|
+
result.message = f"Verification failed: {result.error}"
|
|
321
|
+
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def verify_model(
|
|
326
|
+
provider_id: str,
|
|
327
|
+
model_id: str,
|
|
328
|
+
endpoint: str = "",
|
|
329
|
+
protocol: str = "openai",
|
|
330
|
+
api_key: str = "",
|
|
331
|
+
context_window: int = 32000,
|
|
332
|
+
capabilities: Optional[Dict] = None,
|
|
333
|
+
on_step_update: Optional[Callable[[VerificationStep], None]] = None,
|
|
334
|
+
) -> VerificationResult:
|
|
335
|
+
"""Module-level helper to execute ModelVerificationEngine."""
|
|
336
|
+
engine = get_verification_engine()
|
|
337
|
+
return engine.verify_custom_model(
|
|
338
|
+
provider_id=provider_id,
|
|
339
|
+
model_id=model_id,
|
|
340
|
+
endpoint=endpoint,
|
|
341
|
+
protocol=protocol,
|
|
342
|
+
api_key=api_key,
|
|
343
|
+
context_window=context_window,
|
|
344
|
+
capabilities=capabilities,
|
|
345
|
+
on_step_update=on_step_update,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
_DEFAULT_ENGINE: Optional[ModelVerificationEngine] = None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_verification_engine(timeout: float = 8.0) -> ModelVerificationEngine:
|
|
353
|
+
"""Return singleton instance of ModelVerificationEngine."""
|
|
354
|
+
global _DEFAULT_ENGINE
|
|
355
|
+
if _DEFAULT_ENGINE is None:
|
|
356
|
+
_DEFAULT_ENGINE = ModelVerificationEngine(timeout=timeout)
|
|
357
|
+
return _DEFAULT_ENGINE
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
__all__ = [
|
|
361
|
+
"ModelVerificationEngine",
|
|
362
|
+
"VerificationStatus",
|
|
363
|
+
"VerificationStep",
|
|
364
|
+
"VerificationResult",
|
|
365
|
+
"get_verification_engine",
|
|
366
|
+
"verify_model",
|
|
367
|
+
"mask_secret",
|
|
368
|
+
]
|