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,113 @@
|
|
|
1
|
+
"""OpenRouter model discovery with 1-hour local cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from velune.core.types.model import CapabilityLevel, ModelCapabilityProfile, ModelDescriptor
|
|
12
|
+
from velune.providers.keystore import get_key
|
|
13
|
+
|
|
14
|
+
_CACHE_TTL = 3600.0 # seconds
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cache_path() -> Path:
|
|
18
|
+
"""Resolve the cache file location, preferring the project .velune dir."""
|
|
19
|
+
project = Path.cwd() / ".velune"
|
|
20
|
+
if project.exists():
|
|
21
|
+
return project / "openrouter_models_cache.json"
|
|
22
|
+
home = Path.home() / ".velune"
|
|
23
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
return home / "openrouter_models_cache.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OpenRouterDiscovery:
|
|
28
|
+
"""Fetches the full OpenRouter model catalogue with a 1-hour disk cache."""
|
|
29
|
+
|
|
30
|
+
provider_id = "openrouter"
|
|
31
|
+
|
|
32
|
+
async def discover(self) -> list[ModelDescriptor]:
|
|
33
|
+
if not get_key("openrouter"):
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
cached = self._load_cache()
|
|
37
|
+
if cached is not None:
|
|
38
|
+
return cached
|
|
39
|
+
|
|
40
|
+
models = await self._fetch()
|
|
41
|
+
self._save_cache(models)
|
|
42
|
+
return models
|
|
43
|
+
|
|
44
|
+
def _load_cache(self) -> list[ModelDescriptor] | None:
|
|
45
|
+
path = _cache_path()
|
|
46
|
+
if not path.exists():
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
if time.time() - data.get("cached_at", 0) > _CACHE_TTL:
|
|
51
|
+
return None
|
|
52
|
+
return [self._raw_to_descriptor(m) for m in data.get("models", [])]
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def _save_cache(self, models: list[ModelDescriptor]) -> None:
|
|
57
|
+
path = _cache_path()
|
|
58
|
+
try:
|
|
59
|
+
raw = [
|
|
60
|
+
{
|
|
61
|
+
"id": m.model_id,
|
|
62
|
+
"name": m.display_name,
|
|
63
|
+
"context_length": m.context_length,
|
|
64
|
+
"cost_per_1k_tokens": m.cost_per_1k_tokens,
|
|
65
|
+
}
|
|
66
|
+
for m in models
|
|
67
|
+
]
|
|
68
|
+
path.write_text(
|
|
69
|
+
json.dumps({"cached_at": time.time(), "models": raw}, indent=2),
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
async def _fetch(self) -> list[ModelDescriptor]:
|
|
76
|
+
api_key = get_key("openrouter")
|
|
77
|
+
try:
|
|
78
|
+
headers = {
|
|
79
|
+
"Authorization": f"Bearer {api_key}",
|
|
80
|
+
"HTTP-Referer": "Velune CLI",
|
|
81
|
+
"X-Title": "Velune CLI",
|
|
82
|
+
}
|
|
83
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
84
|
+
resp = await client.get(
|
|
85
|
+
"https://openrouter.ai/api/v1/models",
|
|
86
|
+
headers=headers,
|
|
87
|
+
)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
data = resp.json()
|
|
90
|
+
return [self._raw_to_descriptor(m) for m in data.get("data", [])]
|
|
91
|
+
except Exception:
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
def _raw_to_descriptor(self, raw: dict) -> ModelDescriptor:
|
|
95
|
+
model_id = raw.get("id", "unknown")
|
|
96
|
+
context = raw.get("context_length") or 4096
|
|
97
|
+
pricing = raw.get("pricing", {})
|
|
98
|
+
cost = float(pricing.get("prompt", 0) or 0) * 1000
|
|
99
|
+
return ModelDescriptor(
|
|
100
|
+
model_id=model_id,
|
|
101
|
+
provider_id="openrouter",
|
|
102
|
+
display_name=raw.get("name") or model_id,
|
|
103
|
+
context_length=context,
|
|
104
|
+
capabilities=ModelCapabilityProfile(
|
|
105
|
+
coding=CapabilityLevel.INTERMEDIATE,
|
|
106
|
+
reasoning=CapabilityLevel.INTERMEDIATE,
|
|
107
|
+
instruction_following=CapabilityLevel.ADVANCED,
|
|
108
|
+
),
|
|
109
|
+
speed_tier="medium",
|
|
110
|
+
cost_per_1k_tokens=cost if cost > 0 else None,
|
|
111
|
+
tags=["cloud", "openrouter"],
|
|
112
|
+
metadata={},
|
|
113
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Unified model discovery coordinator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from velune.core.types.model import ModelDescriptor
|
|
9
|
+
from velune.providers.discovery.anthropic import AnthropicDiscovery
|
|
10
|
+
from velune.providers.discovery.fireworks import FireworksDiscovery
|
|
11
|
+
from velune.providers.discovery.gguf import GGUFDiscovery
|
|
12
|
+
from velune.providers.discovery.google import GoogleDiscovery
|
|
13
|
+
from velune.providers.discovery.groq import GroqDiscovery
|
|
14
|
+
from velune.providers.discovery.huggingface import HuggingFaceDiscovery
|
|
15
|
+
from velune.providers.discovery.lmstudio import LMStudioDiscovery
|
|
16
|
+
from velune.providers.discovery.ollama import OllamaDiscovery
|
|
17
|
+
from velune.providers.discovery.openai import OpenAIDiscovery
|
|
18
|
+
from velune.providers.discovery.openrouter import OpenRouterDiscovery
|
|
19
|
+
from velune.providers.discovery.together import TogetherDiscovery
|
|
20
|
+
from velune.providers.discovery.xai import XAIDiscovery
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("velune.providers.discovery.scanner")
|
|
23
|
+
|
|
24
|
+
# Providers that run locally and need no API key
|
|
25
|
+
_LOCAL_PROVIDERS: frozenset[str] = frozenset({"ollama", "lmstudio", "gguf", "llamacpp"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ModelDiscoveryScanner:
|
|
29
|
+
"""Coordinates model discovery across all configured providers.
|
|
30
|
+
|
|
31
|
+
Cloud discoverers are only invoked when a key is available for that
|
|
32
|
+
provider. Local discoverers always run; Ollama and LM Studio are
|
|
33
|
+
additionally gated on their daemon being reachable.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self.discoverers = [
|
|
38
|
+
OllamaDiscovery(),
|
|
39
|
+
LMStudioDiscovery(),
|
|
40
|
+
GGUFDiscovery(),
|
|
41
|
+
HuggingFaceDiscovery(),
|
|
42
|
+
OpenAIDiscovery(),
|
|
43
|
+
AnthropicDiscovery(),
|
|
44
|
+
XAIDiscovery(),
|
|
45
|
+
GoogleDiscovery(),
|
|
46
|
+
GroqDiscovery(),
|
|
47
|
+
OpenRouterDiscovery(),
|
|
48
|
+
TogetherDiscovery(),
|
|
49
|
+
FireworksDiscovery(),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def _should_run(self, discoverer) -> bool:
|
|
53
|
+
"""Return True if this discoverer should be queried."""
|
|
54
|
+
if discoverer.provider_id in _LOCAL_PROVIDERS:
|
|
55
|
+
return True
|
|
56
|
+
from velune.providers.keystore import has_key
|
|
57
|
+
return has_key(discoverer.provider_id)
|
|
58
|
+
|
|
59
|
+
async def _collect(self, discoverer) -> list[ModelDescriptor]:
|
|
60
|
+
"""Run a single discoverer, returning [] on any failure."""
|
|
61
|
+
try:
|
|
62
|
+
return await discoverer.discover()
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.debug("Discovery failed for %s: %s", discoverer.provider_id, e)
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
async def scan_all(self) -> list[ModelDescriptor]:
|
|
68
|
+
"""Scan all providers for models in parallel."""
|
|
69
|
+
# Gate server-dependent local providers on reachability
|
|
70
|
+
ollama_ok, lmstudio_ok = await asyncio.gather(
|
|
71
|
+
OllamaDiscovery.is_running(),
|
|
72
|
+
LMStudioDiscovery.is_running(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
tasks = []
|
|
76
|
+
for d in self.discoverers:
|
|
77
|
+
if d.provider_id == "ollama" and not ollama_ok:
|
|
78
|
+
continue
|
|
79
|
+
if d.provider_id == "lmstudio" and not lmstudio_ok:
|
|
80
|
+
continue
|
|
81
|
+
if self._should_run(d):
|
|
82
|
+
tasks.append(self._collect(d))
|
|
83
|
+
|
|
84
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
85
|
+
|
|
86
|
+
all_models: list[ModelDescriptor] = []
|
|
87
|
+
for result in results:
|
|
88
|
+
if isinstance(result, list):
|
|
89
|
+
all_models.extend(result)
|
|
90
|
+
|
|
91
|
+
# Summary log
|
|
92
|
+
counts: dict[str, int] = {"gguf": 0, "ollama": 0, "lmstudio": 0, "cloud": 0}
|
|
93
|
+
for m in all_models:
|
|
94
|
+
if m.provider_id in counts:
|
|
95
|
+
counts[m.provider_id] += 1
|
|
96
|
+
elif m.provider_id not in _LOCAL_PROVIDERS:
|
|
97
|
+
counts["cloud"] += 1
|
|
98
|
+
|
|
99
|
+
logger.info(
|
|
100
|
+
"Local: %d GGUF, %d Ollama, %d LM Studio | Cloud: %d models",
|
|
101
|
+
counts["gguf"],
|
|
102
|
+
counts["ollama"],
|
|
103
|
+
counts["lmstudio"],
|
|
104
|
+
counts["cloud"],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return all_models
|
|
108
|
+
|
|
109
|
+
async def scan_provider(self, provider_id: str) -> list[ModelDescriptor]:
|
|
110
|
+
"""Scan a specific provider by ID."""
|
|
111
|
+
for discoverer in self.discoverers:
|
|
112
|
+
if discoverer.provider_id == provider_id:
|
|
113
|
+
return await self._collect(discoverer)
|
|
114
|
+
return []
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Together.AI model discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from velune.core.types.model import CapabilityLevel, ModelCapabilityProfile, ModelDescriptor
|
|
6
|
+
from velune.providers.keystore import get_key
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TogetherDiscovery:
|
|
10
|
+
"""Returns the hardcoded Together.AI model list when a key is configured."""
|
|
11
|
+
|
|
12
|
+
provider_id = "together"
|
|
13
|
+
|
|
14
|
+
async def discover(self) -> list[ModelDescriptor]:
|
|
15
|
+
if not get_key("together"):
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
ModelDescriptor(
|
|
20
|
+
model_id="meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
21
|
+
display_name="Llama 3.3 70B Instruct Turbo",
|
|
22
|
+
provider_id="together",
|
|
23
|
+
context_length=131072,
|
|
24
|
+
capabilities=ModelCapabilityProfile(
|
|
25
|
+
coding=CapabilityLevel.ADVANCED,
|
|
26
|
+
reasoning=CapabilityLevel.ADVANCED,
|
|
27
|
+
planning=CapabilityLevel.ADVANCED,
|
|
28
|
+
summarization=CapabilityLevel.EXPERT,
|
|
29
|
+
instruction_following=CapabilityLevel.EXPERT,
|
|
30
|
+
tool_use=CapabilityLevel.ADVANCED,
|
|
31
|
+
long_context=CapabilityLevel.ADVANCED,
|
|
32
|
+
),
|
|
33
|
+
speed_tier="fast",
|
|
34
|
+
cost_per_1k_tokens=0.00088,
|
|
35
|
+
tags=["cloud", "together", "llama", "turbo"],
|
|
36
|
+
metadata={},
|
|
37
|
+
),
|
|
38
|
+
ModelDescriptor(
|
|
39
|
+
model_id="meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo",
|
|
40
|
+
display_name="Llama 3.2 11B Vision Instruct",
|
|
41
|
+
provider_id="together",
|
|
42
|
+
context_length=131072,
|
|
43
|
+
capabilities=ModelCapabilityProfile(
|
|
44
|
+
coding=CapabilityLevel.INTERMEDIATE,
|
|
45
|
+
reasoning=CapabilityLevel.INTERMEDIATE,
|
|
46
|
+
planning=CapabilityLevel.INTERMEDIATE,
|
|
47
|
+
summarization=CapabilityLevel.ADVANCED,
|
|
48
|
+
instruction_following=CapabilityLevel.ADVANCED,
|
|
49
|
+
tool_use=CapabilityLevel.INTERMEDIATE,
|
|
50
|
+
long_context=CapabilityLevel.INTERMEDIATE,
|
|
51
|
+
),
|
|
52
|
+
speed_tier="fast",
|
|
53
|
+
cost_per_1k_tokens=0.00018,
|
|
54
|
+
tags=["cloud", "together", "llama", "vision", "cheap"],
|
|
55
|
+
metadata={},
|
|
56
|
+
),
|
|
57
|
+
ModelDescriptor(
|
|
58
|
+
model_id="Qwen/Qwen2.5-Coder-32B-Instruct",
|
|
59
|
+
display_name="Qwen 2.5 Coder 32B Instruct",
|
|
60
|
+
provider_id="together",
|
|
61
|
+
context_length=131072,
|
|
62
|
+
capabilities=ModelCapabilityProfile(
|
|
63
|
+
coding=CapabilityLevel.EXPERT,
|
|
64
|
+
reasoning=CapabilityLevel.ADVANCED,
|
|
65
|
+
planning=CapabilityLevel.ADVANCED,
|
|
66
|
+
summarization=CapabilityLevel.ADVANCED,
|
|
67
|
+
instruction_following=CapabilityLevel.EXPERT,
|
|
68
|
+
tool_use=CapabilityLevel.ADVANCED,
|
|
69
|
+
long_context=CapabilityLevel.ADVANCED,
|
|
70
|
+
),
|
|
71
|
+
speed_tier="medium",
|
|
72
|
+
cost_per_1k_tokens=0.0008,
|
|
73
|
+
tags=["cloud", "together", "qwen", "coding"],
|
|
74
|
+
metadata={},
|
|
75
|
+
),
|
|
76
|
+
ModelDescriptor(
|
|
77
|
+
model_id="deepseek-ai/DeepSeek-R1",
|
|
78
|
+
display_name="DeepSeek R1",
|
|
79
|
+
provider_id="together",
|
|
80
|
+
context_length=163840,
|
|
81
|
+
capabilities=ModelCapabilityProfile(
|
|
82
|
+
coding=CapabilityLevel.EXPERT,
|
|
83
|
+
reasoning=CapabilityLevel.EXPERT,
|
|
84
|
+
planning=CapabilityLevel.EXPERT,
|
|
85
|
+
summarization=CapabilityLevel.ADVANCED,
|
|
86
|
+
instruction_following=CapabilityLevel.EXPERT,
|
|
87
|
+
tool_use=CapabilityLevel.ADVANCED,
|
|
88
|
+
long_context=CapabilityLevel.ADVANCED,
|
|
89
|
+
),
|
|
90
|
+
speed_tier="slow",
|
|
91
|
+
cost_per_1k_tokens=0.003,
|
|
92
|
+
tags=["cloud", "together", "deepseek", "reasoning"],
|
|
93
|
+
metadata={},
|
|
94
|
+
),
|
|
95
|
+
ModelDescriptor(
|
|
96
|
+
model_id="mistralai/Mistral-7B-Instruct-v0.3",
|
|
97
|
+
display_name="Mistral 7B Instruct v0.3",
|
|
98
|
+
provider_id="together",
|
|
99
|
+
context_length=32768,
|
|
100
|
+
capabilities=ModelCapabilityProfile(
|
|
101
|
+
coding=CapabilityLevel.INTERMEDIATE,
|
|
102
|
+
reasoning=CapabilityLevel.INTERMEDIATE,
|
|
103
|
+
planning=CapabilityLevel.INTERMEDIATE,
|
|
104
|
+
summarization=CapabilityLevel.ADVANCED,
|
|
105
|
+
instruction_following=CapabilityLevel.ADVANCED,
|
|
106
|
+
tool_use=CapabilityLevel.INTERMEDIATE,
|
|
107
|
+
long_context=CapabilityLevel.BASIC,
|
|
108
|
+
),
|
|
109
|
+
speed_tier="fast",
|
|
110
|
+
cost_per_1k_tokens=0.0002,
|
|
111
|
+
tags=["cloud", "together", "mistral", "cheap"],
|
|
112
|
+
metadata={},
|
|
113
|
+
),
|
|
114
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""xAI (Grok) model discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from velune.core.types.model import CapabilityLevel, ModelCapabilityProfile, ModelDescriptor
|
|
6
|
+
from velune.providers.keystore import get_key
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class XAIDiscovery:
|
|
10
|
+
"""Returns the hardcoded xAI Grok model list when a key is configured."""
|
|
11
|
+
|
|
12
|
+
provider_id = "xai"
|
|
13
|
+
|
|
14
|
+
async def discover(self) -> list[ModelDescriptor]:
|
|
15
|
+
if not get_key("xai"):
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
ModelDescriptor(
|
|
20
|
+
model_id="grok-2",
|
|
21
|
+
provider_id="xai",
|
|
22
|
+
display_name="Grok 2",
|
|
23
|
+
context_length=131072,
|
|
24
|
+
capabilities=ModelCapabilityProfile(
|
|
25
|
+
coding=CapabilityLevel.ADVANCED,
|
|
26
|
+
reasoning=CapabilityLevel.ADVANCED,
|
|
27
|
+
planning=CapabilityLevel.ADVANCED,
|
|
28
|
+
summarization=CapabilityLevel.ADVANCED,
|
|
29
|
+
instruction_following=CapabilityLevel.EXPERT,
|
|
30
|
+
tool_use=CapabilityLevel.ADVANCED,
|
|
31
|
+
long_context=CapabilityLevel.ADVANCED,
|
|
32
|
+
),
|
|
33
|
+
speed_tier="medium",
|
|
34
|
+
cost_per_1k_tokens=0.005,
|
|
35
|
+
tags=["cloud", "xai"],
|
|
36
|
+
metadata={},
|
|
37
|
+
),
|
|
38
|
+
ModelDescriptor(
|
|
39
|
+
model_id="grok-2-mini",
|
|
40
|
+
provider_id="xai",
|
|
41
|
+
display_name="Grok 2 Mini",
|
|
42
|
+
context_length=131072,
|
|
43
|
+
capabilities=ModelCapabilityProfile(
|
|
44
|
+
coding=CapabilityLevel.INTERMEDIATE,
|
|
45
|
+
reasoning=CapabilityLevel.INTERMEDIATE,
|
|
46
|
+
planning=CapabilityLevel.INTERMEDIATE,
|
|
47
|
+
summarization=CapabilityLevel.ADVANCED,
|
|
48
|
+
instruction_following=CapabilityLevel.ADVANCED,
|
|
49
|
+
tool_use=CapabilityLevel.INTERMEDIATE,
|
|
50
|
+
long_context=CapabilityLevel.ADVANCED,
|
|
51
|
+
),
|
|
52
|
+
speed_tier="fast",
|
|
53
|
+
cost_per_1k_tokens=0.0005,
|
|
54
|
+
tags=["cloud", "xai", "mini"],
|
|
55
|
+
metadata={},
|
|
56
|
+
),
|
|
57
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""BYOK key storage backed by the OS keyring with env-var fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import keyring
|
|
8
|
+
import keyring.errors
|
|
9
|
+
|
|
10
|
+
_SERVICE = "velune/{}"
|
|
11
|
+
_USERNAME = "api_key"
|
|
12
|
+
|
|
13
|
+
PROVIDER_ENV_VARS: dict[str, str] = {
|
|
14
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
15
|
+
"openai": "OPENAI_API_KEY",
|
|
16
|
+
"xai": "XAI_API_KEY",
|
|
17
|
+
"google": "GOOGLE_API_KEY",
|
|
18
|
+
"groq": "GROQ_API_KEY",
|
|
19
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
20
|
+
"huggingface": "HF_TOKEN",
|
|
21
|
+
"together": "TOGETHER_API_KEY",
|
|
22
|
+
"fireworks": "FIREWORKS_API_KEY",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_ENV_VARS = PROVIDER_ENV_VARS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_key(provider_id: str, api_key: str) -> None:
|
|
29
|
+
"""Persist *api_key* for *provider_id* in the OS keyring."""
|
|
30
|
+
keyring.set_password(_SERVICE.format(provider_id), _USERNAME, api_key)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_key(provider_id: str) -> str | None:
|
|
34
|
+
"""Return the API key for *provider_id*.
|
|
35
|
+
|
|
36
|
+
Lookup order:
|
|
37
|
+
1. OS keyring (``velune/{provider_id}`` / ``api_key``)
|
|
38
|
+
2. Environment variable mapped in ``_ENV_VARS``
|
|
39
|
+
3. ``None``
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
key = keyring.get_password(_SERVICE.format(provider_id), _USERNAME)
|
|
43
|
+
if key:
|
|
44
|
+
return key
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
env_var = _ENV_VARS.get(provider_id)
|
|
49
|
+
if env_var:
|
|
50
|
+
return os.getenv(env_var) or None
|
|
51
|
+
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def delete_key(provider_id: str) -> None:
|
|
56
|
+
"""Remove the stored key for *provider_id* from the OS keyring."""
|
|
57
|
+
try:
|
|
58
|
+
keyring.delete_password(_SERVICE.format(provider_id), _USERNAME)
|
|
59
|
+
except keyring.errors.PasswordDeleteError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def has_key(provider_id: str) -> bool:
|
|
64
|
+
"""Return True if a key is available for *provider_id*."""
|
|
65
|
+
return get_key(provider_id) is not None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_configured_providers() -> list[str]:
|
|
69
|
+
"""Return provider IDs that have a key in the keyring or environment.
|
|
70
|
+
|
|
71
|
+
Ollama is local and keyless — it counts as configured when its server
|
|
72
|
+
is reachable, not when a key is present.
|
|
73
|
+
"""
|
|
74
|
+
configured = [pid for pid in _ENV_VARS if has_key(pid)]
|
|
75
|
+
# Ollama is local — check if it's running, not if it has a key.
|
|
76
|
+
try:
|
|
77
|
+
import httpx
|
|
78
|
+
r = httpx.get("http://localhost:11434/api/tags", timeout=2.0)
|
|
79
|
+
if r.status_code == 200 and "ollama" not in configured:
|
|
80
|
+
configured.insert(0, "ollama")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return configured
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Persistent JSON cache that maps model_id strings to resolved GGUF file paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _cache_path() -> Path:
|
|
10
|
+
project = Path.cwd() / ".velune"
|
|
11
|
+
if project.exists():
|
|
12
|
+
return project / "model_paths.json"
|
|
13
|
+
home = Path.home() / ".velune"
|
|
14
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
return home / "model_paths.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load() -> dict[str, str]:
|
|
19
|
+
path = _cache_path()
|
|
20
|
+
if path.exists():
|
|
21
|
+
try:
|
|
22
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _save(data: dict[str, str]) -> None:
|
|
29
|
+
try:
|
|
30
|
+
_cache_path().write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def save_model_path(model_id: str, path: Path) -> None:
|
|
36
|
+
"""Persist the resolved *path* for *model_id* in the cache."""
|
|
37
|
+
data = _load()
|
|
38
|
+
data[model_id] = str(path)
|
|
39
|
+
_save(data)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_model_path(model_id: str) -> Path | None:
|
|
43
|
+
"""Return the cached path for *model_id*, or None if missing/stale."""
|
|
44
|
+
val = _load().get(model_id)
|
|
45
|
+
if val:
|
|
46
|
+
p = Path(val)
|
|
47
|
+
if p.exists():
|
|
48
|
+
return p
|
|
49
|
+
return None
|