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,186 @@
|
|
|
1
|
+
"""Ollama provider adapter implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from velune.core.errors.provider import InferenceError, ProviderConnectionError
|
|
13
|
+
from velune.core.types.inference import InferenceRequest, InferenceResponse, StreamChunk
|
|
14
|
+
from velune.core.types.model import CapabilityLevel, ModelDescriptor
|
|
15
|
+
from velune.core.types.provider import ProviderCapabilities, ProviderHealth
|
|
16
|
+
from velune.providers.base import ModelProvider
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("velune.providers.adapters.ollama")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OllamaProvider(ModelProvider):
|
|
22
|
+
"""Ollama provider for local models."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, base_url: str = "http://localhost:11434") -> None:
|
|
25
|
+
self._base_url = base_url
|
|
26
|
+
self.client: httpx.AsyncClient | None = None
|
|
27
|
+
self._capabilities = ProviderCapabilities(
|
|
28
|
+
supports_streaming=True,
|
|
29
|
+
supports_function_calling=False,
|
|
30
|
+
supports_embeddings=True,
|
|
31
|
+
max_context_window=8192,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def provider_id(self) -> str:
|
|
36
|
+
return "ollama"
|
|
37
|
+
|
|
38
|
+
async def initialize(self) -> None:
|
|
39
|
+
"""Initialize the async client."""
|
|
40
|
+
if not self.client:
|
|
41
|
+
self.client = httpx.AsyncClient(base_url=self._base_url, timeout=300.0)
|
|
42
|
+
|
|
43
|
+
async def list_models(self) -> list[ModelDescriptor]:
|
|
44
|
+
"""Fetch models from active Ollama endpoint."""
|
|
45
|
+
await self.initialize()
|
|
46
|
+
assert self.client is not None
|
|
47
|
+
try:
|
|
48
|
+
response = await self.client.get("/api/tags")
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
data = response.json()
|
|
51
|
+
|
|
52
|
+
descriptors: list[ModelDescriptor] = []
|
|
53
|
+
for item in data.get("models", []):
|
|
54
|
+
descriptors.append(
|
|
55
|
+
ModelDescriptor(
|
|
56
|
+
model_id=item["name"],
|
|
57
|
+
display_name=item["name"],
|
|
58
|
+
provider_id="ollama",
|
|
59
|
+
context_length=8192,
|
|
60
|
+
capabilities={
|
|
61
|
+
"coding": CapabilityLevel.INTERMEDIATE,
|
|
62
|
+
"reasoning": CapabilityLevel.INTERMEDIATE,
|
|
63
|
+
"planning": CapabilityLevel.BASIC,
|
|
64
|
+
"summarization": CapabilityLevel.INTERMEDIATE,
|
|
65
|
+
"embedding": CapabilityLevel.INTERMEDIATE,
|
|
66
|
+
"instruction_following": CapabilityLevel.INTERMEDIATE,
|
|
67
|
+
"multimodal": CapabilityLevel.NONE,
|
|
68
|
+
"tool_use": CapabilityLevel.NONE,
|
|
69
|
+
"long_context": CapabilityLevel.NONE,
|
|
70
|
+
},
|
|
71
|
+
is_local=True,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return descriptors
|
|
75
|
+
except httpx.HTTPError as e:
|
|
76
|
+
raise ProviderConnectionError(f"Failed to fetch models from Ollama: {e}")
|
|
77
|
+
|
|
78
|
+
async def infer(self, request: InferenceRequest) -> InferenceResponse:
|
|
79
|
+
"""Synchronous chat inference."""
|
|
80
|
+
await self.initialize()
|
|
81
|
+
assert self.client is not None
|
|
82
|
+
start = time.perf_counter()
|
|
83
|
+
try:
|
|
84
|
+
payload = {
|
|
85
|
+
"model": request.model_id,
|
|
86
|
+
"messages": request.messages,
|
|
87
|
+
"stream": False,
|
|
88
|
+
"options": {
|
|
89
|
+
"temperature": request.temperature,
|
|
90
|
+
"num_predict": request.max_tokens,
|
|
91
|
+
"top_p": request.top_p,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
if request.stop_sequences:
|
|
95
|
+
payload["options"]["stop"] = request.stop_sequences
|
|
96
|
+
|
|
97
|
+
response = await self.client.post("/api/chat", json=payload)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
data = response.json()
|
|
100
|
+
latency = (time.perf_counter() - start) * 1000.0
|
|
101
|
+
|
|
102
|
+
if latency > 30000.0:
|
|
103
|
+
logger.warning(
|
|
104
|
+
"Slow inference on %s (%.1fs). Consider a smaller model for your hardware.",
|
|
105
|
+
request.model_id, latency / 1000.0
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return InferenceResponse(
|
|
109
|
+
content=data["message"]["content"],
|
|
110
|
+
model_id=request.model_id,
|
|
111
|
+
finish_reason=data.get("done_reason", "stop"),
|
|
112
|
+
tokens_used=data.get("eval_count", 0) + data.get("prompt_eval_count", 0),
|
|
113
|
+
latency_ms=latency,
|
|
114
|
+
)
|
|
115
|
+
except httpx.HTTPError as e:
|
|
116
|
+
raise InferenceError(f"Ollama inference failed: {e}")
|
|
117
|
+
|
|
118
|
+
async def stream(self, request: InferenceRequest) -> AsyncIterator[StreamChunk]:
|
|
119
|
+
"""Streaming chat completion."""
|
|
120
|
+
await self.initialize()
|
|
121
|
+
assert self.client is not None
|
|
122
|
+
try:
|
|
123
|
+
payload = {
|
|
124
|
+
"model": request.model_id,
|
|
125
|
+
"messages": request.messages,
|
|
126
|
+
"stream": True,
|
|
127
|
+
"options": {
|
|
128
|
+
"temperature": request.temperature,
|
|
129
|
+
"num_predict": request.max_tokens,
|
|
130
|
+
"top_p": request.top_p,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
if request.stop_sequences:
|
|
134
|
+
payload["options"]["stop"] = request.stop_sequences
|
|
135
|
+
|
|
136
|
+
async with self.client.stream("POST", "/api/chat", json=payload) as response:
|
|
137
|
+
response.raise_for_status()
|
|
138
|
+
async for line in response.aiter_lines():
|
|
139
|
+
if not line:
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
data = json.loads(line)
|
|
143
|
+
if "message" in data:
|
|
144
|
+
yield StreamChunk(
|
|
145
|
+
content=data["message"].get("content", ""),
|
|
146
|
+
finish_reason=data.get("done_reason"),
|
|
147
|
+
)
|
|
148
|
+
except json.JSONDecodeError:
|
|
149
|
+
continue
|
|
150
|
+
except httpx.HTTPError as e:
|
|
151
|
+
raise InferenceError(f"Ollama streaming failed: {e}")
|
|
152
|
+
|
|
153
|
+
async def embed(self, texts: list[str], model_id: str) -> list[list[float]]:
|
|
154
|
+
"""Batch embedding generation."""
|
|
155
|
+
await self.initialize()
|
|
156
|
+
assert self.client is not None
|
|
157
|
+
embeddings: list[list[float]] = []
|
|
158
|
+
try:
|
|
159
|
+
for text in texts:
|
|
160
|
+
resp = await self.client.post("/api/embeddings", json={"model": model_id, "prompt": text})
|
|
161
|
+
resp.raise_for_status()
|
|
162
|
+
embeddings.append(resp.json()["embedding"])
|
|
163
|
+
return embeddings
|
|
164
|
+
except httpx.HTTPError as e:
|
|
165
|
+
raise InferenceError(f"Ollama embedding failed: {e}")
|
|
166
|
+
|
|
167
|
+
async def health_check(self) -> ProviderHealth:
|
|
168
|
+
"""Pings Ollama core endpoint."""
|
|
169
|
+
await self.initialize()
|
|
170
|
+
assert self.client is not None
|
|
171
|
+
try:
|
|
172
|
+
resp = await self.client.get("/")
|
|
173
|
+
if resp.status_code == 200:
|
|
174
|
+
return ProviderHealth.HEALTHY
|
|
175
|
+
return ProviderHealth.DEGRADED
|
|
176
|
+
except Exception:
|
|
177
|
+
return ProviderHealth.UNHEALTHY
|
|
178
|
+
|
|
179
|
+
def get_capabilities(self) -> ProviderCapabilities:
|
|
180
|
+
return self._capabilities
|
|
181
|
+
|
|
182
|
+
async def shutdown(self) -> None:
|
|
183
|
+
"""Close connection pools."""
|
|
184
|
+
if self.client:
|
|
185
|
+
await self.client.aclose()
|
|
186
|
+
self.client = None
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""OpenAI provider adapter implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import SecretStr
|
|
11
|
+
|
|
12
|
+
from velune.core.errors.provider import (
|
|
13
|
+
InferenceError,
|
|
14
|
+
ProviderAuthenticationError,
|
|
15
|
+
)
|
|
16
|
+
from velune.core.types.inference import InferenceRequest, InferenceResponse, StreamChunk
|
|
17
|
+
from velune.core.types.model import CapabilityLevel, ModelDescriptor
|
|
18
|
+
from velune.core.types.provider import ProviderCapabilities, ProviderHealth
|
|
19
|
+
from velune.providers.base import ModelProvider
|
|
20
|
+
from velune.providers.keystore import get_key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OpenAIProvider(ModelProvider):
|
|
24
|
+
"""OpenAI provider for GPT chat and embedding models."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, api_key: str | SecretStr | None = None, base_url: str = "https://api.openai.com/v1") -> None:
|
|
27
|
+
self._api_key = api_key or get_key("openai")
|
|
28
|
+
if hasattr(self._api_key, 'get_secret_value'):
|
|
29
|
+
self._api_key = self._api_key.get_secret_value()
|
|
30
|
+
self._base_url = base_url
|
|
31
|
+
self.client: httpx.AsyncClient | None = None
|
|
32
|
+
self._capabilities = ProviderCapabilities(
|
|
33
|
+
supports_streaming=True,
|
|
34
|
+
supports_function_calling=True,
|
|
35
|
+
supports_embeddings=True,
|
|
36
|
+
max_context_window=128000,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def provider_id(self) -> str:
|
|
41
|
+
return "openai"
|
|
42
|
+
|
|
43
|
+
async def initialize(self) -> None:
|
|
44
|
+
"""Initialize headers and async client connection."""
|
|
45
|
+
if not self._api_key:
|
|
46
|
+
raise ProviderAuthenticationError("OpenAI API key not found in configuration or environment")
|
|
47
|
+
if not self.client:
|
|
48
|
+
headers = {"Authorization": f"Bearer {self._api_key}"}
|
|
49
|
+
self.client = httpx.AsyncClient(base_url=self._base_url, headers=headers, timeout=300.0)
|
|
50
|
+
|
|
51
|
+
async def list_models(self) -> list[ModelDescriptor]:
|
|
52
|
+
"""Return the current OpenAI model lineup."""
|
|
53
|
+
await self.initialize()
|
|
54
|
+
return [
|
|
55
|
+
ModelDescriptor(
|
|
56
|
+
model_id="gpt-4o",
|
|
57
|
+
display_name="GPT-4o",
|
|
58
|
+
provider_id="openai",
|
|
59
|
+
context_length=128000,
|
|
60
|
+
capabilities={
|
|
61
|
+
"coding": CapabilityLevel.EXPERT,
|
|
62
|
+
"reasoning": CapabilityLevel.EXPERT,
|
|
63
|
+
"planning": CapabilityLevel.EXPERT,
|
|
64
|
+
"summarization": CapabilityLevel.EXPERT,
|
|
65
|
+
"instruction_following": CapabilityLevel.EXPERT,
|
|
66
|
+
"tool_use": CapabilityLevel.EXPERT,
|
|
67
|
+
"long_context": CapabilityLevel.EXPERT,
|
|
68
|
+
},
|
|
69
|
+
is_local=False,
|
|
70
|
+
),
|
|
71
|
+
ModelDescriptor(
|
|
72
|
+
model_id="gpt-4o-mini",
|
|
73
|
+
display_name="GPT-4o Mini",
|
|
74
|
+
provider_id="openai",
|
|
75
|
+
context_length=128000,
|
|
76
|
+
capabilities={
|
|
77
|
+
"coding": CapabilityLevel.ADVANCED,
|
|
78
|
+
"reasoning": CapabilityLevel.ADVANCED,
|
|
79
|
+
"planning": CapabilityLevel.ADVANCED,
|
|
80
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
81
|
+
"instruction_following": CapabilityLevel.EXPERT,
|
|
82
|
+
"tool_use": CapabilityLevel.EXPERT,
|
|
83
|
+
"long_context": CapabilityLevel.ADVANCED,
|
|
84
|
+
},
|
|
85
|
+
is_local=False,
|
|
86
|
+
),
|
|
87
|
+
ModelDescriptor(
|
|
88
|
+
model_id="gpt-3.5-turbo",
|
|
89
|
+
display_name="GPT-3.5 Turbo",
|
|
90
|
+
provider_id="openai",
|
|
91
|
+
context_length=16385,
|
|
92
|
+
capabilities={
|
|
93
|
+
"coding": CapabilityLevel.INTERMEDIATE,
|
|
94
|
+
"reasoning": CapabilityLevel.INTERMEDIATE,
|
|
95
|
+
"planning": CapabilityLevel.INTERMEDIATE,
|
|
96
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
97
|
+
"instruction_following": CapabilityLevel.ADVANCED,
|
|
98
|
+
"tool_use": CapabilityLevel.INTERMEDIATE,
|
|
99
|
+
"long_context": CapabilityLevel.BASIC,
|
|
100
|
+
},
|
|
101
|
+
is_local=False,
|
|
102
|
+
tags=["fallback"],
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
async def infer(self, request: InferenceRequest) -> InferenceResponse:
|
|
107
|
+
"""Standard chat inference."""
|
|
108
|
+
await self.initialize()
|
|
109
|
+
assert self.client is not None
|
|
110
|
+
start = time.perf_counter()
|
|
111
|
+
try:
|
|
112
|
+
payload = {
|
|
113
|
+
"model": request.model_id,
|
|
114
|
+
"messages": request.messages,
|
|
115
|
+
"temperature": request.temperature,
|
|
116
|
+
"max_tokens": request.max_tokens,
|
|
117
|
+
"top_p": request.top_p,
|
|
118
|
+
}
|
|
119
|
+
if request.stop_sequences:
|
|
120
|
+
payload["stop"] = request.stop_sequences
|
|
121
|
+
|
|
122
|
+
response = await self.client.post("/chat/completions", json=payload)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
data = response.json()
|
|
125
|
+
latency = (time.perf_counter() - start) * 1000.0
|
|
126
|
+
|
|
127
|
+
usage = data.get("usage", {})
|
|
128
|
+
return InferenceResponse(
|
|
129
|
+
content=data["choices"][0]["message"]["content"],
|
|
130
|
+
model_id=request.model_id,
|
|
131
|
+
finish_reason=data["choices"][0]["finish_reason"] or "stop",
|
|
132
|
+
tokens_used=usage.get("total_tokens", 0),
|
|
133
|
+
prompt_tokens=usage.get("prompt_tokens", 0),
|
|
134
|
+
completion_tokens=usage.get("completion_tokens", 0),
|
|
135
|
+
latency_ms=latency,
|
|
136
|
+
)
|
|
137
|
+
except httpx.HTTPError as e:
|
|
138
|
+
raise InferenceError(f"OpenAI completion failed: {e}")
|
|
139
|
+
|
|
140
|
+
async def stream(self, request: InferenceRequest) -> AsyncIterator[StreamChunk]:
|
|
141
|
+
"""Streaming chat completions."""
|
|
142
|
+
await self.initialize()
|
|
143
|
+
assert self.client is not None
|
|
144
|
+
try:
|
|
145
|
+
payload = {
|
|
146
|
+
"model": request.model_id,
|
|
147
|
+
"messages": request.messages,
|
|
148
|
+
"temperature": request.temperature,
|
|
149
|
+
"max_tokens": request.max_tokens,
|
|
150
|
+
"top_p": request.top_p,
|
|
151
|
+
"stream": True,
|
|
152
|
+
}
|
|
153
|
+
if request.stop_sequences:
|
|
154
|
+
payload["stop"] = request.stop_sequences
|
|
155
|
+
|
|
156
|
+
async with self.client.stream("POST", "/chat/completions", json=payload) as response:
|
|
157
|
+
response.raise_for_status()
|
|
158
|
+
async for line in response.aiter_lines():
|
|
159
|
+
if line.startswith("data: "):
|
|
160
|
+
data_str = line[6:]
|
|
161
|
+
if data_str == "[DONE]":
|
|
162
|
+
break
|
|
163
|
+
try:
|
|
164
|
+
data = json.loads(data_str)
|
|
165
|
+
delta = data["choices"][0]["delta"]
|
|
166
|
+
yield StreamChunk(
|
|
167
|
+
content=delta.get("content", ""),
|
|
168
|
+
finish_reason=data["choices"][0].get("finish_reason"),
|
|
169
|
+
)
|
|
170
|
+
except (json.JSONDecodeError, KeyError):
|
|
171
|
+
continue
|
|
172
|
+
except httpx.HTTPError as e:
|
|
173
|
+
raise InferenceError(f"OpenAI stream failed: {e}")
|
|
174
|
+
|
|
175
|
+
async def embed(self, texts: list[str], model_id: str) -> list[list[float]]:
|
|
176
|
+
"""Generate batch embeddings."""
|
|
177
|
+
await self.initialize()
|
|
178
|
+
assert self.client is not None
|
|
179
|
+
try:
|
|
180
|
+
response = await self.client.post("/embeddings", json={"model": model_id, "input": texts})
|
|
181
|
+
response.raise_for_status()
|
|
182
|
+
data = response.json()
|
|
183
|
+
# Sort by index to maintain token alignments
|
|
184
|
+
sorted_data = sorted(data["data"], key=lambda x: x["index"])
|
|
185
|
+
return [item["embedding"] for item in sorted_data]
|
|
186
|
+
except httpx.HTTPError as e:
|
|
187
|
+
raise InferenceError(f"OpenAI embedding failed: {e}")
|
|
188
|
+
|
|
189
|
+
async def health_check(self) -> ProviderHealth:
|
|
190
|
+
"""Verifies API credentials and connectivity."""
|
|
191
|
+
try:
|
|
192
|
+
await self.initialize()
|
|
193
|
+
assert self.client is not None
|
|
194
|
+
resp = await self.client.get("/models")
|
|
195
|
+
if resp.status_code == 200:
|
|
196
|
+
return ProviderHealth.HEALTHY
|
|
197
|
+
return ProviderHealth.DEGRADED
|
|
198
|
+
except Exception:
|
|
199
|
+
return ProviderHealth.UNHEALTHY
|
|
200
|
+
|
|
201
|
+
def get_capabilities(self) -> ProviderCapabilities:
|
|
202
|
+
return self._capabilities
|
|
203
|
+
|
|
204
|
+
async def shutdown(self) -> None:
|
|
205
|
+
if self.client:
|
|
206
|
+
await self.client.aclose()
|
|
207
|
+
self.client = None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""OpenRouter provider adapter — OpenAI-compatible with dynamic model listing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from velune.core.errors.provider import ProviderAuthenticationError
|
|
8
|
+
from velune.core.types.model import CapabilityLevel, ModelCapabilityProfile, ModelDescriptor
|
|
9
|
+
from velune.providers.adapters.openai import OpenAIProvider
|
|
10
|
+
from velune.providers.keystore import get_key
|
|
11
|
+
|
|
12
|
+
_REFERER_HEADERS = {
|
|
13
|
+
"HTTP-Referer": "Velune CLI",
|
|
14
|
+
"X-Title": "Velune CLI",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OpenRouterProvider(OpenAIProvider):
|
|
19
|
+
"""OpenRouter provider — routes to many upstream models via the OpenAI API shape."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
base_url: str = "https://openrouter.ai/api/v1",
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(api_key=api_key or get_key("openrouter"), base_url=base_url)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def provider_id(self) -> str:
|
|
30
|
+
return "openrouter"
|
|
31
|
+
|
|
32
|
+
async def initialize(self) -> None:
|
|
33
|
+
"""Override to inject the OpenRouter-required headers."""
|
|
34
|
+
if not self._api_key:
|
|
35
|
+
raise ProviderAuthenticationError(
|
|
36
|
+
"OpenRouter API key not found — set OPENROUTER_API_KEY or run: velune config set-key openrouter"
|
|
37
|
+
)
|
|
38
|
+
if not self.client:
|
|
39
|
+
headers = {
|
|
40
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
41
|
+
**_REFERER_HEADERS,
|
|
42
|
+
}
|
|
43
|
+
self.client = httpx.AsyncClient(
|
|
44
|
+
base_url=self._base_url,
|
|
45
|
+
headers=headers,
|
|
46
|
+
timeout=300.0,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def list_models(self) -> list[ModelDescriptor]:
|
|
50
|
+
"""Fetch the current model catalogue from the OpenRouter API."""
|
|
51
|
+
await self.initialize()
|
|
52
|
+
assert self.client is not None
|
|
53
|
+
try:
|
|
54
|
+
resp = await self.client.get("/models")
|
|
55
|
+
resp.raise_for_status()
|
|
56
|
+
data = resp.json()
|
|
57
|
+
return [self._parse_model(m) for m in data.get("data", [])]
|
|
58
|
+
except Exception:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
def _parse_model(self, raw: dict) -> ModelDescriptor:
|
|
62
|
+
model_id = raw.get("id", "unknown")
|
|
63
|
+
context = raw.get("context_length") or 4096
|
|
64
|
+
pricing = raw.get("pricing", {})
|
|
65
|
+
cost_prompt = float(pricing.get("prompt", 0) or 0) * 1000
|
|
66
|
+
profile = ModelCapabilityProfile(
|
|
67
|
+
coding=CapabilityLevel.INTERMEDIATE,
|
|
68
|
+
reasoning=CapabilityLevel.INTERMEDIATE,
|
|
69
|
+
instruction_following=CapabilityLevel.ADVANCED,
|
|
70
|
+
)
|
|
71
|
+
return ModelDescriptor(
|
|
72
|
+
model_id=model_id,
|
|
73
|
+
display_name=raw.get("name") or model_id,
|
|
74
|
+
provider_id="openrouter",
|
|
75
|
+
context_length=context,
|
|
76
|
+
capabilities=profile,
|
|
77
|
+
is_local=False,
|
|
78
|
+
cost_per_1k_tokens=cost_prompt if cost_prompt > 0 else None,
|
|
79
|
+
tags=["cloud", "openrouter"],
|
|
80
|
+
metadata={"raw": raw},
|
|
81
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Together.AI provider adapter — OpenAI-compatible REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from velune.core.types.model import CapabilityLevel, ModelDescriptor
|
|
6
|
+
from velune.core.types.provider import ProviderCapabilities
|
|
7
|
+
from velune.providers.adapters.openai import OpenAIProvider
|
|
8
|
+
from velune.providers.keystore import get_key
|
|
9
|
+
|
|
10
|
+
TOGETHER_MODELS: list[ModelDescriptor] = [
|
|
11
|
+
ModelDescriptor(
|
|
12
|
+
model_id="meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
13
|
+
display_name="Llama 3.3 70B Instruct Turbo",
|
|
14
|
+
provider_id="together",
|
|
15
|
+
context_length=131072,
|
|
16
|
+
capabilities={
|
|
17
|
+
"coding": CapabilityLevel.ADVANCED,
|
|
18
|
+
"reasoning": CapabilityLevel.ADVANCED,
|
|
19
|
+
"planning": CapabilityLevel.ADVANCED,
|
|
20
|
+
"summarization": CapabilityLevel.EXPERT,
|
|
21
|
+
"instruction_following": CapabilityLevel.EXPERT,
|
|
22
|
+
"tool_use": CapabilityLevel.ADVANCED,
|
|
23
|
+
"long_context": CapabilityLevel.ADVANCED,
|
|
24
|
+
},
|
|
25
|
+
speed_tier="fast",
|
|
26
|
+
is_local=False,
|
|
27
|
+
cost_per_1k_tokens=0.00088,
|
|
28
|
+
tags=["cloud", "together", "llama", "turbo"],
|
|
29
|
+
),
|
|
30
|
+
ModelDescriptor(
|
|
31
|
+
model_id="meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo",
|
|
32
|
+
display_name="Llama 3.2 11B Vision Instruct",
|
|
33
|
+
provider_id="together",
|
|
34
|
+
context_length=131072,
|
|
35
|
+
capabilities={
|
|
36
|
+
"coding": CapabilityLevel.INTERMEDIATE,
|
|
37
|
+
"reasoning": CapabilityLevel.INTERMEDIATE,
|
|
38
|
+
"planning": CapabilityLevel.INTERMEDIATE,
|
|
39
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
40
|
+
"instruction_following": CapabilityLevel.ADVANCED,
|
|
41
|
+
"tool_use": CapabilityLevel.INTERMEDIATE,
|
|
42
|
+
"long_context": CapabilityLevel.INTERMEDIATE,
|
|
43
|
+
},
|
|
44
|
+
speed_tier="fast",
|
|
45
|
+
is_local=False,
|
|
46
|
+
cost_per_1k_tokens=0.00018,
|
|
47
|
+
tags=["cloud", "together", "llama", "vision", "cheap"],
|
|
48
|
+
),
|
|
49
|
+
ModelDescriptor(
|
|
50
|
+
model_id="Qwen/Qwen2.5-Coder-32B-Instruct",
|
|
51
|
+
display_name="Qwen 2.5 Coder 32B Instruct",
|
|
52
|
+
provider_id="together",
|
|
53
|
+
context_length=131072,
|
|
54
|
+
capabilities={
|
|
55
|
+
"coding": CapabilityLevel.EXPERT,
|
|
56
|
+
"reasoning": CapabilityLevel.ADVANCED,
|
|
57
|
+
"planning": CapabilityLevel.ADVANCED,
|
|
58
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
59
|
+
"instruction_following": CapabilityLevel.EXPERT,
|
|
60
|
+
"tool_use": CapabilityLevel.ADVANCED,
|
|
61
|
+
"long_context": CapabilityLevel.ADVANCED,
|
|
62
|
+
},
|
|
63
|
+
speed_tier="medium",
|
|
64
|
+
is_local=False,
|
|
65
|
+
cost_per_1k_tokens=0.0008,
|
|
66
|
+
tags=["cloud", "together", "qwen", "coding"],
|
|
67
|
+
),
|
|
68
|
+
ModelDescriptor(
|
|
69
|
+
model_id="deepseek-ai/DeepSeek-R1",
|
|
70
|
+
display_name="DeepSeek R1",
|
|
71
|
+
provider_id="together",
|
|
72
|
+
context_length=163840,
|
|
73
|
+
capabilities={
|
|
74
|
+
"coding": CapabilityLevel.EXPERT,
|
|
75
|
+
"reasoning": CapabilityLevel.EXPERT,
|
|
76
|
+
"planning": CapabilityLevel.EXPERT,
|
|
77
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
78
|
+
"instruction_following": CapabilityLevel.EXPERT,
|
|
79
|
+
"tool_use": CapabilityLevel.ADVANCED,
|
|
80
|
+
"long_context": CapabilityLevel.ADVANCED,
|
|
81
|
+
},
|
|
82
|
+
speed_tier="slow",
|
|
83
|
+
is_local=False,
|
|
84
|
+
cost_per_1k_tokens=0.003,
|
|
85
|
+
tags=["cloud", "together", "deepseek", "reasoning"],
|
|
86
|
+
),
|
|
87
|
+
ModelDescriptor(
|
|
88
|
+
model_id="mistralai/Mistral-7B-Instruct-v0.3",
|
|
89
|
+
display_name="Mistral 7B Instruct v0.3",
|
|
90
|
+
provider_id="together",
|
|
91
|
+
context_length=32768,
|
|
92
|
+
capabilities={
|
|
93
|
+
"coding": CapabilityLevel.INTERMEDIATE,
|
|
94
|
+
"reasoning": CapabilityLevel.INTERMEDIATE,
|
|
95
|
+
"planning": CapabilityLevel.INTERMEDIATE,
|
|
96
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
97
|
+
"instruction_following": CapabilityLevel.ADVANCED,
|
|
98
|
+
"tool_use": CapabilityLevel.INTERMEDIATE,
|
|
99
|
+
"long_context": CapabilityLevel.BASIC,
|
|
100
|
+
},
|
|
101
|
+
speed_tier="fast",
|
|
102
|
+
is_local=False,
|
|
103
|
+
cost_per_1k_tokens=0.0002,
|
|
104
|
+
tags=["cloud", "together", "mistral", "cheap"],
|
|
105
|
+
),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TogetherProvider(OpenAIProvider):
|
|
110
|
+
"""Together.AI — 50+ open models via OpenAI-compatible inference."""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
api_key: str | None = None,
|
|
115
|
+
base_url: str = "https://api.together.xyz/v1",
|
|
116
|
+
) -> None:
|
|
117
|
+
self._api_key = api_key or get_key("together")
|
|
118
|
+
if hasattr(self._api_key, "get_secret_value"):
|
|
119
|
+
self._api_key = self._api_key.get_secret_value()
|
|
120
|
+
self._base_url = base_url
|
|
121
|
+
self.client = None
|
|
122
|
+
self._capabilities = ProviderCapabilities(
|
|
123
|
+
supports_streaming=True,
|
|
124
|
+
supports_function_calling=True,
|
|
125
|
+
supports_embeddings=False,
|
|
126
|
+
max_context_window=131072,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def provider_id(self) -> str:
|
|
131
|
+
return "together"
|
|
132
|
+
|
|
133
|
+
async def list_models(self) -> list[ModelDescriptor]:
|
|
134
|
+
return list(TOGETHER_MODELS)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""xAI (Grok) provider adapter — OpenAI-compatible endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from velune.core.types.model import CapabilityLevel, ModelDescriptor
|
|
6
|
+
from velune.providers.adapters.openai import OpenAIProvider
|
|
7
|
+
from velune.providers.keystore import get_key
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class XAIProvider(OpenAIProvider):
|
|
11
|
+
"""xAI Grok provider. Wire-compatible with the OpenAI chat API."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: str | None = None,
|
|
16
|
+
base_url: str = "https://api.x.ai/v1",
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(api_key=api_key or get_key("xai"), base_url=base_url)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def provider_id(self) -> str:
|
|
22
|
+
return "xai"
|
|
23
|
+
|
|
24
|
+
async def list_models(self) -> list[ModelDescriptor]:
|
|
25
|
+
return [
|
|
26
|
+
ModelDescriptor(
|
|
27
|
+
model_id="grok-2",
|
|
28
|
+
display_name="Grok 2",
|
|
29
|
+
provider_id="xai",
|
|
30
|
+
context_length=131072,
|
|
31
|
+
capabilities={
|
|
32
|
+
"coding": CapabilityLevel.ADVANCED,
|
|
33
|
+
"reasoning": CapabilityLevel.ADVANCED,
|
|
34
|
+
"planning": CapabilityLevel.ADVANCED,
|
|
35
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
36
|
+
"instruction_following": CapabilityLevel.EXPERT,
|
|
37
|
+
"tool_use": CapabilityLevel.ADVANCED,
|
|
38
|
+
"long_context": CapabilityLevel.ADVANCED,
|
|
39
|
+
},
|
|
40
|
+
is_local=False,
|
|
41
|
+
tags=["cloud", "xai"],
|
|
42
|
+
),
|
|
43
|
+
ModelDescriptor(
|
|
44
|
+
model_id="grok-2-mini",
|
|
45
|
+
display_name="Grok 2 Mini",
|
|
46
|
+
provider_id="xai",
|
|
47
|
+
context_length=131072,
|
|
48
|
+
capabilities={
|
|
49
|
+
"coding": CapabilityLevel.INTERMEDIATE,
|
|
50
|
+
"reasoning": CapabilityLevel.INTERMEDIATE,
|
|
51
|
+
"planning": CapabilityLevel.INTERMEDIATE,
|
|
52
|
+
"summarization": CapabilityLevel.ADVANCED,
|
|
53
|
+
"instruction_following": CapabilityLevel.ADVANCED,
|
|
54
|
+
"tool_use": CapabilityLevel.INTERMEDIATE,
|
|
55
|
+
"long_context": CapabilityLevel.ADVANCED,
|
|
56
|
+
},
|
|
57
|
+
is_local=False,
|
|
58
|
+
tags=["cloud", "xai", "mini"],
|
|
59
|
+
),
|
|
60
|
+
]
|