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.
@@ -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
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .env
9
+ *.egg
10
+ .pytest_cache/
11
+ .mypy_cache/
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,4 @@
1
+ # Integrations are imported lazily to avoid hard-coding optional dependencies.
2
+ # Import from submodules directly:
3
+ # from silvol.integrations.langchain import SilvolChat
4
+ # from silvol.integrations.crewai import SilvolLLM
@@ -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__}"