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.
Files changed (229) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +212 -0
  5. velune/cli/autocomplete.py +76 -0
  6. velune/cli/banner.py +98 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +149 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +188 -0
  11. velune/cli/commands/config.py +182 -0
  12. velune/cli/commands/daemon.py +85 -0
  13. velune/cli/commands/doctor.py +373 -0
  14. velune/cli/commands/init.py +160 -0
  15. velune/cli/commands/mcp.py +80 -0
  16. velune/cli/commands/memory.py +269 -0
  17. velune/cli/commands/models.py +462 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +171 -0
  20. velune/cli/commands/setup.py +182 -0
  21. velune/cli/commands/workspace.py +217 -0
  22. velune/cli/context.py +37 -0
  23. velune/cli/councilmodel_ui.py +171 -0
  24. velune/cli/display/council_view.py +240 -0
  25. velune/cli/display/memory_view.py +93 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +21 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +44 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +118 -0
  33. velune/cli/registry.py +81 -0
  34. velune/cli/repl.py +1178 -0
  35. velune/cli/session_manager.py +69 -0
  36. velune/cli/slash_commands.py +37 -0
  37. velune/cognition/__init__.py +19 -0
  38. velune/cognition/arbitrator.py +216 -0
  39. velune/cognition/architecture.py +398 -0
  40. velune/cognition/council/__init__.py +47 -0
  41. velune/cognition/council/base.py +216 -0
  42. velune/cognition/council/challenger.py +70 -0
  43. velune/cognition/council/coder.py +79 -0
  44. velune/cognition/council/critic_agent.py +39 -0
  45. velune/cognition/council/critic_configs.py +111 -0
  46. velune/cognition/council/critics.py +41 -0
  47. velune/cognition/council/debate.py +44 -0
  48. velune/cognition/council/factory.py +140 -0
  49. velune/cognition/council/messages.py +53 -0
  50. velune/cognition/council/planner.py +119 -0
  51. velune/cognition/council/reviewer.py +72 -0
  52. velune/cognition/council/synthesizer.py +67 -0
  53. velune/cognition/council/tiers.py +181 -0
  54. velune/cognition/firewall.py +256 -0
  55. velune/cognition/module.py +38 -0
  56. velune/cognition/orchestrator.py +886 -0
  57. velune/cognition/personality.py +236 -0
  58. velune/cognition/style_resolver.py +62 -0
  59. velune/cognition/verification.py +201 -0
  60. velune/context/__init__.py +11 -0
  61. velune/context/extractive.py +94 -0
  62. velune/context/window.py +62 -0
  63. velune/core/__init__.py +89 -0
  64. velune/core/background.py +5 -0
  65. velune/core/config/__init__.py +37 -0
  66. velune/core/errors/__init__.py +53 -0
  67. velune/core/errors/execution.py +26 -0
  68. velune/core/errors/memory.py +21 -0
  69. velune/core/errors/orchestration.py +26 -0
  70. velune/core/errors/provider.py +31 -0
  71. velune/core/event_loop.py +30 -0
  72. velune/core/logging.py +83 -0
  73. velune/core/runtime.py +106 -0
  74. velune/core/task_registry.py +120 -0
  75. velune/core/trace.py +80 -0
  76. velune/core/types/__init__.py +48 -0
  77. velune/core/types/agent.py +49 -0
  78. velune/core/types/context.py +39 -0
  79. velune/core/types/inference.py +35 -0
  80. velune/core/types/memory.py +39 -0
  81. velune/core/types/model.py +64 -0
  82. velune/core/types/provider.py +35 -0
  83. velune/core/types/repository.py +35 -0
  84. velune/core/types/task.py +56 -0
  85. velune/core/types/workspace.py +28 -0
  86. velune/daemon/client.py +13 -0
  87. velune/daemon/server.py +115 -0
  88. velune/daemon/transport.py +169 -0
  89. velune/events.py +194 -0
  90. velune/execution/__init__.py +22 -0
  91. velune/execution/benchmarker.py +311 -0
  92. velune/execution/cancellation.py +53 -0
  93. velune/execution/checkpointer.py +128 -0
  94. velune/execution/command_spec.py +140 -0
  95. velune/execution/diff_preview.py +172 -0
  96. velune/execution/executor.py +173 -0
  97. velune/execution/module.py +16 -0
  98. velune/execution/multi_diff.py +70 -0
  99. velune/execution/path_guard.py +19 -0
  100. velune/execution/planner.py +91 -0
  101. velune/execution/rollback.py +80 -0
  102. velune/execution/sandbox.py +257 -0
  103. velune/execution/validator.py +113 -0
  104. velune/hardware/__init__.py +1 -0
  105. velune/hardware/detector.py +162 -0
  106. velune/kernel/__init__.py +58 -0
  107. velune/kernel/bootstrap.py +107 -0
  108. velune/kernel/config.py +252 -0
  109. velune/kernel/health.py +54 -0
  110. velune/kernel/lifecycle.py +102 -0
  111. velune/kernel/module.py +15 -0
  112. velune/kernel/modules.py +23 -0
  113. velune/kernel/registry.py +93 -0
  114. velune/kernel/schemas.py +28 -0
  115. velune/main.py +9 -0
  116. velune/mcp/__init__.py +9 -0
  117. velune/mcp/client.py +113 -0
  118. velune/mcp/config.py +19 -0
  119. velune/mcp/server.py +90 -0
  120. velune/memory/__init__.py +28 -0
  121. velune/memory/lifecycle.py +154 -0
  122. velune/memory/module.py +94 -0
  123. velune/memory/prioritizer.py +65 -0
  124. velune/memory/storage/sqlite_manager.py +368 -0
  125. velune/memory/tiers/episodic.py +156 -0
  126. velune/memory/tiers/graph.py +282 -0
  127. velune/memory/tiers/lineage.py +367 -0
  128. velune/memory/tiers/semantic.py +198 -0
  129. velune/memory/tiers/working.py +165 -0
  130. velune/models/__init__.py +16 -0
  131. velune/models/module.py +18 -0
  132. velune/models/probes.py +182 -0
  133. velune/models/profile_cache.py +82 -0
  134. velune/models/profiler.py +105 -0
  135. velune/models/registry.py +201 -0
  136. velune/models/scorer.py +225 -0
  137. velune/models/specializations.py +196 -0
  138. velune/orchestration/__init__.py +15 -0
  139. velune/orchestration/module.py +14 -0
  140. velune/orchestration/role_assignments.py +78 -0
  141. velune/orchestration/schemas.py +99 -0
  142. velune/plugins/__init__.py +13 -0
  143. velune/plugins/hooks.py +49 -0
  144. velune/plugins/loader.py +95 -0
  145. velune/plugins/registry.py +54 -0
  146. velune/plugins/schemas.py +19 -0
  147. velune/providers/__init__.py +18 -0
  148. velune/providers/adapters/anthropic.py +231 -0
  149. velune/providers/adapters/fireworks.py +115 -0
  150. velune/providers/adapters/google.py +234 -0
  151. velune/providers/adapters/groq.py +151 -0
  152. velune/providers/adapters/huggingface.py +203 -0
  153. velune/providers/adapters/llamacpp.py +202 -0
  154. velune/providers/adapters/lmstudio.py +173 -0
  155. velune/providers/adapters/ollama.py +186 -0
  156. velune/providers/adapters/openai.py +207 -0
  157. velune/providers/adapters/openrouter.py +81 -0
  158. velune/providers/adapters/together.py +134 -0
  159. velune/providers/adapters/xai.py +60 -0
  160. velune/providers/base.py +86 -0
  161. velune/providers/benchmarker.py +135 -0
  162. velune/providers/discovery/__init__.py +33 -0
  163. velune/providers/discovery/anthropic.py +77 -0
  164. velune/providers/discovery/benchmarks.py +44 -0
  165. velune/providers/discovery/classifier.py +69 -0
  166. velune/providers/discovery/fireworks.py +95 -0
  167. velune/providers/discovery/gguf.py +87 -0
  168. velune/providers/discovery/google.py +95 -0
  169. velune/providers/discovery/gpu.py +109 -0
  170. velune/providers/discovery/groq.py +20 -0
  171. velune/providers/discovery/huggingface.py +67 -0
  172. velune/providers/discovery/lmstudio.py +80 -0
  173. velune/providers/discovery/ollama.py +165 -0
  174. velune/providers/discovery/openai.py +96 -0
  175. velune/providers/discovery/openrouter.py +113 -0
  176. velune/providers/discovery/scanner.py +114 -0
  177. velune/providers/discovery/together.py +114 -0
  178. velune/providers/discovery/xai.py +57 -0
  179. velune/providers/keystore.py +83 -0
  180. velune/providers/local_paths.py +49 -0
  181. velune/providers/local_resolver.py +208 -0
  182. velune/providers/module.py +15 -0
  183. velune/providers/ollama_manager.py +193 -0
  184. velune/providers/registry.py +173 -0
  185. velune/providers/router.py +82 -0
  186. velune/py.typed +0 -0
  187. velune/repository/__init__.py +21 -0
  188. velune/repository/analyzer.py +123 -0
  189. velune/repository/cognition.py +172 -0
  190. velune/repository/grapher.py +182 -0
  191. velune/repository/indexer.py +229 -0
  192. velune/repository/module.py +15 -0
  193. velune/repository/parser.py +378 -0
  194. velune/repository/project_type.py +293 -0
  195. velune/repository/scanner.py +177 -0
  196. velune/repository/schemas.py +102 -0
  197. velune/repository/tracker.py +233 -0
  198. velune/retrieval/__init__.py +27 -0
  199. velune/retrieval/graph.py +117 -0
  200. velune/retrieval/hybrid.py +250 -0
  201. velune/retrieval/keyword.py +113 -0
  202. velune/retrieval/module.py +19 -0
  203. velune/retrieval/reranker.py +68 -0
  204. velune/retrieval/schemas.py +59 -0
  205. velune/retrieval/vector.py +163 -0
  206. velune/telemetry/__init__.py +7 -0
  207. velune/telemetry/cognition.py +260 -0
  208. velune/telemetry/token_tracker.py +133 -0
  209. velune/tools/__init__.py +41 -0
  210. velune/tools/base/registry.py +84 -0
  211. velune/tools/base/tool.py +63 -0
  212. velune/tools/code/navigate.py +107 -0
  213. velune/tools/code/search.py +112 -0
  214. velune/tools/filesystem/read.py +75 -0
  215. velune/tools/filesystem/search.py +123 -0
  216. velune/tools/filesystem/write.py +160 -0
  217. velune/tools/git/history.py +185 -0
  218. velune/tools/git/operations.py +132 -0
  219. velune/tools/git/state.py +134 -0
  220. velune/tools/module.py +65 -0
  221. velune/tools/terminal/execute.py +72 -0
  222. velune/tools/terminal/history.py +47 -0
  223. velune/tools/web/fetch.py +55 -0
  224. velune/tools/web/validator.py +96 -0
  225. velune_cli-1.0.0.dist-info/METADATA +497 -0
  226. velune_cli-1.0.0.dist-info/RECORD +229 -0
  227. velune_cli-1.0.0.dist-info/WHEEL +4 -0
  228. velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
  229. 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