silvol 0.2.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.
- silvol/__init__.py +40 -0
- silvol/_version.py +1 -0
- silvol/client.py +87 -0
- silvol/finetune.py +285 -0
- silvol/integrations/__init__.py +4 -0
- silvol/integrations/crewai.py +47 -0
- silvol/integrations/langchain.py +47 -0
- silvol-0.2.0.dist-info/METADATA +173 -0
- silvol-0.2.0.dist-info/RECORD +11 -0
- silvol-0.2.0.dist-info/WHEEL +4 -0
- silvol-0.2.0.dist-info/licenses/LICENSE +21 -0
silvol/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Silvol — Python SDK for the Silvol inference + fine-tuning API.
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for the OpenAI SDK pointing at Silvol's
|
|
5
|
+
decentralised GPU gateway, plus a fine-tuning client at ``client.finetune``.
|
|
6
|
+
|
|
7
|
+
Quickstart::
|
|
8
|
+
|
|
9
|
+
from silvol import Silvol
|
|
10
|
+
|
|
11
|
+
client = Silvol(api_key="sk-svl-...")
|
|
12
|
+
|
|
13
|
+
# Inference
|
|
14
|
+
resp = client.chat.completions.create(
|
|
15
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
16
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
17
|
+
)
|
|
18
|
+
print(resp.choices[0].message.content)
|
|
19
|
+
|
|
20
|
+
# Fine-tuning ($15 flat per run)
|
|
21
|
+
job = client.finetune.submit_job(
|
|
22
|
+
job_name="legal-assistant-v1",
|
|
23
|
+
base_model="llama-3.1-8b",
|
|
24
|
+
dataset_path="training.jsonl",
|
|
25
|
+
)
|
|
26
|
+
client.finetune.upload_dataset(job["dataset_upload_url"], "training.jsonl")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from ._version import __version__
|
|
30
|
+
from .client import AsyncSilvol, Silvol
|
|
31
|
+
from .finetune import AsyncFinetune, Finetune, FinetuneError
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Silvol",
|
|
35
|
+
"AsyncSilvol",
|
|
36
|
+
"Finetune",
|
|
37
|
+
"AsyncFinetune",
|
|
38
|
+
"FinetuneError",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
silvol/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
silvol/client.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Silvol Python SDK — sync and async clients.
|
|
3
|
+
|
|
4
|
+
Drop-in replacements for openai.OpenAI / openai.AsyncOpenAI that point at
|
|
5
|
+
the Silvol inference gateway by default.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from openai import OpenAI, AsyncOpenAI
|
|
9
|
+
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
from .finetune import Finetune, AsyncFinetune
|
|
12
|
+
|
|
13
|
+
__all__ = ["Silvol", "AsyncSilvol"]
|
|
14
|
+
|
|
15
|
+
_DEFAULT_BASE_URL = "https://api.silvol.ai/v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Silvol(OpenAI):
|
|
19
|
+
"""
|
|
20
|
+
Synchronous Silvol client.
|
|
21
|
+
|
|
22
|
+
Usage::
|
|
23
|
+
|
|
24
|
+
from silvol import Silvol
|
|
25
|
+
|
|
26
|
+
client = Silvol(api_key="sk-svl-...")
|
|
27
|
+
resp = client.chat.completions.create(
|
|
28
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
29
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
30
|
+
)
|
|
31
|
+
print(resp.choices[0].message.content)
|
|
32
|
+
|
|
33
|
+
Fine-tuning is available at ``client.finetune`` — see ``silvol.finetune``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
DEFAULT_BASE_URL: str = _DEFAULT_BASE_URL
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: str | None = None,
|
|
41
|
+
base_url: str | None = None,
|
|
42
|
+
**kwargs,
|
|
43
|
+
) -> None:
|
|
44
|
+
super().__init__(
|
|
45
|
+
api_key=api_key,
|
|
46
|
+
base_url=base_url or self.DEFAULT_BASE_URL,
|
|
47
|
+
**kwargs,
|
|
48
|
+
)
|
|
49
|
+
self.finetune: Finetune = Finetune(self)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AsyncSilvol(AsyncOpenAI):
|
|
53
|
+
"""
|
|
54
|
+
Asynchronous Silvol client.
|
|
55
|
+
|
|
56
|
+
Usage::
|
|
57
|
+
|
|
58
|
+
import asyncio
|
|
59
|
+
from silvol import AsyncSilvol
|
|
60
|
+
|
|
61
|
+
async def main():
|
|
62
|
+
client = AsyncSilvol(api_key="sk-svl-...")
|
|
63
|
+
resp = await client.chat.completions.create(
|
|
64
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
65
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
66
|
+
)
|
|
67
|
+
print(resp.choices[0].message.content)
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
|
|
71
|
+
Fine-tuning is available at ``client.finetune`` — see ``silvol.finetune``.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
DEFAULT_BASE_URL: str = _DEFAULT_BASE_URL
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
api_key: str | None = None,
|
|
79
|
+
base_url: str | None = None,
|
|
80
|
+
**kwargs,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(
|
|
83
|
+
api_key=api_key,
|
|
84
|
+
base_url=base_url or self.DEFAULT_BASE_URL,
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
87
|
+
self.finetune: AsyncFinetune = AsyncFinetune(self)
|
silvol/finetune.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Silvol fine-tuning API client.
|
|
3
|
+
|
|
4
|
+
Wraps the gateway endpoints under ``/v1/finetune/*`` with a small typed surface.
|
|
5
|
+
Attached to ``Silvol`` and ``AsyncSilvol`` clients as ``client.finetune``.
|
|
6
|
+
|
|
7
|
+
Quickstart::
|
|
8
|
+
|
|
9
|
+
from silvol import Silvol
|
|
10
|
+
|
|
11
|
+
client = Silvol(api_key="sk-svl-...")
|
|
12
|
+
|
|
13
|
+
# Submit a training job
|
|
14
|
+
with open("dataset.jsonl", "rb") as f:
|
|
15
|
+
job = client.finetune.submit_job(
|
|
16
|
+
job_name="legal-assistant-v1",
|
|
17
|
+
base_model="llama-3.1-8b",
|
|
18
|
+
dataset_file=f,
|
|
19
|
+
)
|
|
20
|
+
print(job["id"], job["price_cents"])
|
|
21
|
+
|
|
22
|
+
# Poll until ready
|
|
23
|
+
while True:
|
|
24
|
+
job = client.finetune.get_job(job["id"])
|
|
25
|
+
if job["status"] in ("ready", "failed", "cancelled"):
|
|
26
|
+
break
|
|
27
|
+
|
|
28
|
+
# Deploy and use
|
|
29
|
+
if job["status"] == "ready":
|
|
30
|
+
client.finetune.deploy_job(job["id"])
|
|
31
|
+
resp = client.chat.completions.create(
|
|
32
|
+
model=f"silvol/{job['job_name']}",
|
|
33
|
+
messages=[{"role": "user", "content": "What is force majeure?"}],
|
|
34
|
+
)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import base64
|
|
40
|
+
from typing import Any, BinaryIO, Iterable, Literal
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
BaseModel = Literal["llama-3.1-8b", "mistral-7b"]
|
|
44
|
+
JobStatus = Literal[
|
|
45
|
+
"pending", "training", "uploading", "ready",
|
|
46
|
+
"deploying", "deployed", "failed", "cancelled",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _encode_dataset(dataset: bytes | BinaryIO | str | Iterable[dict]) -> str:
|
|
51
|
+
"""Accept multiple input shapes and return base64-encoded JSONL."""
|
|
52
|
+
if isinstance(dataset, str):
|
|
53
|
+
# Already JSONL text
|
|
54
|
+
data = dataset.encode("utf-8")
|
|
55
|
+
elif isinstance(dataset, bytes):
|
|
56
|
+
data = dataset
|
|
57
|
+
elif hasattr(dataset, "read"):
|
|
58
|
+
chunk = dataset.read()
|
|
59
|
+
data = chunk.encode("utf-8") if isinstance(chunk, str) else chunk
|
|
60
|
+
else:
|
|
61
|
+
# Iterable of message dicts → JSONL
|
|
62
|
+
import json
|
|
63
|
+
data = "\n".join(json.dumps(ex) for ex in dataset).encode("utf-8")
|
|
64
|
+
return base64.b64encode(data).decode("ascii")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _build_path(*parts: str) -> str:
|
|
68
|
+
return "/" + "/".join(p.strip("/") for p in parts)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Sync ─────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Finetune:
|
|
75
|
+
"""Synchronous fine-tuning API. Accessed via ``client.finetune``."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, client: Any) -> None:
|
|
78
|
+
# `client` is a Silvol (subclass of OpenAI) — reuse its underlying
|
|
79
|
+
# httpx client so we share auth, base URL, timeouts, retries, etc.
|
|
80
|
+
self._client = client
|
|
81
|
+
|
|
82
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
83
|
+
# openai SDK exposes `_client._request` on the OpenAI instance; we use
|
|
84
|
+
# `_client._client.request` via the underlying httpx client for raw access.
|
|
85
|
+
url = path # base_url is already set on the openai client
|
|
86
|
+
# The openai sync client carries a base URL in self.base_url and an
|
|
87
|
+
# httpx client at self._client. We hit that directly for non-OpenAI routes.
|
|
88
|
+
http = self._client._client # httpx.Client
|
|
89
|
+
full_url = f"{str(self._client.base_url).rstrip('/')}{url}"
|
|
90
|
+
headers = {
|
|
91
|
+
"Authorization": f"Bearer {self._client.api_key}",
|
|
92
|
+
"Content-Type": "application/json",
|
|
93
|
+
}
|
|
94
|
+
if "headers" in kwargs:
|
|
95
|
+
headers.update(kwargs.pop("headers"))
|
|
96
|
+
resp = http.request(method, full_url, headers=headers, **kwargs)
|
|
97
|
+
if resp.status_code >= 400:
|
|
98
|
+
try:
|
|
99
|
+
detail = resp.json().get("error") or resp.text
|
|
100
|
+
except Exception:
|
|
101
|
+
detail = resp.text
|
|
102
|
+
raise FinetuneError(f"{method} {path} failed ({resp.status_code}): {detail}")
|
|
103
|
+
if resp.status_code == 204 or not resp.content:
|
|
104
|
+
return None
|
|
105
|
+
return resp.json()
|
|
106
|
+
|
|
107
|
+
# ── Jobs ────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
def submit_job(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
job_name: str,
|
|
113
|
+
base_model: BaseModel,
|
|
114
|
+
dataset_file: bytes | BinaryIO | str | Iterable[dict] | None = None,
|
|
115
|
+
dataset_path: str | None = None,
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
Submit a training job. Charges $15 via Stripe immediately.
|
|
119
|
+
|
|
120
|
+
Provide either ``dataset_file`` (bytes / file object / JSONL string /
|
|
121
|
+
iterable of message dicts) or ``dataset_path`` (path to a .jsonl file).
|
|
122
|
+
|
|
123
|
+
Returns the job record including a ``dataset_upload_url`` — the SDK
|
|
124
|
+
does NOT upload your dataset for you in this version. Use the returned
|
|
125
|
+
signed URL with your own HTTP client (or call ``upload_dataset()``).
|
|
126
|
+
"""
|
|
127
|
+
if dataset_path and not dataset_file:
|
|
128
|
+
with open(dataset_path, "rb") as f:
|
|
129
|
+
dataset_file = f.read()
|
|
130
|
+
if dataset_file is None:
|
|
131
|
+
raise ValueError("Provide dataset_file or dataset_path")
|
|
132
|
+
|
|
133
|
+
return self._request(
|
|
134
|
+
"POST",
|
|
135
|
+
"/v1/finetune/jobs",
|
|
136
|
+
json={
|
|
137
|
+
"jobName": job_name,
|
|
138
|
+
"baseModel": base_model,
|
|
139
|
+
"jsonlBase64": _encode_dataset(dataset_file),
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def upload_dataset(self, signed_upload_url: str, dataset_path: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Upload a JSONL file to the signed URL returned by ``submit_job``.
|
|
146
|
+
Training starts automatically on the next poll cycle (~5 min).
|
|
147
|
+
"""
|
|
148
|
+
with open(dataset_path, "rb") as f:
|
|
149
|
+
data = f.read()
|
|
150
|
+
# The signed URL hits Supabase Storage directly, not the gateway —
|
|
151
|
+
# use a fresh httpx call without our Bearer token.
|
|
152
|
+
import httpx
|
|
153
|
+
resp = httpx.put(
|
|
154
|
+
signed_upload_url,
|
|
155
|
+
content=data,
|
|
156
|
+
headers={"Content-Type": "application/jsonl"},
|
|
157
|
+
timeout=60.0,
|
|
158
|
+
)
|
|
159
|
+
if resp.status_code >= 400:
|
|
160
|
+
raise FinetuneError(f"Dataset upload failed ({resp.status_code}): {resp.text}")
|
|
161
|
+
|
|
162
|
+
def list_jobs(self) -> list[dict[str, Any]]:
|
|
163
|
+
return self._request("GET", "/v1/finetune/jobs")["jobs"]
|
|
164
|
+
|
|
165
|
+
def get_job(self, job_id: str) -> dict[str, Any]:
|
|
166
|
+
return self._request("GET", _build_path("v1", "finetune", "jobs", job_id))
|
|
167
|
+
|
|
168
|
+
def cancel_job(self, job_id: str) -> dict[str, Any]:
|
|
169
|
+
"""Cancel a pending job. Refund issued automatically."""
|
|
170
|
+
return self._request("DELETE", _build_path("v1", "finetune", "jobs", job_id))
|
|
171
|
+
|
|
172
|
+
def deploy_job(self, job_id: str) -> dict[str, Any]:
|
|
173
|
+
"""Deploy a ready model. Provisions a dedicated GPU node at $0.80/hr."""
|
|
174
|
+
return self._request(
|
|
175
|
+
"POST", _build_path("v1", "finetune", "jobs", job_id, "deploy")
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# ── Models ──────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
def list_models(self) -> list[dict[str, Any]]:
|
|
181
|
+
return self._request("GET", "/v1/finetune/models")["models"]
|
|
182
|
+
|
|
183
|
+
def delete_model(self, model_id: str) -> dict[str, Any]:
|
|
184
|
+
"""Stop a deployed model or delete a stored one."""
|
|
185
|
+
return self._request(
|
|
186
|
+
"DELETE", _build_path("v1", "finetune", "models", model_id)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── Async ────────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class AsyncFinetune:
|
|
194
|
+
"""Asynchronous fine-tuning API. Accessed via ``async_client.finetune``."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, client: Any) -> None:
|
|
197
|
+
self._client = client
|
|
198
|
+
|
|
199
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
200
|
+
http = self._client._client # httpx.AsyncClient
|
|
201
|
+
full_url = f"{str(self._client.base_url).rstrip('/')}{path}"
|
|
202
|
+
headers = {
|
|
203
|
+
"Authorization": f"Bearer {self._client.api_key}",
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
}
|
|
206
|
+
if "headers" in kwargs:
|
|
207
|
+
headers.update(kwargs.pop("headers"))
|
|
208
|
+
resp = await http.request(method, full_url, headers=headers, **kwargs)
|
|
209
|
+
if resp.status_code >= 400:
|
|
210
|
+
try:
|
|
211
|
+
detail = resp.json().get("error") or resp.text
|
|
212
|
+
except Exception:
|
|
213
|
+
detail = resp.text
|
|
214
|
+
raise FinetuneError(f"{method} {path} failed ({resp.status_code}): {detail}")
|
|
215
|
+
if resp.status_code == 204 or not resp.content:
|
|
216
|
+
return None
|
|
217
|
+
return resp.json()
|
|
218
|
+
|
|
219
|
+
async def submit_job(
|
|
220
|
+
self,
|
|
221
|
+
*,
|
|
222
|
+
job_name: str,
|
|
223
|
+
base_model: BaseModel,
|
|
224
|
+
dataset_file: bytes | BinaryIO | str | Iterable[dict] | None = None,
|
|
225
|
+
dataset_path: str | None = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
if dataset_path and not dataset_file:
|
|
228
|
+
with open(dataset_path, "rb") as f:
|
|
229
|
+
dataset_file = f.read()
|
|
230
|
+
if dataset_file is None:
|
|
231
|
+
raise ValueError("Provide dataset_file or dataset_path")
|
|
232
|
+
|
|
233
|
+
return await self._request(
|
|
234
|
+
"POST",
|
|
235
|
+
"/v1/finetune/jobs",
|
|
236
|
+
json={
|
|
237
|
+
"jobName": job_name,
|
|
238
|
+
"baseModel": base_model,
|
|
239
|
+
"jsonlBase64": _encode_dataset(dataset_file),
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
async def upload_dataset(self, signed_upload_url: str, dataset_path: str) -> None:
|
|
244
|
+
with open(dataset_path, "rb") as f:
|
|
245
|
+
data = f.read()
|
|
246
|
+
import httpx
|
|
247
|
+
async with httpx.AsyncClient(timeout=60.0) as http:
|
|
248
|
+
resp = await http.put(
|
|
249
|
+
signed_upload_url,
|
|
250
|
+
content=data,
|
|
251
|
+
headers={"Content-Type": "application/jsonl"},
|
|
252
|
+
)
|
|
253
|
+
if resp.status_code >= 400:
|
|
254
|
+
raise FinetuneError(f"Dataset upload failed ({resp.status_code}): {resp.text}")
|
|
255
|
+
|
|
256
|
+
async def list_jobs(self) -> list[dict[str, Any]]:
|
|
257
|
+
return (await self._request("GET", "/v1/finetune/jobs"))["jobs"]
|
|
258
|
+
|
|
259
|
+
async def get_job(self, job_id: str) -> dict[str, Any]:
|
|
260
|
+
return await self._request("GET", _build_path("v1", "finetune", "jobs", job_id))
|
|
261
|
+
|
|
262
|
+
async def cancel_job(self, job_id: str) -> dict[str, Any]:
|
|
263
|
+
return await self._request("DELETE", _build_path("v1", "finetune", "jobs", job_id))
|
|
264
|
+
|
|
265
|
+
async def deploy_job(self, job_id: str) -> dict[str, Any]:
|
|
266
|
+
return await self._request(
|
|
267
|
+
"POST", _build_path("v1", "finetune", "jobs", job_id, "deploy")
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async def list_models(self) -> list[dict[str, Any]]:
|
|
271
|
+
return (await self._request("GET", "/v1/finetune/models"))["models"]
|
|
272
|
+
|
|
273
|
+
async def delete_model(self, model_id: str) -> dict[str, Any]:
|
|
274
|
+
return await self._request(
|
|
275
|
+
"DELETE", _build_path("v1", "finetune", "models", model_id)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ── Errors ───────────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class FinetuneError(Exception):
|
|
283
|
+
"""Raised on non-2xx responses from the Silvol fine-tuning API."""
|
|
284
|
+
|
|
285
|
+
pass
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrewAI integration for Silvol.
|
|
3
|
+
|
|
4
|
+
Requires: pip install silvol[crewai]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ["SilvolLLM"]
|
|
10
|
+
|
|
11
|
+
_BASE_URL = "https://api.silvol.ai/v1"
|
|
12
|
+
_DEFAULT_MODEL = "DeepSeek-R1-Distill-Qwen-7B"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def SilvolLLM(
|
|
16
|
+
api_key: str | None = None,
|
|
17
|
+
model: str = _DEFAULT_MODEL,
|
|
18
|
+
**kwargs,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Return a CrewAI ``LLM`` instance wired to the Silvol gateway.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
from silvol.integrations.crewai import SilvolLLM
|
|
26
|
+
from crewai import Agent
|
|
27
|
+
|
|
28
|
+
llm = SilvolLLM(api_key="sk-svl-...")
|
|
29
|
+
agent = Agent(role="researcher", goal="...", llm=llm)
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
api_key:
|
|
34
|
+
Your Silvol API key (``sk-svl-...``).
|
|
35
|
+
model:
|
|
36
|
+
Model ID to use. Defaults to ``DeepSeek-R1-Distill-Qwen-7B``.
|
|
37
|
+
**kwargs:
|
|
38
|
+
Forwarded verbatim to ``LLM``.
|
|
39
|
+
"""
|
|
40
|
+
from crewai import LLM # noqa: PLC0415
|
|
41
|
+
|
|
42
|
+
return LLM(
|
|
43
|
+
model=f"openai/{model}",
|
|
44
|
+
api_key=api_key,
|
|
45
|
+
base_url=_BASE_URL,
|
|
46
|
+
**kwargs,
|
|
47
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain integration for Silvol.
|
|
3
|
+
|
|
4
|
+
Requires: pip install silvol[langchain]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ["SilvolChat"]
|
|
10
|
+
|
|
11
|
+
_BASE_URL = "https://api.silvol.ai/v1"
|
|
12
|
+
_DEFAULT_MODEL = "DeepSeek-R1-Distill-Qwen-7B"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def SilvolChat(
|
|
16
|
+
api_key: str | None = None,
|
|
17
|
+
model: str = _DEFAULT_MODEL,
|
|
18
|
+
**kwargs,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Return a LangChain ``ChatOpenAI`` instance wired to the Silvol gateway.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
from silvol.integrations.langchain import SilvolChat
|
|
26
|
+
|
|
27
|
+
llm = SilvolChat(api_key="sk-svl-...")
|
|
28
|
+
print(llm.invoke("Hello"))
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
api_key:
|
|
33
|
+
Your Silvol API key (``sk-svl-...``). Falls back to the
|
|
34
|
+
``OPENAI_API_KEY`` environment variable if omitted.
|
|
35
|
+
model:
|
|
36
|
+
Model ID to use. Defaults to ``DeepSeek-R1-Distill-Qwen-7B``.
|
|
37
|
+
**kwargs:
|
|
38
|
+
Forwarded verbatim to ``ChatOpenAI``.
|
|
39
|
+
"""
|
|
40
|
+
from langchain_openai import ChatOpenAI # noqa: PLC0415
|
|
41
|
+
|
|
42
|
+
return ChatOpenAI(
|
|
43
|
+
openai_api_key=api_key,
|
|
44
|
+
openai_api_base=_BASE_URL,
|
|
45
|
+
model=model,
|
|
46
|
+
**kwargs,
|
|
47
|
+
)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: silvol
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for the Silvol inference API — OpenAI-compatible, decentralised GPU.
|
|
5
|
+
Project-URL: Homepage, https://silvol.ai
|
|
6
|
+
Project-URL: Documentation, https://silvol.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/optimuscodexprimus/silvol-python
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/optimuscodexprimus/silvol-python/issues
|
|
9
|
+
Author-email: Silvol <hello@silvol.ai>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Silvol
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: gpu,inference,llm,nosana,openai,silvol
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Requires-Dist: httpx>=0.25
|
|
43
|
+
Requires-Dist: openai>=1.0
|
|
44
|
+
Provides-Extra: all
|
|
45
|
+
Requires-Dist: crewai>=0.30; extra == 'all'
|
|
46
|
+
Requires-Dist: langchain-openai>=0.1; extra == 'all'
|
|
47
|
+
Provides-Extra: crewai
|
|
48
|
+
Requires-Dist: crewai>=0.30; extra == 'crewai'
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: build; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
52
|
+
Requires-Dist: twine; extra == 'dev'
|
|
53
|
+
Provides-Extra: langchain
|
|
54
|
+
Requires-Dist: langchain-openai>=0.1; extra == 'langchain'
|
|
55
|
+
Description-Content-Type: text/markdown
|
|
56
|
+
|
|
57
|
+
# silvol-python
|
|
58
|
+
|
|
59
|
+
Python SDK for [Silvol](https://silvol.ai) — an OpenAI-compatible inference API running on
|
|
60
|
+
Nosana's decentralised GPU grid.
|
|
61
|
+
|
|
62
|
+
Drop-in replacement for the OpenAI SDK. Change the base URL and your key; keep the rest
|
|
63
|
+
of your code.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install silvol
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
With optional framework integrations:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install silvol[langchain] # LangChain
|
|
77
|
+
pip install silvol[crewai] # CrewAI
|
|
78
|
+
pip install silvol[all] # both
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from silvol import Silvol
|
|
87
|
+
|
|
88
|
+
client = Silvol(api_key="sk-svl-...") # or set SILVOL_API_KEY env var
|
|
89
|
+
|
|
90
|
+
resp = client.chat.completions.create(
|
|
91
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
92
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
93
|
+
)
|
|
94
|
+
print(resp.choices[0].message.content)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Async:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
from silvol import AsyncSilvol
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
client = AsyncSilvol(api_key="sk-svl-...")
|
|
105
|
+
resp = await client.chat.completions.create(
|
|
106
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
107
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
108
|
+
stream=True,
|
|
109
|
+
)
|
|
110
|
+
async for chunk in resp:
|
|
111
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
112
|
+
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## LangChain
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from silvol.integrations.langchain import SilvolChat
|
|
122
|
+
|
|
123
|
+
llm = SilvolChat(api_key="sk-svl-...")
|
|
124
|
+
result = llm.invoke("Summarise the Silvol architecture in one sentence.")
|
|
125
|
+
print(result.content)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## CrewAI
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from silvol.integrations.crewai import SilvolLLM
|
|
134
|
+
from crewai import Agent, Task, Crew
|
|
135
|
+
|
|
136
|
+
llm = SilvolLLM(api_key="sk-svl-...")
|
|
137
|
+
|
|
138
|
+
researcher = Agent(
|
|
139
|
+
role="Senior Researcher",
|
|
140
|
+
goal="Uncover groundbreaking technologies in AI",
|
|
141
|
+
backstory="...",
|
|
142
|
+
llm=llm,
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Models
|
|
149
|
+
|
|
150
|
+
| Model ID | Context | Notes |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `DeepSeek-R1-Distill-Qwen-7B` | 32k | Always-on (free tier) |
|
|
153
|
+
| `llama-3.1-70b` | 128k | On-demand deployment |
|
|
154
|
+
| `qwen-2.5-coder-32b` | 32k | On-demand deployment |
|
|
155
|
+
|
|
156
|
+
Full list: `GET https://api.silvol.ai/v1/models`
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Authentication
|
|
161
|
+
|
|
162
|
+
Get your API key from the [Silvol Dashboard](https://silvol.ai/dashboard).
|
|
163
|
+
Keys are prefixed `sk-svl-`. Pass it as `api_key=` or set the `OPENAI_API_KEY`
|
|
164
|
+
environment variable (the SDK checks it automatically).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Links
|
|
169
|
+
|
|
170
|
+
- Docs: [silvol.ai/docs](https://silvol.ai/docs)
|
|
171
|
+
- Dashboard: [silvol.ai/dashboard](https://silvol.ai/dashboard)
|
|
172
|
+
- PyPI: [pypi.org/project/silvol](https://pypi.org/project/silvol)
|
|
173
|
+
- GitHub: [github.com/optimuscodexprimus/silvol-python](https://github.com/optimuscodexprimus/silvol-python)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
silvol/__init__.py,sha256=yQyCF6Rr2pSG3nFZp-KNTQhkNXXfvXkzYZ1EtemOqJs,1047
|
|
2
|
+
silvol/_version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
3
|
+
silvol/client.py,sha256=MMv0KcfKBhXf24wTdRZoQX153p8dQQXMOMjVXRlIOns,2195
|
|
4
|
+
silvol/finetune.py,sha256=XQ8P9wUxiQ-kwfX_qdnnXP5nc2z0JR3oxxQxBP3ietg,11232
|
|
5
|
+
silvol/integrations/__init__.py,sha256=hTjlDVsCUJnEMTsfYqfna_txr0JIqlDmbDSOpSYw8mc,226
|
|
6
|
+
silvol/integrations/crewai.py,sha256=O7l4uAwD_t3YUAMULhJJal5iJ_OQjhmkNfIgcoqJ2_0,1015
|
|
7
|
+
silvol/integrations/langchain.py,sha256=OUV1hY_I4VUBXbVozxjcvzNkIVmQN00T8fT_Tmp6dJk,1090
|
|
8
|
+
silvol-0.2.0.dist-info/METADATA,sha256=AYSXHtH7QWL16pJaxliCQRGMl8qhkieLyG7MTXUZrrs,5236
|
|
9
|
+
silvol-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
silvol-0.2.0.dist-info/licenses/LICENSE,sha256=qiFscecdrSkdosFBZBJaHUw2m0Fu8bh9YtE3drb6lTk,1063
|
|
11
|
+
silvol-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Silvol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|