silvol 0.2.0__tar.gz
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-0.2.0/.github/workflows/publish.yml +66 -0
- silvol-0.2.0/.gitignore +11 -0
- silvol-0.2.0/CLAUDE.md +50 -0
- silvol-0.2.0/LICENSE +21 -0
- silvol-0.2.0/PKG-INFO +173 -0
- silvol-0.2.0/README.md +117 -0
- silvol-0.2.0/pyproject.toml +48 -0
- silvol-0.2.0/src/silvol/__init__.py +40 -0
- silvol-0.2.0/src/silvol/_version.py +1 -0
- silvol-0.2.0/src/silvol/client.py +87 -0
- silvol-0.2.0/src/silvol/finetune.py +285 -0
- silvol-0.2.0/src/silvol/integrations/__init__.py +4 -0
- silvol-0.2.0/src/silvol/integrations/crewai.py +47 -0
- silvol-0.2.0/src/silvol/integrations/langchain.py +47 -0
- silvol-0.2.0/tests/test_client.py +56 -0
- silvol-0.2.0/tests/test_finetune.py +80 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main] # → TestPyPI on every push to main
|
|
6
|
+
release:
|
|
7
|
+
types: [published] # → PyPI on tagged GitHub releases
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
|
|
20
|
+
- name: Install build tools
|
|
21
|
+
run: pip install build
|
|
22
|
+
|
|
23
|
+
- name: Build wheel + sdist
|
|
24
|
+
run: python -m build
|
|
25
|
+
|
|
26
|
+
- name: Upload dist as artifact
|
|
27
|
+
uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish-testpypi:
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
36
|
+
environment: testpypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write # OIDC trusted publishing
|
|
39
|
+
steps:
|
|
40
|
+
- name: Download dist artifact
|
|
41
|
+
uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
|
|
46
|
+
- name: Publish to TestPyPI
|
|
47
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
48
|
+
with:
|
|
49
|
+
repository-url: https://test.pypi.org/legacy/
|
|
50
|
+
|
|
51
|
+
publish-pypi:
|
|
52
|
+
needs: build
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
if: github.event_name == 'release' && github.event.action == 'published'
|
|
55
|
+
environment: pypi
|
|
56
|
+
permissions:
|
|
57
|
+
id-token: write # OIDC trusted publishing
|
|
58
|
+
steps:
|
|
59
|
+
- name: Download dist artifact
|
|
60
|
+
uses: actions/download-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: dist
|
|
63
|
+
path: dist/
|
|
64
|
+
|
|
65
|
+
- name: Publish to PyPI
|
|
66
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
silvol-0.2.0/.gitignore
ADDED
silvol-0.2.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# CLAUDE.md — silvol-python (PyPI SDK)
|
|
2
|
+
|
|
3
|
+
> **For Claude:** Full project context lives in the `silvol` repo:
|
|
4
|
+
> `C:\Users\otien\AppData\Local\Temp\silvol\CLAUDE.md` — read that first.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## This Repo at a Glance
|
|
9
|
+
|
|
10
|
+
- **Purpose:** Thin Python SDK wrapping the Silvol OpenAI-compatible API + fine-tuning API
|
|
11
|
+
- **PyPI package name:** `silvol`
|
|
12
|
+
- **Version:** 0.2.0 — added fine-tuning client at `client.finetune` (2026-05-30)
|
|
13
|
+
- **GitHub:** `https://github.com/optimuscodexprimus/silvol-python`
|
|
14
|
+
- **Local clone:** `C:\Users\otien\AppData\Local\Temp\silvol-python`
|
|
15
|
+
- **Default base URL:** `https://api.silvol.ai/v1`
|
|
16
|
+
|
|
17
|
+
## Package Layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/silvol/
|
|
21
|
+
__init__.py Exports: Silvol, AsyncSilvol, Finetune, AsyncFinetune, FinetuneError, __version__
|
|
22
|
+
client.py Silvol (subclasses openai.OpenAI) + .finetune; AsyncSilvol (subclasses openai.AsyncOpenAI) + .finetune
|
|
23
|
+
finetune.py Finetune, AsyncFinetune — wraps /v1/finetune/* endpoints
|
|
24
|
+
_version.py __version__ = "0.2.0"
|
|
25
|
+
integrations/
|
|
26
|
+
langchain.py SilvolChat() → ChatOpenAI wired to Silvol gateway
|
|
27
|
+
crewai.py SilvolLLM() → crewai.LLM wired to Silvol gateway
|
|
28
|
+
tests/
|
|
29
|
+
test_client.py 5 smoke tests (client init + base URL)
|
|
30
|
+
test_finetune.py 7 smoke tests (attribute attached, dataset encoder, exports)
|
|
31
|
+
.github/workflows/
|
|
32
|
+
publish.yml push to main → TestPyPI; tagged release → PyPI (OIDC)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Building & Testing
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Build wheel + sdist
|
|
39
|
+
python -m build
|
|
40
|
+
|
|
41
|
+
# Run tests (from repo root, with src on PYTHONPATH)
|
|
42
|
+
PYTHONPATH=src python -m pytest tests/ -v
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## PyPI Publish Checklist (manual — see Prompt 8 TODOs)
|
|
46
|
+
|
|
47
|
+
1. Create account at pypi.org + test.pypi.org
|
|
48
|
+
2. `python -m twine upload --repository testpypi dist/*`
|
|
49
|
+
3. `pip install --index-url https://test.pypi.org/simple/ silvol` (verify)
|
|
50
|
+
4. Tag a release → GitHub Actions auto-publishes to production PyPI
|
silvol-0.2.0/LICENSE
ADDED
|
@@ -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.
|
silvol-0.2.0/PKG-INFO
ADDED
|
@@ -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)
|
silvol-0.2.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# silvol-python
|
|
2
|
+
|
|
3
|
+
Python SDK for [Silvol](https://silvol.ai) — an OpenAI-compatible inference API running on
|
|
4
|
+
Nosana's decentralised GPU grid.
|
|
5
|
+
|
|
6
|
+
Drop-in replacement for the OpenAI SDK. Change the base URL and your key; keep the rest
|
|
7
|
+
of your code.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install silvol
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
With optional framework integrations:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install silvol[langchain] # LangChain
|
|
21
|
+
pip install silvol[crewai] # CrewAI
|
|
22
|
+
pip install silvol[all] # both
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from silvol import Silvol
|
|
31
|
+
|
|
32
|
+
client = Silvol(api_key="sk-svl-...") # or set SILVOL_API_KEY env var
|
|
33
|
+
|
|
34
|
+
resp = client.chat.completions.create(
|
|
35
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
36
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
37
|
+
)
|
|
38
|
+
print(resp.choices[0].message.content)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Async:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
from silvol import AsyncSilvol
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
client = AsyncSilvol(api_key="sk-svl-...")
|
|
49
|
+
resp = await client.chat.completions.create(
|
|
50
|
+
model="DeepSeek-R1-Distill-Qwen-7B",
|
|
51
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
52
|
+
stream=True,
|
|
53
|
+
)
|
|
54
|
+
async for chunk in resp:
|
|
55
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
56
|
+
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## LangChain
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from silvol.integrations.langchain import SilvolChat
|
|
66
|
+
|
|
67
|
+
llm = SilvolChat(api_key="sk-svl-...")
|
|
68
|
+
result = llm.invoke("Summarise the Silvol architecture in one sentence.")
|
|
69
|
+
print(result.content)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## CrewAI
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from silvol.integrations.crewai import SilvolLLM
|
|
78
|
+
from crewai import Agent, Task, Crew
|
|
79
|
+
|
|
80
|
+
llm = SilvolLLM(api_key="sk-svl-...")
|
|
81
|
+
|
|
82
|
+
researcher = Agent(
|
|
83
|
+
role="Senior Researcher",
|
|
84
|
+
goal="Uncover groundbreaking technologies in AI",
|
|
85
|
+
backstory="...",
|
|
86
|
+
llm=llm,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Models
|
|
93
|
+
|
|
94
|
+
| Model ID | Context | Notes |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `DeepSeek-R1-Distill-Qwen-7B` | 32k | Always-on (free tier) |
|
|
97
|
+
| `llama-3.1-70b` | 128k | On-demand deployment |
|
|
98
|
+
| `qwen-2.5-coder-32b` | 32k | On-demand deployment |
|
|
99
|
+
|
|
100
|
+
Full list: `GET https://api.silvol.ai/v1/models`
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Authentication
|
|
105
|
+
|
|
106
|
+
Get your API key from the [Silvol Dashboard](https://silvol.ai/dashboard).
|
|
107
|
+
Keys are prefixed `sk-svl-`. Pass it as `api_key=` or set the `OPENAI_API_KEY`
|
|
108
|
+
environment variable (the SDK checks it automatically).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Links
|
|
113
|
+
|
|
114
|
+
- Docs: [silvol.ai/docs](https://silvol.ai/docs)
|
|
115
|
+
- Dashboard: [silvol.ai/dashboard](https://silvol.ai/dashboard)
|
|
116
|
+
- PyPI: [pypi.org/project/silvol](https://pypi.org/project/silvol)
|
|
117
|
+
- GitHub: [github.com/optimuscodexprimus/silvol-python](https://github.com/optimuscodexprimus/silvol-python)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "silvol"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for the Silvol inference API — OpenAI-compatible, decentralised GPU."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Silvol", email = "hello@silvol.ai" }]
|
|
13
|
+
keywords = ["llm", "inference", "openai", "nosana", "gpu", "silvol"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"openai>=1.0",
|
|
26
|
+
"httpx>=0.25",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
langchain = ["langchain-openai>=0.1"]
|
|
31
|
+
crewai = ["crewai>=0.30"]
|
|
32
|
+
all = ["langchain-openai>=0.1", "crewai>=0.30"]
|
|
33
|
+
dev = ["pytest>=7", "build", "twine"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://silvol.ai"
|
|
37
|
+
Documentation = "https://silvol.ai/docs"
|
|
38
|
+
Repository = "https://github.com/optimuscodexprimus/silvol-python"
|
|
39
|
+
"Bug Tracker" = "https://github.com/optimuscodexprimus/silvol-python/issues"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.version]
|
|
42
|
+
path = "src/silvol/_version.py"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/silvol"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -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)
|
|
@@ -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,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smoke tests for the Silvol SDK.
|
|
3
|
+
|
|
4
|
+
These tests run without a real API key — they verify that the client
|
|
5
|
+
initialises correctly and points at the right base URL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_silvol_default_base_url():
|
|
12
|
+
"""Silvol() should use the Silvol gateway base URL by default."""
|
|
13
|
+
from silvol import Silvol
|
|
14
|
+
|
|
15
|
+
with patch("openai.OpenAI.__init__", return_value=None) as mock_init:
|
|
16
|
+
Silvol(api_key="sk-svl-test")
|
|
17
|
+
_, kwargs = mock_init.call_args
|
|
18
|
+
assert kwargs.get("base_url") == "https://api.silvol.ai/v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_silvol_custom_base_url():
|
|
22
|
+
"""Silvol() should respect a caller-supplied base_url."""
|
|
23
|
+
from silvol import Silvol
|
|
24
|
+
|
|
25
|
+
custom = "https://my-proxy.example.com/v1"
|
|
26
|
+
with patch("openai.OpenAI.__init__", return_value=None) as mock_init:
|
|
27
|
+
Silvol(api_key="sk-svl-test", base_url=custom)
|
|
28
|
+
_, kwargs = mock_init.call_args
|
|
29
|
+
assert kwargs.get("base_url") == custom
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_async_silvol_default_base_url():
|
|
33
|
+
"""AsyncSilvol() should use the Silvol gateway base URL by default."""
|
|
34
|
+
from silvol import AsyncSilvol
|
|
35
|
+
|
|
36
|
+
with patch("openai.AsyncOpenAI.__init__", return_value=None) as mock_init:
|
|
37
|
+
AsyncSilvol(api_key="sk-svl-test")
|
|
38
|
+
_, kwargs = mock_init.call_args
|
|
39
|
+
assert kwargs.get("base_url") == "https://api.silvol.ai/v1"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_version_exported():
|
|
43
|
+
"""__version__ should be importable from the top-level package."""
|
|
44
|
+
import silvol
|
|
45
|
+
|
|
46
|
+
assert isinstance(silvol.__version__, str)
|
|
47
|
+
assert silvol.__version__ != ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_exports():
|
|
51
|
+
"""Top-level package should export Silvol, AsyncSilvol, __version__."""
|
|
52
|
+
import silvol
|
|
53
|
+
|
|
54
|
+
assert hasattr(silvol, "Silvol")
|
|
55
|
+
assert hasattr(silvol, "AsyncSilvol")
|
|
56
|
+
assert hasattr(silvol, "__version__")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smoke tests for the Silvol fine-tuning SDK.
|
|
3
|
+
|
|
4
|
+
These tests verify the client attaches the finetune attribute, that the
|
|
5
|
+
dataset encoder accepts multiple input shapes, and that the FinetuneError
|
|
6
|
+
class is exported.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_finetune_attribute_attached_sync():
|
|
15
|
+
"""Silvol() should expose .finetune as a Finetune instance."""
|
|
16
|
+
from silvol import Silvol, Finetune
|
|
17
|
+
|
|
18
|
+
with patch("openai.OpenAI.__init__", return_value=None):
|
|
19
|
+
c = Silvol(api_key="sk-svl-test")
|
|
20
|
+
assert isinstance(c.finetune, Finetune)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_finetune_attribute_attached_async():
|
|
24
|
+
"""AsyncSilvol() should expose .finetune as an AsyncFinetune instance."""
|
|
25
|
+
from silvol import AsyncSilvol, AsyncFinetune
|
|
26
|
+
|
|
27
|
+
with patch("openai.AsyncOpenAI.__init__", return_value=None):
|
|
28
|
+
c = AsyncSilvol(api_key="sk-svl-test")
|
|
29
|
+
assert isinstance(c.finetune, AsyncFinetune)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_encode_dataset_bytes():
|
|
33
|
+
"""_encode_dataset accepts raw bytes."""
|
|
34
|
+
from silvol.finetune import _encode_dataset
|
|
35
|
+
|
|
36
|
+
raw = b'{"messages":[{"role":"user","content":"hi"}]}\n'
|
|
37
|
+
encoded = _encode_dataset(raw)
|
|
38
|
+
assert base64.b64decode(encoded) == raw
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_encode_dataset_string():
|
|
42
|
+
"""_encode_dataset accepts a JSONL string."""
|
|
43
|
+
from silvol.finetune import _encode_dataset
|
|
44
|
+
|
|
45
|
+
raw = '{"messages":[{"role":"user","content":"hi"}]}'
|
|
46
|
+
encoded = _encode_dataset(raw)
|
|
47
|
+
assert base64.b64decode(encoded).decode("utf-8") == raw
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_encode_dataset_iterable_of_dicts():
|
|
51
|
+
"""_encode_dataset converts an iterable of dicts to JSONL."""
|
|
52
|
+
from silvol.finetune import _encode_dataset
|
|
53
|
+
|
|
54
|
+
examples = [
|
|
55
|
+
{"messages": [{"role": "user", "content": "one"}]},
|
|
56
|
+
{"messages": [{"role": "user", "content": "two"}]},
|
|
57
|
+
]
|
|
58
|
+
encoded = _encode_dataset(examples)
|
|
59
|
+
decoded = base64.b64decode(encoded).decode("utf-8")
|
|
60
|
+
lines = decoded.split("\n")
|
|
61
|
+
assert len(lines) == 2
|
|
62
|
+
assert json.loads(lines[0])["messages"][0]["content"] == "one"
|
|
63
|
+
assert json.loads(lines[1])["messages"][0]["content"] == "two"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_finetune_error_exported():
|
|
67
|
+
"""FinetuneError should be importable from the top-level package."""
|
|
68
|
+
import silvol
|
|
69
|
+
|
|
70
|
+
assert hasattr(silvol, "FinetuneError")
|
|
71
|
+
assert issubclass(silvol.FinetuneError, Exception)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_version_bumped_to_0_2():
|
|
75
|
+
"""SDK version should be bumped to 0.2.x for the fine-tuning release."""
|
|
76
|
+
import silvol
|
|
77
|
+
|
|
78
|
+
parts = silvol.__version__.split(".")
|
|
79
|
+
assert parts[0] == "0"
|
|
80
|
+
assert int(parts[1]) >= 2, f"expected version >= 0.2.0 for finetune release, got {silvol.__version__}"
|