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,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Model Capabilities & Profile System.
|
|
3
|
+
|
|
4
|
+
Enforces Section 13, 14, 26, 35-37 of the CAT AI Mode Reliability Contract.
|
|
5
|
+
Distinguishes small/base models (e.g. deepseek-coder:1.3b-base-q8_0) from
|
|
6
|
+
large instruction-following models, providing conservative budgets,
|
|
7
|
+
minimal prompting, and specialized generation parameters.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ModelCapabilities:
|
|
17
|
+
chat: bool = True
|
|
18
|
+
instruction_following: bool = True
|
|
19
|
+
tool_calling: bool = True
|
|
20
|
+
vision: bool = False
|
|
21
|
+
reasoning: bool = False
|
|
22
|
+
context_length: int = 4096
|
|
23
|
+
is_small_model: bool = False
|
|
24
|
+
is_base_model: bool = False
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"chat": self.chat,
|
|
29
|
+
"instruction_following": self.instruction_following,
|
|
30
|
+
"tool_calling": self.tool_calling,
|
|
31
|
+
"vision": self.vision,
|
|
32
|
+
"reasoning": self.reasoning,
|
|
33
|
+
"context_length": self.context_length,
|
|
34
|
+
"is_small_model": self.is_small_model,
|
|
35
|
+
"is_base_model": self.is_base_model,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ModelProfile:
|
|
41
|
+
provider: str
|
|
42
|
+
model: str
|
|
43
|
+
capabilities: ModelCapabilities
|
|
44
|
+
prompt_template: str = "chat" # "chat", "minimal", "completion"
|
|
45
|
+
recommended_temperature: float = 0.7
|
|
46
|
+
max_context_tokens: int = 4096
|
|
47
|
+
reserved_output_tokens: int = 1024
|
|
48
|
+
generation_options: Dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
def is_small_or_base(self) -> bool:
|
|
51
|
+
return self.capabilities.is_small_model or self.capabilities.is_base_model
|
|
52
|
+
|
|
53
|
+
def supports_tools(self) -> bool:
|
|
54
|
+
return self.capabilities.tool_calling and not self.capabilities.is_base_model
|
|
55
|
+
|
|
56
|
+
def get_effective_options(self, user_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
57
|
+
"""Merge base generation options with model defaults and optional user overrides."""
|
|
58
|
+
opts = dict(self.generation_options)
|
|
59
|
+
if user_options:
|
|
60
|
+
for k, v in user_options.items():
|
|
61
|
+
if v is not None:
|
|
62
|
+
opts[k] = v
|
|
63
|
+
return opts
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Regex patterns identifying small or base models
|
|
67
|
+
_BASE_MODEL_PATTERNS = [
|
|
68
|
+
re.compile(r"[-:_]base($|[-:_])", re.IGNORECASE),
|
|
69
|
+
re.compile(r"base-q\d", re.IGNORECASE),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
_SMALL_MODEL_PATTERNS = [
|
|
73
|
+
re.compile(r"[:\-_](0\.\d+|1\.[0-8]+|2|3)b($|[:\-_])", re.IGNORECASE),
|
|
74
|
+
re.compile(r"(tinyllama|smollm|qwen.*0\.5b|qwen.*1\.8b|deepseek.*1\.3b)", re.IGNORECASE),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
_VISION_PATTERNS = [
|
|
78
|
+
re.compile(r"(vision|llava|gpt-4o|claude-3|gemini-1\.5|gemini-2\.0)", re.IGNORECASE),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
_REASONING_PATTERNS = [
|
|
82
|
+
re.compile(r"(o1|o3|r1|reasoning|deepseek-r1)", re.IGNORECASE),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
_TOOL_CAPABLE_PATTERNS = [
|
|
86
|
+
re.compile(r"(gpt-4|claude-3|gemini|llama-?3\.[123]|qwen2\.5.*(7b|14b|32b|72b)|mistral)", re.IGNORECASE),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def detect_capabilities(provider: str, model_name: str) -> ModelCapabilities:
|
|
91
|
+
"""Analyze provider and model string to infer capabilities safely."""
|
|
92
|
+
provider_str = (provider or "").lower().strip()
|
|
93
|
+
model_str = (model_name or "").lower().strip()
|
|
94
|
+
|
|
95
|
+
is_base = any(p.search(model_str) for p in _BASE_MODEL_PATTERNS)
|
|
96
|
+
is_small = any(p.search(model_str) for p in _SMALL_MODEL_PATTERNS) or ("1.3b" in model_str)
|
|
97
|
+
|
|
98
|
+
# Base models do not reliably follow complex multi-turn instruction schemas
|
|
99
|
+
instruction_following = not is_base
|
|
100
|
+
tool_calling = False if (is_base or is_small) else any(p.search(model_str) for p in _TOOL_CAPABLE_PATTERNS)
|
|
101
|
+
|
|
102
|
+
# If provider is OpenAI/Anthropic/Gemini standard models, tool calling is standard
|
|
103
|
+
if provider_str in ("openai", "anthropic", "gemini") and not is_small:
|
|
104
|
+
tool_calling = True
|
|
105
|
+
instruction_following = True
|
|
106
|
+
|
|
107
|
+
vision = any(p.search(model_str) for p in _VISION_PATTERNS)
|
|
108
|
+
reasoning = any(p.search(model_str) for p in _REASONING_PATTERNS)
|
|
109
|
+
|
|
110
|
+
if is_small:
|
|
111
|
+
context_length = 2048
|
|
112
|
+
elif is_base:
|
|
113
|
+
context_length = 2048
|
|
114
|
+
elif "16k" in model_str:
|
|
115
|
+
context_length = 16384
|
|
116
|
+
elif "32k" in model_str:
|
|
117
|
+
context_length = 32768
|
|
118
|
+
elif "128k" in model_str or "gpt-4o" in model_str or "gemini" in model_str:
|
|
119
|
+
context_length = 131072
|
|
120
|
+
else:
|
|
121
|
+
context_length = 4096
|
|
122
|
+
|
|
123
|
+
return ModelCapabilities(
|
|
124
|
+
chat=True,
|
|
125
|
+
instruction_following=instruction_following,
|
|
126
|
+
tool_calling=tool_calling,
|
|
127
|
+
vision=vision,
|
|
128
|
+
reasoning=reasoning,
|
|
129
|
+
context_length=context_length,
|
|
130
|
+
is_small_model=is_small,
|
|
131
|
+
is_base_model=is_base,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_model_profile(provider: str, model_name: str, config: Optional[Dict[str, Any]] = None) -> ModelProfile:
|
|
136
|
+
"""Build a complete ModelProfile tailored to the given provider and model."""
|
|
137
|
+
caps = detect_capabilities(provider, model_name)
|
|
138
|
+
|
|
139
|
+
if caps.is_small_model or caps.is_base_model:
|
|
140
|
+
# Conservative defaults for small/base local models (e.g. deepseek-coder:1.3b-base-q8_0)
|
|
141
|
+
recommended_temp = 0.3
|
|
142
|
+
max_context = min(caps.context_length, 1536)
|
|
143
|
+
reserved_out = 512
|
|
144
|
+
prompt_template = "minimal" if caps.is_base_model else "chat"
|
|
145
|
+
gen_opts = {
|
|
146
|
+
"temperature": recommended_temp,
|
|
147
|
+
"top_p": 0.9,
|
|
148
|
+
"repeat_penalty": 1.18,
|
|
149
|
+
"num_predict": reserved_out,
|
|
150
|
+
}
|
|
151
|
+
else:
|
|
152
|
+
# Standard instruction/chat model defaults
|
|
153
|
+
recommended_temp = 0.7
|
|
154
|
+
max_context = min(caps.context_length, 8192)
|
|
155
|
+
reserved_out = 1024
|
|
156
|
+
prompt_template = "chat"
|
|
157
|
+
gen_opts = {
|
|
158
|
+
"temperature": recommended_temp,
|
|
159
|
+
"top_p": 0.95,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Override temperature if explicitly provided in config
|
|
163
|
+
if config and config.get("temperature") is not None:
|
|
164
|
+
try:
|
|
165
|
+
gen_opts["temperature"] = float(config["temperature"])
|
|
166
|
+
except (ValueError, TypeError):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# For Ollama models, check if model size exceeds GPU VRAM (e.g. 8B/9.6GB model on 4GB VRAM)
|
|
170
|
+
# Default to num_gpu: 0 (CPU) to prevent llama-server crashing with 0xc0000409 buffer overrun
|
|
171
|
+
if (provider or "").lower() == "ollama":
|
|
172
|
+
if config and config.get("num_gpu") is not None:
|
|
173
|
+
gen_opts["num_gpu"] = int(config["num_gpu"])
|
|
174
|
+
else:
|
|
175
|
+
try:
|
|
176
|
+
from .. import pc_specs
|
|
177
|
+
specs = pc_specs.get_specs()
|
|
178
|
+
vram = specs.get("gpu_vram_gb", 0)
|
|
179
|
+
is_large = any(k in (model_name or "").lower() for k in ("8b", "9b", "14b", "32b", "70b", "e4b", "gemma4", "gemma:7b", "llama3:8b"))
|
|
180
|
+
if vram and vram <= 4.5 and is_large:
|
|
181
|
+
gen_opts["num_gpu"] = 0
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
return ModelProfile(
|
|
186
|
+
provider=provider or "ollama",
|
|
187
|
+
model=model_name or "default",
|
|
188
|
+
capabilities=caps,
|
|
189
|
+
prompt_template=prompt_template,
|
|
190
|
+
recommended_temperature=recommended_temp,
|
|
191
|
+
max_context_tokens=max_context,
|
|
192
|
+
reserved_output_tokens=reserved_out,
|
|
193
|
+
generation_options=gen_opts,
|
|
194
|
+
)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Model registry — curated metadata + dynamic discovery merging.
|
|
2
|
+
|
|
3
|
+
Data-driven redesign (v0.7.4+): curated model metadata lives in
|
|
4
|
+
`models/model_metadata.json`, provider metadata + fallback model lists
|
|
5
|
+
live in `providers/providers.json`. No model names are hardcoded in
|
|
6
|
+
Python source — this module only loads and queries the JSON stores.
|
|
7
|
+
|
|
8
|
+
Dynamic model discovery, disk caching, refresh and category filtering
|
|
9
|
+
live in `models/manager.py`; this module keeps the classic lookup API
|
|
10
|
+
used by the picker UIs (get_model_info / get_badges / enrich_models /
|
|
11
|
+
group_by_tier / ...).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from . import manager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Schema ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
class ModelInfo:
|
|
24
|
+
__slots__ = (
|
|
25
|
+
"id", "provider", "family",
|
|
26
|
+
"context_window", "capabilities",
|
|
27
|
+
"pricing_tier", "status",
|
|
28
|
+
"input_price_per_1m", "output_price_per_1m",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __init__(self, id="", provider="", family="",
|
|
32
|
+
context_window=0, capabilities=None,
|
|
33
|
+
pricing_tier="unknown", status="stable",
|
|
34
|
+
input_price_per_1m=0.0, output_price_per_1m=0.0):
|
|
35
|
+
self.id = id
|
|
36
|
+
self.provider = provider
|
|
37
|
+
self.family = family
|
|
38
|
+
self.context_window = context_window
|
|
39
|
+
self.capabilities = capabilities or []
|
|
40
|
+
self.pricing_tier = pricing_tier
|
|
41
|
+
self.status = status
|
|
42
|
+
self.input_price_per_1m = input_price_per_1m
|
|
43
|
+
self.output_price_per_1m = output_price_per_1m
|
|
44
|
+
|
|
45
|
+
def to_dict(self):
|
|
46
|
+
return {
|
|
47
|
+
"id": self.id,
|
|
48
|
+
"provider": self.provider,
|
|
49
|
+
"family": self.family,
|
|
50
|
+
"context_window": self.context_window,
|
|
51
|
+
"capabilities": list(self.capabilities),
|
|
52
|
+
"pricing_tier": self.pricing_tier,
|
|
53
|
+
"status": self.status,
|
|
54
|
+
"input_price_per_1m": self.input_price_per_1m,
|
|
55
|
+
"output_price_per_1m": self.output_price_per_1m,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Metadata helpers (JSON-backed) ─────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def _meta_to_legacy(entry: dict) -> dict:
|
|
62
|
+
"""Convert a model_metadata.json entry to the legacy ModelInfo dict."""
|
|
63
|
+
caps = []
|
|
64
|
+
for key, label in (("reasoning", "reasoning"), ("vision", "vision"),
|
|
65
|
+
("coding", "coding"), ("embedding", "embeddings"),
|
|
66
|
+
("image_generation", "image_gen"), ("speech", "audio"),
|
|
67
|
+
("tools", "tool_use"), ("streaming", "streaming")):
|
|
68
|
+
if entry.get(key):
|
|
69
|
+
caps.append(label)
|
|
70
|
+
if not caps:
|
|
71
|
+
caps = ["chat", "streaming"]
|
|
72
|
+
tier = "free" if entry.get("free") else "paid" if entry.get("paid") else "unknown"
|
|
73
|
+
status = ("deprecated" if entry.get("deprecated")
|
|
74
|
+
else "preview" if entry.get("preview") else "stable")
|
|
75
|
+
return {
|
|
76
|
+
"id": entry.get("id", ""),
|
|
77
|
+
"provider": entry.get("provider", ""),
|
|
78
|
+
"family": entry.get("family", ""),
|
|
79
|
+
"context_window": int(entry.get("context_length", 0) or 0),
|
|
80
|
+
"capabilities": caps,
|
|
81
|
+
"pricing_tier": tier,
|
|
82
|
+
"status": status,
|
|
83
|
+
"input_price_per_1m": float(entry.get("input_price_per_1m", 0.0) or 0.0),
|
|
84
|
+
"output_price_per_1m": float(entry.get("output_price_per_1m", 0.0) or 0.0),
|
|
85
|
+
"recommendation_badges": entry.get("recommendation_badges", []),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_meta(model_id: str, provider_id: str) -> Optional[dict]:
|
|
90
|
+
entry = manager.get_model_meta(model_id, provider_id)
|
|
91
|
+
if entry:
|
|
92
|
+
return _meta_to_legacy(entry)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── Lookup Helpers ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
def get_model_info(model_id: str, provider_id: str) -> Optional[dict]:
|
|
99
|
+
"""Look up curated metadata for a specific model+provider combination."""
|
|
100
|
+
return _resolve_meta(model_id, provider_id)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_price_tier(model_id: str, provider_id: str) -> str:
|
|
104
|
+
"""Return 'free', 'paid', or 'unknown'."""
|
|
105
|
+
info = get_model_info(model_id, provider_id)
|
|
106
|
+
if info:
|
|
107
|
+
return info.get("pricing_tier", "unknown")
|
|
108
|
+
lower = model_id.lower()
|
|
109
|
+
if any(kw in lower for kw in ("free", "open-")):
|
|
110
|
+
return "free"
|
|
111
|
+
return "unknown"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_capabilities(model_id: str, provider_id: str) -> list[str]:
|
|
115
|
+
"""Return list of known capabilities for this model."""
|
|
116
|
+
info = get_model_info(model_id, provider_id)
|
|
117
|
+
if info:
|
|
118
|
+
return list(info.get("capabilities", []))
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_badges(model_id: str, provider_id: str) -> list[str]:
|
|
123
|
+
"""Return list of badge strings for UI display.
|
|
124
|
+
Each badge is a short color-tagged string like \"[FREE]\", \"[VISION]\", etc.
|
|
125
|
+
"""
|
|
126
|
+
badges = []
|
|
127
|
+
info = get_model_info(model_id, provider_id)
|
|
128
|
+
|
|
129
|
+
# Pricing badge
|
|
130
|
+
tier = (info.get("pricing_tier", "unknown") if info
|
|
131
|
+
else get_price_tier(model_id, provider_id))
|
|
132
|
+
if tier == "free":
|
|
133
|
+
badges.append("[FREE]")
|
|
134
|
+
elif tier == "paid":
|
|
135
|
+
badges.append("[PAID]")
|
|
136
|
+
else:
|
|
137
|
+
badges.append("[?]")
|
|
138
|
+
|
|
139
|
+
# Status badge
|
|
140
|
+
if info:
|
|
141
|
+
status = info.get("status", "")
|
|
142
|
+
if status == "preview":
|
|
143
|
+
badges.append("[PREVIEW]")
|
|
144
|
+
elif status == "experimental":
|
|
145
|
+
badges.append("[EXP]")
|
|
146
|
+
elif status == "deprecated":
|
|
147
|
+
badges.append("[DEPRECATED]")
|
|
148
|
+
|
|
149
|
+
# Capability badges
|
|
150
|
+
caps = info.get("capabilities", []) if info else []
|
|
151
|
+
if "vision" in caps:
|
|
152
|
+
badges.append("[VISION]")
|
|
153
|
+
if "reasoning" in caps:
|
|
154
|
+
badges.append("[REASONING]")
|
|
155
|
+
if "coding" in caps:
|
|
156
|
+
badges.append("[CODING]")
|
|
157
|
+
if "image_gen" in caps:
|
|
158
|
+
badges.append("[IMAGE]")
|
|
159
|
+
if "audio" in caps:
|
|
160
|
+
badges.append("[AUDIO]")
|
|
161
|
+
if "embeddings" in caps:
|
|
162
|
+
badges.append("[EMBED]")
|
|
163
|
+
|
|
164
|
+
# Context window badge
|
|
165
|
+
if info:
|
|
166
|
+
ctx = info.get("context_window", 0)
|
|
167
|
+
if ctx >= 1000000:
|
|
168
|
+
badges.append("[1M]")
|
|
169
|
+
elif ctx >= 100000:
|
|
170
|
+
badges.append("[100K]")
|
|
171
|
+
elif ctx >= 32000:
|
|
172
|
+
badges.append("[32K]")
|
|
173
|
+
elif ctx >= 8000:
|
|
174
|
+
badges.append("[8K]")
|
|
175
|
+
|
|
176
|
+
# Recommendation badges from dynamic registry (e.g. Best for Coding)
|
|
177
|
+
if info:
|
|
178
|
+
for b in info.get("recommendation_badges", []):
|
|
179
|
+
b_clean = f"[{b.strip('[]').upper()}]"
|
|
180
|
+
if b_clean not in badges:
|
|
181
|
+
badges.append(b_clean)
|
|
182
|
+
|
|
183
|
+
return badges
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_family(model_id: str, provider_id: str) -> str:
|
|
187
|
+
"""Return the model family name, if known."""
|
|
188
|
+
info = get_model_info(model_id, provider_id)
|
|
189
|
+
if info:
|
|
190
|
+
return info.get("family", "")
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_context_window(model_id: str, provider_id: str) -> int:
|
|
195
|
+
"""Return the context window size, if known."""
|
|
196
|
+
info = get_model_info(model_id, provider_id)
|
|
197
|
+
if info:
|
|
198
|
+
return info.get("context_window", 0)
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ── Dynamic Merge ──────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
def enrich_models(provider_id: str, model_ids: list[str]) -> list[dict]:
|
|
205
|
+
"""Merge dynamic model list with curated metadata.
|
|
206
|
+
|
|
207
|
+
Returns a list of dicts, one per model, sorted alphabetically:
|
|
208
|
+
{"id": str, "pricing_tier": str, "badges": list[str],
|
|
209
|
+
"capabilities": list[str], "family": str, "context_window": int}
|
|
210
|
+
"""
|
|
211
|
+
result = []
|
|
212
|
+
for mid in model_ids:
|
|
213
|
+
info = get_model_info(mid, provider_id)
|
|
214
|
+
if info:
|
|
215
|
+
result.append({
|
|
216
|
+
"id": mid,
|
|
217
|
+
"pricing_tier": info.get("pricing_tier", "unknown"),
|
|
218
|
+
"badges": get_badges(mid, provider_id),
|
|
219
|
+
"capabilities": list(info.get("capabilities", [])),
|
|
220
|
+
"family": info.get("family", ""),
|
|
221
|
+
"context_window": info.get("context_window", 0),
|
|
222
|
+
"status": info.get("status", "stable"),
|
|
223
|
+
})
|
|
224
|
+
else:
|
|
225
|
+
tier = get_price_tier(mid, provider_id)
|
|
226
|
+
result.append({
|
|
227
|
+
"id": mid,
|
|
228
|
+
"pricing_tier": tier,
|
|
229
|
+
"badges": ["[?]"],
|
|
230
|
+
"capabilities": [],
|
|
231
|
+
"family": "",
|
|
232
|
+
"context_window": 0,
|
|
233
|
+
"status": "unknown",
|
|
234
|
+
})
|
|
235
|
+
result.sort(key=lambda x: x["id"].lower())
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def tier_sort_key(entry: dict) -> int:
|
|
240
|
+
"""Sort key: free (0) before paid (1) before unknown (2)."""
|
|
241
|
+
t = entry.get("pricing_tier", "unknown")
|
|
242
|
+
return {"free": 0, "paid": 1, "unknown": 2}.get(t, 2)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def group_by_tier(enriched: list[dict]) -> dict[str, list[dict]]:
|
|
246
|
+
"""Group enriched model list by pricing tier.
|
|
247
|
+
|
|
248
|
+
Returns {"free": [...], "paid": [...], "unknown": [...]}
|
|
249
|
+
"""
|
|
250
|
+
groups: dict[str, list[dict]] = {"free": [], "paid": [], "unknown": []}
|
|
251
|
+
for entry in enriched:
|
|
252
|
+
tier = entry.get("pricing_tier", "unknown")
|
|
253
|
+
groups.setdefault(tier, []).append(entry)
|
|
254
|
+
return groups
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ── Re-exports (new manager API convenience) ───────────────────────────────
|
|
258
|
+
|
|
259
|
+
def get_providers() -> list[dict]:
|
|
260
|
+
"""All provider metadata from providers.json."""
|
|
261
|
+
return manager.list_providers()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_provider(provider_id: str) -> Optional[dict]:
|
|
265
|
+
return manager.get_provider(provider_id)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal/models/schema.py
|
|
3
|
+
==============================
|
|
4
|
+
Standardized Model Metadata Schema for CAT's Dynamic Model Registry.
|
|
5
|
+
|
|
6
|
+
Represents AI models in a unified, provider-agnostic format supporting:
|
|
7
|
+
- Canonical model identity across multiple deployment endpoints.
|
|
8
|
+
- Granular capability detection (reasoning, coding, vision, computer use, etc.).
|
|
9
|
+
- Multi-level reasoning configurations (e.g. low, medium, high, xhigh, max).
|
|
10
|
+
- Clear access tier categorization (open_weight, free_api, paid_api, local, cloud).
|
|
11
|
+
- Release and verification tracking (first_seen, last_seen, last_verified).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field, asdict
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import List, Dict, Any, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Standard Availability States
|
|
20
|
+
AVAILABILITY_API = "api"
|
|
21
|
+
AVAILABILITY_OPEN_WEIGHT = "open_weight"
|
|
22
|
+
AVAILABILITY_FREE_API = "free_api"
|
|
23
|
+
AVAILABILITY_FREE_TIER = "free_tier"
|
|
24
|
+
AVAILABILITY_PAID_API = "paid_api"
|
|
25
|
+
AVAILABILITY_LOCAL = "local"
|
|
26
|
+
AVAILABILITY_CLOUD = "cloud"
|
|
27
|
+
AVAILABILITY_SUBSCRIPTION = "subscription"
|
|
28
|
+
|
|
29
|
+
# Standard Model Statuses
|
|
30
|
+
STATUS_ACTIVE = "active"
|
|
31
|
+
STATUS_PREVIEW = "preview"
|
|
32
|
+
STATUS_BETA = "beta"
|
|
33
|
+
STATUS_DEPRECATED = "deprecated"
|
|
34
|
+
STATUS_RETIRED = "retired"
|
|
35
|
+
STATUS_UNAVAILABLE = "unavailable"
|
|
36
|
+
STATUS_AUTH_REQUIRED = "auth_required"
|
|
37
|
+
STATUS_DISCOVERED_UNAVAILABLE = "discovered_but_unavailable"
|
|
38
|
+
|
|
39
|
+
# Standard Categories
|
|
40
|
+
CATEGORY_GENERAL = "general"
|
|
41
|
+
CATEGORY_REASONING = "reasoning"
|
|
42
|
+
CATEGORY_CODING = "coding"
|
|
43
|
+
CATEGORY_FAST = "fast"
|
|
44
|
+
CATEGORY_CHEAP = "cheap"
|
|
45
|
+
CATEGORY_LONG_CONTEXT = "long_context"
|
|
46
|
+
CATEGORY_VISION = "vision"
|
|
47
|
+
CATEGORY_AUDIO = "audio"
|
|
48
|
+
CATEGORY_MULTIMODAL = "multimodal"
|
|
49
|
+
CATEGORY_AGENT = "agent"
|
|
50
|
+
CATEGORY_RESEARCH = "research"
|
|
51
|
+
CATEGORY_LOCAL = "local"
|
|
52
|
+
CATEGORY_OPEN_WEIGHT = "open_weight"
|
|
53
|
+
CATEGORY_EMBEDDING = "embedding"
|
|
54
|
+
CATEGORY_SPECIALIZED = "specialized"
|
|
55
|
+
CATEGORY_FREE_API = "free"
|
|
56
|
+
CATEGORY_PAID_API = "paid"
|
|
57
|
+
AVAILABILITY_FREE_TIER = "free_tier"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _now_iso() -> str:
|
|
61
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ModelInfo:
|
|
66
|
+
"""Standardized representation of an AI model in CAT."""
|
|
67
|
+
|
|
68
|
+
provider: str
|
|
69
|
+
model_id: str
|
|
70
|
+
display_name: str = ""
|
|
71
|
+
family: str = ""
|
|
72
|
+
version: str = ""
|
|
73
|
+
canonical_model_id: str = ""
|
|
74
|
+
release_date: Optional[str] = None
|
|
75
|
+
status: str = STATUS_ACTIVE
|
|
76
|
+
availability: str = AVAILABILITY_PAID_API
|
|
77
|
+
modalities: List[str] = field(default_factory=lambda: ["text"])
|
|
78
|
+
capabilities: List[str] = field(default_factory=lambda: ["chat", "streaming"])
|
|
79
|
+
context_window: int = 128000
|
|
80
|
+
max_output_tokens: int = 4096
|
|
81
|
+
reasoning_levels: List[str] = field(default_factory=list)
|
|
82
|
+
pricing: Dict[str, float] = field(default_factory=lambda: {"input_price_per_1m": 0.0, "output_price_per_1m": 0.0})
|
|
83
|
+
endpoint: str = ""
|
|
84
|
+
authentication_required: bool = True
|
|
85
|
+
local: bool = False
|
|
86
|
+
cloud: bool = True
|
|
87
|
+
verified: bool = False
|
|
88
|
+
source: str = "official_api"
|
|
89
|
+
first_seen: str = field(default_factory=_now_iso)
|
|
90
|
+
last_seen: str = field(default_factory=_now_iso)
|
|
91
|
+
last_verified: str = field(default_factory=_now_iso)
|
|
92
|
+
stale: bool = False
|
|
93
|
+
tags: List[str] = field(default_factory=list)
|
|
94
|
+
recommendation_badges: List[str] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
def __post_init__(self):
|
|
97
|
+
if not self.display_name:
|
|
98
|
+
self.display_name = self.model_id
|
|
99
|
+
if not self.canonical_model_id:
|
|
100
|
+
self.canonical_model_id = f"{self.provider}:{self.model_id}"
|
|
101
|
+
if not self.family:
|
|
102
|
+
self.family = self._derive_family()
|
|
103
|
+
|
|
104
|
+
def _derive_family(self) -> str:
|
|
105
|
+
mid = self.model_id.lower()
|
|
106
|
+
if "gpt" in mid or "o1" in mid or "o3" in mid:
|
|
107
|
+
return "GPT"
|
|
108
|
+
if "claude" in mid:
|
|
109
|
+
return "Claude"
|
|
110
|
+
if "gemini" in mid:
|
|
111
|
+
return "Gemini"
|
|
112
|
+
if "deepseek" in mid:
|
|
113
|
+
return "DeepSeek"
|
|
114
|
+
if "qwen" in mid:
|
|
115
|
+
return "Qwen"
|
|
116
|
+
if "kimi" in mid or "moonshot" in mid:
|
|
117
|
+
return "Kimi"
|
|
118
|
+
if "glm" in mid:
|
|
119
|
+
return "GLM"
|
|
120
|
+
if "grok" in mid:
|
|
121
|
+
return "Grok"
|
|
122
|
+
if "mistral" in mid or "codestral" in mid:
|
|
123
|
+
return "Mistral"
|
|
124
|
+
if "command" in mid:
|
|
125
|
+
return "Command"
|
|
126
|
+
if "llama" in mid:
|
|
127
|
+
return "Llama"
|
|
128
|
+
if "minimax" in mid or "abab" in mid:
|
|
129
|
+
return "MiniMax"
|
|
130
|
+
if "hunyuan" in mid or "hy" in mid:
|
|
131
|
+
return "Hunyuan"
|
|
132
|
+
if "doubao" in mid or "seed" in mid:
|
|
133
|
+
return "Seed"
|
|
134
|
+
if "ernie" in mid:
|
|
135
|
+
return "ERNIE"
|
|
136
|
+
return self.provider.capitalize()
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def availability_state(self) -> str:
|
|
140
|
+
return self.status
|
|
141
|
+
|
|
142
|
+
def is_usable(self) -> bool:
|
|
143
|
+
"""A model is usable only when it has an active/preview state,
|
|
144
|
+
is verified or from an official provider endpoint, and is not
|
|
145
|
+
marked as discovered_but_unavailable."""
|
|
146
|
+
return (
|
|
147
|
+
self.status in (STATUS_ACTIVE, STATUS_PREVIEW, STATUS_BETA)
|
|
148
|
+
and not self.status == STATUS_DISCOVERED_UNAVAILABLE
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def has_capability(self, capability: str) -> bool:
|
|
152
|
+
cap = capability.lower().strip()
|
|
153
|
+
return cap in [c.lower() for c in self.capabilities]
|
|
154
|
+
|
|
155
|
+
def matches_category(self, category: str) -> bool:
|
|
156
|
+
cat = category.lower().strip()
|
|
157
|
+
if cat in ("all", "*"):
|
|
158
|
+
return True
|
|
159
|
+
if cat == CATEGORY_LOCAL:
|
|
160
|
+
return self.local
|
|
161
|
+
if cat == CATEGORY_OPEN_WEIGHT:
|
|
162
|
+
return self.availability == AVAILABILITY_OPEN_WEIGHT or "open_weight" in self.tags
|
|
163
|
+
if cat in (CATEGORY_FREE_API, "free"):
|
|
164
|
+
return self.availability in (AVAILABILITY_FREE_API, AVAILABILITY_FREE_TIER) or self.pricing.get("input_price_per_1m", 0.0) == 0.0
|
|
165
|
+
if cat in (CATEGORY_PAID_API, "paid"):
|
|
166
|
+
return self.pricing.get("input_price_per_1m", 0.0) > 0.0
|
|
167
|
+
if cat == CATEGORY_REASONING:
|
|
168
|
+
return self.has_capability("reasoning") or bool(self.reasoning_levels) or any(k in self.model_id.lower() for k in ("reasoner", "r1", "o1", "o3", "qwq", "k3", "thinking"))
|
|
169
|
+
if cat == CATEGORY_CODING:
|
|
170
|
+
return self.has_capability("coding") or any(k in self.model_id.lower() for k in ("coder", "codestral", "astra", "code"))
|
|
171
|
+
if cat == CATEGORY_VISION:
|
|
172
|
+
return self.has_capability("vision") or "image" in self.modalities or any(k in self.model_id.lower() for k in ("vision", "4o", "4v", "pixtral", "vl", "gemini"))
|
|
173
|
+
if cat == CATEGORY_FAST:
|
|
174
|
+
return self.has_capability("fast") or any(k in self.model_id.lower() for k in ("mini", "flash", "turbo", "instant", "haiku", "8b"))
|
|
175
|
+
if cat == CATEGORY_LONG_CONTEXT:
|
|
176
|
+
return self.context_window >= 128000
|
|
177
|
+
if cat == CATEGORY_AGENT:
|
|
178
|
+
return self.has_capability("agent") or self.has_capability("computer_use") or self.has_capability("tools")
|
|
179
|
+
if cat == CATEGORY_RESEARCH:
|
|
180
|
+
return self.has_capability("research") or self.matches_category(CATEGORY_REASONING)
|
|
181
|
+
return self.has_capability(cat) or cat in self.tags
|
|
182
|
+
|
|
183
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
184
|
+
return asdict(self)
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ModelInfo":
|
|
188
|
+
cleaned = dict(data)
|
|
189
|
+
# Handle backward-compatible fields
|
|
190
|
+
if "id" in cleaned and "model_id" not in cleaned:
|
|
191
|
+
cleaned["model_id"] = cleaned.pop("id")
|
|
192
|
+
if "context_length" in cleaned and "context_window" not in cleaned:
|
|
193
|
+
cleaned["context_window"] = int(cleaned.pop("context_length") or 128000)
|
|
194
|
+
# Retain only recognized dataclass keys
|
|
195
|
+
valid_keys = {f for f in cls.__dataclass_fields__}
|
|
196
|
+
filtered = {k: v for k, v in cleaned.items() if k in valid_keys}
|
|
197
|
+
return cls(**filtered)
|