velune-cli 1.0.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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +212 -0
- velune/cli/autocomplete.py +76 -0
- velune/cli/banner.py +98 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +149 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +188 -0
- velune/cli/commands/config.py +182 -0
- velune/cli/commands/daemon.py +85 -0
- velune/cli/commands/doctor.py +373 -0
- velune/cli/commands/init.py +160 -0
- velune/cli/commands/mcp.py +80 -0
- velune/cli/commands/memory.py +269 -0
- velune/cli/commands/models.py +462 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +171 -0
- velune/cli/commands/setup.py +182 -0
- velune/cli/commands/workspace.py +217 -0
- velune/cli/context.py +37 -0
- velune/cli/councilmodel_ui.py +171 -0
- velune/cli/display/council_view.py +240 -0
- velune/cli/display/memory_view.py +93 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +21 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +44 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +118 -0
- velune/cli/registry.py +81 -0
- velune/cli/repl.py +1178 -0
- velune/cli/session_manager.py +69 -0
- velune/cli/slash_commands.py +37 -0
- velune/cognition/__init__.py +19 -0
- velune/cognition/arbitrator.py +216 -0
- velune/cognition/architecture.py +398 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +216 -0
- velune/cognition/council/challenger.py +70 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +39 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +44 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +53 -0
- velune/cognition/council/planner.py +119 -0
- velune/cognition/council/reviewer.py +72 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +181 -0
- velune/cognition/firewall.py +256 -0
- velune/cognition/module.py +38 -0
- velune/cognition/orchestrator.py +886 -0
- velune/cognition/personality.py +236 -0
- velune/cognition/style_resolver.py +62 -0
- velune/cognition/verification.py +201 -0
- velune/context/__init__.py +11 -0
- velune/context/extractive.py +94 -0
- velune/context/window.py +62 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +53 -0
- velune/core/errors/execution.py +26 -0
- velune/core/errors/memory.py +21 -0
- velune/core/errors/orchestration.py +26 -0
- velune/core/errors/provider.py +31 -0
- velune/core/event_loop.py +30 -0
- velune/core/logging.py +83 -0
- velune/core/runtime.py +106 -0
- velune/core/task_registry.py +120 -0
- velune/core/trace.py +80 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +49 -0
- velune/core/types/context.py +39 -0
- velune/core/types/inference.py +35 -0
- velune/core/types/memory.py +39 -0
- velune/core/types/model.py +64 -0
- velune/core/types/provider.py +35 -0
- velune/core/types/repository.py +35 -0
- velune/core/types/task.py +56 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +115 -0
- velune/daemon/transport.py +169 -0
- velune/events.py +194 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +311 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +128 -0
- velune/execution/command_spec.py +140 -0
- velune/execution/diff_preview.py +172 -0
- velune/execution/executor.py +173 -0
- velune/execution/module.py +16 -0
- velune/execution/multi_diff.py +70 -0
- velune/execution/path_guard.py +19 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +80 -0
- velune/execution/sandbox.py +257 -0
- velune/execution/validator.py +113 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +162 -0
- velune/kernel/__init__.py +58 -0
- velune/kernel/bootstrap.py +107 -0
- velune/kernel/config.py +252 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +102 -0
- velune/kernel/module.py +15 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +93 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +113 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +90 -0
- velune/memory/__init__.py +28 -0
- velune/memory/lifecycle.py +154 -0
- velune/memory/module.py +94 -0
- velune/memory/prioritizer.py +65 -0
- velune/memory/storage/sqlite_manager.py +368 -0
- velune/memory/tiers/episodic.py +156 -0
- velune/memory/tiers/graph.py +282 -0
- velune/memory/tiers/lineage.py +367 -0
- velune/memory/tiers/semantic.py +198 -0
- velune/memory/tiers/working.py +165 -0
- velune/models/__init__.py +16 -0
- velune/models/module.py +18 -0
- velune/models/probes.py +182 -0
- velune/models/profile_cache.py +82 -0
- velune/models/profiler.py +105 -0
- velune/models/registry.py +201 -0
- velune/models/scorer.py +225 -0
- velune/models/specializations.py +196 -0
- velune/orchestration/__init__.py +15 -0
- velune/orchestration/module.py +14 -0
- velune/orchestration/role_assignments.py +78 -0
- velune/orchestration/schemas.py +99 -0
- velune/plugins/__init__.py +13 -0
- velune/plugins/hooks.py +49 -0
- velune/plugins/loader.py +95 -0
- velune/plugins/registry.py +54 -0
- velune/plugins/schemas.py +19 -0
- velune/providers/__init__.py +18 -0
- velune/providers/adapters/anthropic.py +231 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +203 -0
- velune/providers/adapters/llamacpp.py +202 -0
- velune/providers/adapters/lmstudio.py +173 -0
- velune/providers/adapters/ollama.py +186 -0
- velune/providers/adapters/openai.py +207 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +135 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +77 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +87 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +109 -0
- velune/providers/discovery/groq.py +20 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +165 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +114 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/keystore.py +83 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +208 -0
- velune/providers/module.py +15 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +173 -0
- velune/providers/router.py +82 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +21 -0
- velune/repository/analyzer.py +123 -0
- velune/repository/cognition.py +172 -0
- velune/repository/grapher.py +182 -0
- velune/repository/indexer.py +229 -0
- velune/repository/module.py +15 -0
- velune/repository/parser.py +378 -0
- velune/repository/project_type.py +293 -0
- velune/repository/scanner.py +177 -0
- velune/repository/schemas.py +102 -0
- velune/repository/tracker.py +233 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/graph.py +117 -0
- velune/retrieval/hybrid.py +250 -0
- velune/retrieval/keyword.py +113 -0
- velune/retrieval/module.py +19 -0
- velune/retrieval/reranker.py +68 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/vector.py +163 -0
- velune/telemetry/__init__.py +7 -0
- velune/telemetry/cognition.py +260 -0
- velune/telemetry/token_tracker.py +133 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +84 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +107 -0
- velune/tools/code/search.py +112 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +123 -0
- velune/tools/filesystem/write.py +160 -0
- velune/tools/git/history.py +185 -0
- velune/tools/git/operations.py +132 -0
- velune/tools/git/state.py +134 -0
- velune/tools/module.py +65 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +96 -0
- velune_cli-1.0.0.dist-info/METADATA +497 -0
- velune_cli-1.0.0.dist-info/RECORD +229 -0
- velune_cli-1.0.0.dist-info/WHEEL +4 -0
- velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
- velune_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""Models command - velune models scan/list/assign."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from velune.cli.context import CLIContext
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
models_cmd = typer.Typer(help="Model management commands")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@models_cmd.command("scan")
|
|
19
|
+
def models_scan(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
provider: str = typer.Option(None, "--provider", "-p", help="Specific provider to scan"),
|
|
22
|
+
probe: bool = typer.Option(False, "--probe", help="Run empirical capability probes synchronously and cache results"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Scan for available models."""
|
|
25
|
+
cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
|
|
26
|
+
if cli_context is None:
|
|
27
|
+
if ctx.obj and getattr(ctx.obj, "json_mode", False):
|
|
28
|
+
import json
|
|
29
|
+
print(json.dumps({"error": "Model discovery service is unavailable"}))
|
|
30
|
+
else:
|
|
31
|
+
console.print("[red]Model discovery service is unavailable.[/red]")
|
|
32
|
+
raise typer.Exit(code=1)
|
|
33
|
+
|
|
34
|
+
from velune.core.event_loop import submit
|
|
35
|
+
records = submit(_models_scan_async(cli_context, provider, probe))
|
|
36
|
+
|
|
37
|
+
from velune.core.types.model import CapabilityLevel
|
|
38
|
+
|
|
39
|
+
if cli_context.json_mode:
|
|
40
|
+
import json
|
|
41
|
+
out = []
|
|
42
|
+
for record in records:
|
|
43
|
+
capabilities_map = {
|
|
44
|
+
"coding": record.capabilities.coding,
|
|
45
|
+
"reasoning": record.capabilities.reasoning,
|
|
46
|
+
"planning": record.capabilities.planning,
|
|
47
|
+
"summarization": record.capabilities.summarization,
|
|
48
|
+
"tool_use": record.capabilities.tool_use,
|
|
49
|
+
}
|
|
50
|
+
highest_cap = "general"
|
|
51
|
+
highest_level = CapabilityLevel.NONE
|
|
52
|
+
for cap_name, level in capabilities_map.items():
|
|
53
|
+
if level > highest_level:
|
|
54
|
+
highest_level = level
|
|
55
|
+
highest_cap = cap_name
|
|
56
|
+
specialization = highest_cap if highest_level > CapabilityLevel.NONE else "general"
|
|
57
|
+
embedding_supported = record.capabilities.embedding > CapabilityLevel.NONE
|
|
58
|
+
validated = record.metadata.get("validated")
|
|
59
|
+
status = "cached" if validated is None else ("online" if validated else "offline")
|
|
60
|
+
|
|
61
|
+
out.append({
|
|
62
|
+
"provider_id": record.provider_id,
|
|
63
|
+
"model_id": record.model_id,
|
|
64
|
+
"specialization": specialization,
|
|
65
|
+
"speed_tier": record.speed_tier,
|
|
66
|
+
"context_length": record.context_length,
|
|
67
|
+
"embedding_supported": embedding_supported,
|
|
68
|
+
"status": status,
|
|
69
|
+
})
|
|
70
|
+
print(json.dumps(out))
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
table = Table(title="Discovered Models")
|
|
74
|
+
table.add_column("Provider", style="cyan")
|
|
75
|
+
table.add_column("Model", style="green")
|
|
76
|
+
table.add_column("Specialization", style="magenta")
|
|
77
|
+
table.add_column("Speed", style="blue")
|
|
78
|
+
table.add_column("Context", style="yellow")
|
|
79
|
+
table.add_column("Embedding", style="white")
|
|
80
|
+
table.add_column("Status", style="bold")
|
|
81
|
+
|
|
82
|
+
for record in records:
|
|
83
|
+
capabilities_map = {
|
|
84
|
+
"coding": record.capabilities.coding,
|
|
85
|
+
"reasoning": record.capabilities.reasoning,
|
|
86
|
+
"planning": record.capabilities.planning,
|
|
87
|
+
"summarization": record.capabilities.summarization,
|
|
88
|
+
"tool_use": record.capabilities.tool_use,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
highest_cap = "general"
|
|
92
|
+
highest_level = CapabilityLevel.NONE
|
|
93
|
+
for cap_name, level in capabilities_map.items():
|
|
94
|
+
if level > highest_level:
|
|
95
|
+
highest_level = level
|
|
96
|
+
highest_cap = cap_name
|
|
97
|
+
|
|
98
|
+
specialization = highest_cap if highest_level > CapabilityLevel.NONE else "general"
|
|
99
|
+
embedding_supported = "yes" if record.capabilities.embedding > CapabilityLevel.NONE else "no"
|
|
100
|
+
|
|
101
|
+
validated = record.metadata.get("validated")
|
|
102
|
+
if validated is None:
|
|
103
|
+
status = "[dim]cached[/dim]"
|
|
104
|
+
elif validated:
|
|
105
|
+
status = "[green]●[/green]"
|
|
106
|
+
else:
|
|
107
|
+
status = "[red]✗ offline[/red]"
|
|
108
|
+
|
|
109
|
+
table.add_row(
|
|
110
|
+
record.provider_id,
|
|
111
|
+
record.model_id,
|
|
112
|
+
specialization,
|
|
113
|
+
record.speed_tier,
|
|
114
|
+
str(record.context_length),
|
|
115
|
+
embedding_supported,
|
|
116
|
+
status,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
console.print(table)
|
|
120
|
+
|
|
121
|
+
total = len(records)
|
|
122
|
+
providers = set(r.provider_id for r in records)
|
|
123
|
+
console.print(
|
|
124
|
+
f"[dim]Discovered {total} model(s) across {len(providers)} provider(s).[/dim]"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _models_scan_async(
|
|
129
|
+
cli_context: CLIContext,
|
|
130
|
+
provider_id: str | None,
|
|
131
|
+
probe: bool
|
|
132
|
+
) -> Any:
|
|
133
|
+
container = cli_context.container
|
|
134
|
+
lifecycle = container.get("runtime.lifecycle")
|
|
135
|
+
discovery = container.get("runtime.model_discovery")
|
|
136
|
+
provider_registry = container.get("runtime.provider_registry")
|
|
137
|
+
|
|
138
|
+
if probe:
|
|
139
|
+
await lifecycle.startup()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if provider_id:
|
|
143
|
+
records = await discovery.scan_provider(provider_id=provider_id)
|
|
144
|
+
else:
|
|
145
|
+
records = await discovery.scan_all()
|
|
146
|
+
|
|
147
|
+
if probe:
|
|
148
|
+
from pathlib import Path
|
|
149
|
+
|
|
150
|
+
from velune.models.probes import FastProbe, ModelProber
|
|
151
|
+
from velune.models.profile_cache import ModelProfileCache
|
|
152
|
+
|
|
153
|
+
profile_cache = ModelProfileCache(Path(".velune") / "model_profiles.json")
|
|
154
|
+
fast_probe = FastProbe()
|
|
155
|
+
|
|
156
|
+
if not cli_context.json_mode:
|
|
157
|
+
console.print("[bold cyan]⠋[/bold cyan] Probing discovered models synchronously...")
|
|
158
|
+
|
|
159
|
+
probe_tasks = []
|
|
160
|
+
valid_records = []
|
|
161
|
+
|
|
162
|
+
for record in records:
|
|
163
|
+
provider = provider_registry.get(record.provider_id)
|
|
164
|
+
if provider:
|
|
165
|
+
valid_records.append(record)
|
|
166
|
+
probe_tasks.append(fast_probe.ping(provider, record.model_id))
|
|
167
|
+
|
|
168
|
+
if valid_records:
|
|
169
|
+
import asyncio
|
|
170
|
+
responsiveness = await asyncio.gather(*probe_tasks, return_exceptions=True)
|
|
171
|
+
|
|
172
|
+
empirical_probe_tasks = []
|
|
173
|
+
probing_models = []
|
|
174
|
+
|
|
175
|
+
for record, is_responsive in zip(valid_records, responsiveness):
|
|
176
|
+
if is_responsive is True:
|
|
177
|
+
provider = provider_registry.get(record.provider_id)
|
|
178
|
+
prober = ModelProber(provider, record.model_id)
|
|
179
|
+
probing_models.append((record, prober))
|
|
180
|
+
empirical_probe_tasks.append(prober.run_all_probes())
|
|
181
|
+
record.metadata["validated"] = True
|
|
182
|
+
else:
|
|
183
|
+
record.metadata["validated"] = False
|
|
184
|
+
|
|
185
|
+
if empirical_probe_tasks:
|
|
186
|
+
if not cli_context.json_mode:
|
|
187
|
+
console.print(f"[bold magenta]⚡ Running empirical capability probes for {len(empirical_probe_tasks)} active model(s)...[/bold magenta]")
|
|
188
|
+
results = await asyncio.gather(*empirical_probe_tasks, return_exceptions=True)
|
|
189
|
+
|
|
190
|
+
for (record, prober), result in zip(probing_models, results):
|
|
191
|
+
# gather(return_exceptions=True) can also surface BaseException
|
|
192
|
+
# subclasses (e.g. asyncio.CancelledError); treat any of them
|
|
193
|
+
# as a failed probe so they are never cached as valid results.
|
|
194
|
+
if isinstance(result, BaseException):
|
|
195
|
+
if not cli_context.json_mode:
|
|
196
|
+
console.print(f"[red]✗[/red] Probe failed for {record.model_id}: {result}")
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
profile_cache.set(record.model_id, record.provider_id, result)
|
|
200
|
+
|
|
201
|
+
registry = container.get("runtime.model_registry")
|
|
202
|
+
if registry:
|
|
203
|
+
registry._apply_probe_results(record, result)
|
|
204
|
+
registry.register(record)
|
|
205
|
+
|
|
206
|
+
if not cli_context.json_mode:
|
|
207
|
+
console.print("[bold green]✓[/bold green] Empirical benchmarks completed and cached.")
|
|
208
|
+
|
|
209
|
+
return records
|
|
210
|
+
finally:
|
|
211
|
+
if probe:
|
|
212
|
+
await lifecycle.shutdown()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@models_cmd.command("list")
|
|
216
|
+
def models_list(ctx: typer.Context) -> None:
|
|
217
|
+
"""List registered models."""
|
|
218
|
+
cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
|
|
219
|
+
registry = cli_context.container.get("runtime.model_registry") if cli_context else None
|
|
220
|
+
|
|
221
|
+
if cli_context and cli_context.json_mode:
|
|
222
|
+
import json
|
|
223
|
+
out = []
|
|
224
|
+
if registry is not None:
|
|
225
|
+
from velune.core.types.model import CapabilityLevel
|
|
226
|
+
records = registry.list_all()
|
|
227
|
+
for record in records:
|
|
228
|
+
capabilities = []
|
|
229
|
+
for cap_name in ["coding", "reasoning", "planning", "summarization", "tool_use", "long_context"]:
|
|
230
|
+
level = getattr(record.capabilities, cap_name, None)
|
|
231
|
+
if level and level > CapabilityLevel.NONE:
|
|
232
|
+
capabilities.append(cap_name)
|
|
233
|
+
out.append({
|
|
234
|
+
"model_id": record.model_id,
|
|
235
|
+
"display_name": record.display_name,
|
|
236
|
+
"provider_id": record.provider_id,
|
|
237
|
+
"capabilities": capabilities,
|
|
238
|
+
})
|
|
239
|
+
print(json.dumps(out))
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
table = Table(title="Registered Models")
|
|
243
|
+
table.add_column("ID", style="cyan")
|
|
244
|
+
table.add_column("Name", style="green")
|
|
245
|
+
table.add_column("Provider", style="magenta")
|
|
246
|
+
table.add_column("Capabilities", style="blue")
|
|
247
|
+
|
|
248
|
+
records = []
|
|
249
|
+
if registry is None:
|
|
250
|
+
table.add_row("<uninitialized>", "Velune", "system", "bootstrap only")
|
|
251
|
+
else:
|
|
252
|
+
from velune.core.types.model import CapabilityLevel
|
|
253
|
+
records = registry.list_all()
|
|
254
|
+
for record in records:
|
|
255
|
+
capabilities = []
|
|
256
|
+
for cap_name in ["coding", "reasoning", "planning", "summarization", "tool_use", "long_context"]:
|
|
257
|
+
level = getattr(record.capabilities, cap_name, None)
|
|
258
|
+
if level and level > CapabilityLevel.NONE:
|
|
259
|
+
capabilities.append(f"{cap_name} ({level.name})")
|
|
260
|
+
|
|
261
|
+
table.add_row(
|
|
262
|
+
record.model_id,
|
|
263
|
+
record.display_name,
|
|
264
|
+
record.provider_id,
|
|
265
|
+
", ".join(capabilities) or "none",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
console.print(table)
|
|
269
|
+
|
|
270
|
+
# Get GPU info and show VRAM details
|
|
271
|
+
gpu_info = None
|
|
272
|
+
if cli_context:
|
|
273
|
+
try:
|
|
274
|
+
gpu_info = cli_context.container.get("runtime.gpu_info")
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
if gpu_info and gpu_info.get("has_gpu"):
|
|
279
|
+
free_gb = gpu_info.get("vram_free_gb")
|
|
280
|
+
if free_gb is not None:
|
|
281
|
+
console.print(f"[dim]Available VRAM: {free_gb:.1f}GB[/dim]")
|
|
282
|
+
|
|
283
|
+
over_budget = [m for m in records if
|
|
284
|
+
m.vram_required_gb and m.vram_required_gb > free_gb]
|
|
285
|
+
if over_budget:
|
|
286
|
+
console.print(f"[yellow]⚠ {len(over_budget)} models exceed available VRAM[/yellow]")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@models_cmd.command("assign")
|
|
290
|
+
def models_assign(
|
|
291
|
+
ctx: typer.Context,
|
|
292
|
+
role: str = typer.Argument(..., help="Agent role (planner, coder, reviewer, challenger, synthesizer)"),
|
|
293
|
+
model_id: str = typer.Argument(..., help="Model ID to assign"),
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Assign a model to an agent role."""
|
|
296
|
+
cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
|
|
297
|
+
orchestrator = cli_context.container.get("runtime.council_orchestrator") if cli_context else None
|
|
298
|
+
|
|
299
|
+
if orchestrator is None:
|
|
300
|
+
if cli_context and cli_context.json_mode:
|
|
301
|
+
import json
|
|
302
|
+
print(json.dumps({"error": "Council orchestrator is unavailable"}))
|
|
303
|
+
else:
|
|
304
|
+
console.print("[red]Council orchestrator is unavailable.[/red]")
|
|
305
|
+
raise typer.Exit(code=1)
|
|
306
|
+
|
|
307
|
+
mapper = orchestrator.mapper
|
|
308
|
+
try:
|
|
309
|
+
from velune.models.specializations import CouncilRole
|
|
310
|
+
council_role = CouncilRole(role.lower())
|
|
311
|
+
except ValueError:
|
|
312
|
+
if cli_context and cli_context.json_mode:
|
|
313
|
+
import json
|
|
314
|
+
print(json.dumps({"error": f"Invalid role '{role}'"}))
|
|
315
|
+
else:
|
|
316
|
+
console.print(f"[red]Invalid role '{role}'. Must be one of: planner, coder, reviewer, challenger, synthesizer[/red]")
|
|
317
|
+
raise typer.Exit(code=1)
|
|
318
|
+
|
|
319
|
+
# Check if model exists
|
|
320
|
+
registry = cli_context.container.get("runtime.model_registry") if cli_context else None
|
|
321
|
+
if registry:
|
|
322
|
+
descriptor = registry.get(model_id)
|
|
323
|
+
if not descriptor and not (cli_context and cli_context.json_mode):
|
|
324
|
+
console.print(f"[yellow]Warning: Model '{model_id}' is not currently registered/discovered.[/yellow]")
|
|
325
|
+
|
|
326
|
+
mapper.overrides[council_role] = model_id
|
|
327
|
+
if cli_context and cli_context.json_mode:
|
|
328
|
+
import json
|
|
329
|
+
print(json.dumps({"success": True, "role": council_role.value, "model_id": model_id}))
|
|
330
|
+
else:
|
|
331
|
+
console.print(f"[green]Successfully assigned role '{council_role.value}' to model '{model_id}' for the current runtime context.[/green]")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@models_cmd.command("benchmark")
|
|
335
|
+
def models_benchmark(
|
|
336
|
+
ctx: typer.Context,
|
|
337
|
+
model_id: str = typer.Argument(None, help="Specific model ID to benchmark. If omitted, benchmarks all registered models."),
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Run capability probes on a specific model or all registered models."""
|
|
340
|
+
cli_context = ctx.obj if isinstance(ctx.obj, CLIContext) else None
|
|
341
|
+
if not cli_context:
|
|
342
|
+
if ctx.obj and getattr(ctx.obj, "json_mode", False):
|
|
343
|
+
import json
|
|
344
|
+
print(json.dumps({"error": "CLI context is unavailable"}))
|
|
345
|
+
else:
|
|
346
|
+
console.print("[red]CLI context is unavailable.[/red]")
|
|
347
|
+
raise typer.Exit(code=1)
|
|
348
|
+
|
|
349
|
+
registry = cli_context.container.get("runtime.model_registry")
|
|
350
|
+
provider_registry = cli_context.container.get("runtime.provider_registry")
|
|
351
|
+
|
|
352
|
+
if registry is None or provider_registry is None:
|
|
353
|
+
if cli_context.json_mode:
|
|
354
|
+
import json
|
|
355
|
+
print(json.dumps({"error": "Model registry or provider registry is unavailable"}))
|
|
356
|
+
else:
|
|
357
|
+
console.print("[red]Model registry or provider registry is unavailable.[/red]")
|
|
358
|
+
raise typer.Exit(code=1)
|
|
359
|
+
|
|
360
|
+
# Get the models to benchmark
|
|
361
|
+
models_to_probe = []
|
|
362
|
+
if model_id:
|
|
363
|
+
model = registry.get(model_id)
|
|
364
|
+
if not model:
|
|
365
|
+
if cli_context.json_mode:
|
|
366
|
+
import json
|
|
367
|
+
print(json.dumps({"error": f"Model '{model_id}' is not registered"}))
|
|
368
|
+
else:
|
|
369
|
+
console.print(f"[red]Model '{model_id}' is not registered.[/red]")
|
|
370
|
+
raise typer.Exit(code=1)
|
|
371
|
+
models_to_probe.append(model)
|
|
372
|
+
else:
|
|
373
|
+
models_to_probe = registry.list_all()
|
|
374
|
+
|
|
375
|
+
if not models_to_probe:
|
|
376
|
+
if cli_context.json_mode:
|
|
377
|
+
import json
|
|
378
|
+
print(json.dumps([]))
|
|
379
|
+
else:
|
|
380
|
+
console.print("[yellow]No models registered for benchmarking.[/yellow]")
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
from velune.core.event_loop import submit
|
|
384
|
+
submit(_models_benchmark_async(cli_context, registry, provider_registry, models_to_probe))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def _models_benchmark_async(
|
|
388
|
+
cli_context: CLIContext,
|
|
389
|
+
registry: Any,
|
|
390
|
+
provider_registry: Any,
|
|
391
|
+
models_to_probe: list[Any],
|
|
392
|
+
) -> None:
|
|
393
|
+
import json
|
|
394
|
+
from pathlib import Path
|
|
395
|
+
|
|
396
|
+
from velune.models.probes import ModelProber
|
|
397
|
+
from velune.models.profile_cache import ModelProfileCache
|
|
398
|
+
|
|
399
|
+
profile_cache = ModelProfileCache(Path(".velune") / "model_profiles.json")
|
|
400
|
+
|
|
401
|
+
table = Table(title="Model Empirical Benchmark Results")
|
|
402
|
+
table.add_column("Model ID", style="cyan")
|
|
403
|
+
table.add_column("Provider", style="magenta")
|
|
404
|
+
table.add_column("Coding Score (Lat)", style="green")
|
|
405
|
+
table.add_column("Reasoning Score (Lat)", style="blue")
|
|
406
|
+
table.add_column("Instruction Score (Lat)", style="yellow")
|
|
407
|
+
table.add_column("Source", style="white")
|
|
408
|
+
|
|
409
|
+
json_results = []
|
|
410
|
+
|
|
411
|
+
for model in models_to_probe:
|
|
412
|
+
provider = provider_registry.get(model.provider_id)
|
|
413
|
+
if not provider:
|
|
414
|
+
if not cli_context.json_mode:
|
|
415
|
+
console.print(f"[yellow]Skipping model '{model.model_id}': Provider '{model.provider_id}' is not active or available.[/yellow]")
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
if not cli_context.json_mode:
|
|
419
|
+
console.print(f"Benchmarking model [bold cyan]{model.model_id}[/bold cyan] via [bold magenta]{model.provider_id}[/bold magenta]...")
|
|
420
|
+
|
|
421
|
+
prober = ModelProber(provider, model.model_id)
|
|
422
|
+
results = await prober.run_all_probes()
|
|
423
|
+
|
|
424
|
+
# Save to cache
|
|
425
|
+
profile_cache.set(model.model_id, model.provider_id, results)
|
|
426
|
+
# Apply to registry in-memory
|
|
427
|
+
registry._apply_probe_results(model, results)
|
|
428
|
+
|
|
429
|
+
coding = results["coding"]
|
|
430
|
+
reasoning = results["reasoning"]
|
|
431
|
+
instruction = results["instruction"]
|
|
432
|
+
|
|
433
|
+
if cli_context.json_mode:
|
|
434
|
+
json_results.append({
|
|
435
|
+
"model_id": model.model_id,
|
|
436
|
+
"provider_id": model.provider_id,
|
|
437
|
+
"results": {
|
|
438
|
+
"coding": {"score": coding.score, "latency_ms": coding.latency_ms, "passed": coding.passed},
|
|
439
|
+
"reasoning": {"score": reasoning.score, "latency_ms": reasoning.latency_ms, "passed": reasoning.passed},
|
|
440
|
+
"instruction": {"score": instruction.score, "latency_ms": instruction.latency_ms, "passed": instruction.passed}
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
else:
|
|
444
|
+
def format_result(res) -> str:
|
|
445
|
+
if res.latency_ms < 0:
|
|
446
|
+
return "[red]Failed[/red]"
|
|
447
|
+
color = "green" if res.passed else "yellow"
|
|
448
|
+
return f"[{color}]{res.score:.2f}[/{color}] ({res.latency_ms:.0f}ms)"
|
|
449
|
+
|
|
450
|
+
table.add_row(
|
|
451
|
+
model.model_id,
|
|
452
|
+
model.provider_id,
|
|
453
|
+
format_result(coding),
|
|
454
|
+
format_result(reasoning),
|
|
455
|
+
format_result(instruction),
|
|
456
|
+
"empirical (forced)",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if cli_context.json_mode:
|
|
460
|
+
print(json.dumps(json_results))
|
|
461
|
+
else:
|
|
462
|
+
console.print(table)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Preflight check validation for model availability and workspace initialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.box import ROUNDED
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from velune.kernel.registry import ServiceContainer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def run_preflight_check(container: ServiceContainer, console: Console | None = None) -> bool:
|
|
16
|
+
"""Runs preflight checks for models and workspace state.
|
|
17
|
+
|
|
18
|
+
If checks fail, displays a gorgeous error panel with copy-pasteable fix commands and returns False.
|
|
19
|
+
Otherwise returns True.
|
|
20
|
+
"""
|
|
21
|
+
issues = []
|
|
22
|
+
|
|
23
|
+
# 1. Check workspace initialization
|
|
24
|
+
workspace = container.get("runtime.workspace")
|
|
25
|
+
if not isinstance(workspace, Path):
|
|
26
|
+
workspace = Path(workspace)
|
|
27
|
+
|
|
28
|
+
# Guard: workspace must exist and be a git repository. On a brand-new
|
|
29
|
+
# install neither will be true — show a single targeted message and bail
|
|
30
|
+
# early rather than cascading through checks that will all fail.
|
|
31
|
+
if not workspace.exists() or not (workspace / ".git").exists():
|
|
32
|
+
if console:
|
|
33
|
+
console.print()
|
|
34
|
+
console.print(
|
|
35
|
+
Panel(
|
|
36
|
+
Text.from_markup(
|
|
37
|
+
"This doesn't look like a code project yet. Navigate to your project\n"
|
|
38
|
+
"folder and run [bold green]velune workspace init[/bold green] first."
|
|
39
|
+
),
|
|
40
|
+
title="[bold yellow]⚠️ Not a Project Directory[/bold yellow]",
|
|
41
|
+
border_style="yellow",
|
|
42
|
+
box=ROUNDED,
|
|
43
|
+
padding=(1, 2),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
console.print()
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# We check for the presence of the Tree-sitter AST index folder or `.velune` directory structure
|
|
50
|
+
if not (workspace / ".velune" / "index").exists():
|
|
51
|
+
issues.append(
|
|
52
|
+
"Workspace has not been initialized yet.\n"
|
|
53
|
+
" [bold white]Fix:[/bold white] Run the initialization command to parse the codebase:\n"
|
|
54
|
+
" [bold green]velune workspace init[/bold green]"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 2. Check models registry
|
|
58
|
+
registry = container.get("runtime.model_registry")
|
|
59
|
+
models = registry.list_all()
|
|
60
|
+
if not models:
|
|
61
|
+
issues.append(
|
|
62
|
+
"No model providers or local LLM instances were detected.\n"
|
|
63
|
+
" [bold white]Fix:[/bold white] Make sure Ollama/LM-Studio is running, or check API keys, and run:\n"
|
|
64
|
+
" [bold green]velune models scan --probe[/bold green]"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if issues:
|
|
68
|
+
if console:
|
|
69
|
+
console.print()
|
|
70
|
+
|
|
71
|
+
body_elements = [
|
|
72
|
+
"[bold red]Velune preflight check failed with the following blocking issues:[/bold red]\n"
|
|
73
|
+
]
|
|
74
|
+
for i, issue in enumerate(issues, 1):
|
|
75
|
+
body_elements.append(f"\n[bold red]{i}.[/bold red] {issue}\n")
|
|
76
|
+
|
|
77
|
+
body_elements.append(
|
|
78
|
+
"\n[dim]Ensure these preflight requirements are satisfied before running Reasoning Council tasks.[/dim]"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
panel_content = Text.from_markup("".join(body_elements))
|
|
82
|
+
|
|
83
|
+
console.print(
|
|
84
|
+
Panel(
|
|
85
|
+
panel_content,
|
|
86
|
+
title="[bold red]⚠️ Preflight Check Blocked[/bold red]",
|
|
87
|
+
border_style="red",
|
|
88
|
+
box=ROUNDED,
|
|
89
|
+
padding=(1, 2),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
console.print()
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
return True
|