singularity-grid 0.1.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.
- singularity_grid-0.1.0/.github/workflows/publish.yml +29 -0
- singularity_grid-0.1.0/.gitignore +5 -0
- singularity_grid-0.1.0/LICENSE +21 -0
- singularity_grid-0.1.0/PKG-INFO +152 -0
- singularity_grid-0.1.0/README.md +124 -0
- singularity_grid-0.1.0/examples/check_capacity.py +28 -0
- singularity_grid-0.1.0/examples/openai_compatible.py +39 -0
- singularity_grid-0.1.0/examples/quickstart.py +31 -0
- singularity_grid-0.1.0/pyproject.toml +39 -0
- singularity_grid-0.1.0/singularity_grid/__init__.py +50 -0
- singularity_grid-0.1.0/singularity_grid/client.py +213 -0
- singularity_grid-0.1.0/singularity_grid/models.py +110 -0
- singularity_grid-0.1.0/singularity_grid/openai_compat.py +51 -0
- singularity_grid-0.1.0/singularity_grid/py.typed +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Install build tools
|
|
23
|
+
run: pip install build
|
|
24
|
+
|
|
25
|
+
- name: Build package
|
|
26
|
+
run: python -m build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Singularity Layer
|
|
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.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: singularity-grid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the SGL Network confidential compute grid
|
|
5
|
+
Project-URL: Homepage, https://singularitylayer.xyz
|
|
6
|
+
Project-URL: Repository, https://github.com/SingularityLayer/sgl-network-sdk
|
|
7
|
+
Project-URL: Documentation, https://docs.singularitylayer.xyz/sdk
|
|
8
|
+
Author-email: Singularity Layer <dev@singularitylayer.xyz>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.24
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: openai
|
|
26
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# singularity-grid
|
|
30
|
+
|
|
31
|
+
Python SDK for the SGL Network confidential compute grid. Submit inference jobs to TEE-verified nodes, check grid capacity, and use the OpenAI-compatible chat completions endpoint -- all from a single package.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install singularity-grid
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To use the OpenAI compatibility helper:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install singularity-grid[openai]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
### 1. OpenAI-compatible usage (simplest)
|
|
48
|
+
|
|
49
|
+
The SGL Network orchestrator exposes an OpenAI-compatible `/v1/chat/completions` endpoint. You can use the standard `openai` package with no wrapper:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from openai import OpenAI
|
|
53
|
+
|
|
54
|
+
client = OpenAI(
|
|
55
|
+
base_url="https://sgl-network-orchestrator.ivaavimusicproductions.workers.dev/v1",
|
|
56
|
+
api_key="scg_your_api_key",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
response = client.chat.completions.create(
|
|
60
|
+
model="gemma-4-26b",
|
|
61
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
62
|
+
)
|
|
63
|
+
print(response.choices[0].message.content)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Helper function
|
|
67
|
+
|
|
68
|
+
If you prefer not to copy the URL, use the built-in helper:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from singularity_grid import create_openai_client
|
|
72
|
+
|
|
73
|
+
client = create_openai_client(api_key="scg_your_api_key")
|
|
74
|
+
|
|
75
|
+
response = client.chat.completions.create(
|
|
76
|
+
model="gemma-4-26b",
|
|
77
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
78
|
+
)
|
|
79
|
+
print(response.choices[0].message.content)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. GridClient (full grid features)
|
|
83
|
+
|
|
84
|
+
For grid-specific features like job submission, TEE attestation, capacity checks, and pricing:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from singularity_grid import GridClient
|
|
88
|
+
|
|
89
|
+
# Public endpoints -- no auth required
|
|
90
|
+
grid = GridClient()
|
|
91
|
+
capacity = grid.capacity()
|
|
92
|
+
models = grid.models()
|
|
93
|
+
pricing = grid.pricing()
|
|
94
|
+
|
|
95
|
+
print(f"Active nodes: {capacity.active_nodes}/{capacity.total_nodes}")
|
|
96
|
+
for m in models:
|
|
97
|
+
print(f" {m.id} -- {m.sgl_node_count} nodes")
|
|
98
|
+
|
|
99
|
+
# Authenticated endpoints
|
|
100
|
+
grid = GridClient(api_key="scg_your_api_key")
|
|
101
|
+
|
|
102
|
+
job = grid.submit_job(
|
|
103
|
+
model="gemma-4-26b",
|
|
104
|
+
input_payload={
|
|
105
|
+
"messages": [{"role": "user", "content": "Analyze this data"}]
|
|
106
|
+
},
|
|
107
|
+
submitter_wallet="0xYourWallet",
|
|
108
|
+
submitter_chain="base",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
print(f"Job {job.job_id}: {job.status}")
|
|
112
|
+
|
|
113
|
+
# Retrieve result and attestation
|
|
114
|
+
result = grid.get_job(job.job_id)
|
|
115
|
+
attestation = grid.get_attestation(job.job_id)
|
|
116
|
+
print(f"TEE verified: {attestation.verified}")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API reference
|
|
120
|
+
|
|
121
|
+
### GridClient
|
|
122
|
+
|
|
123
|
+
| Method | Auth | Description |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `capacity()` | No | Grid-wide capacity summary |
|
|
126
|
+
| `models()` | No | Available models with pricing and TEE info |
|
|
127
|
+
| `pricing()` | No | Pricing table for all models |
|
|
128
|
+
| `submit_job(model, input_payload, ...)` | Yes | Submit a compute job |
|
|
129
|
+
| `get_job(job_id)` | Yes | Get job status and result |
|
|
130
|
+
| `get_attestation(job_id)` | Yes | Get TEE attestation proof |
|
|
131
|
+
|
|
132
|
+
### Exceptions
|
|
133
|
+
|
|
134
|
+
| Exception | When |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `SGLError` | Base class for all SDK errors |
|
|
137
|
+
| `SGLAPIError` | Non-2xx response from the API |
|
|
138
|
+
| `SGLAuthError` | 401 or 403 response |
|
|
139
|
+
| `SGLNotFoundError` | 404 response |
|
|
140
|
+
| `SGLConnectionError` | Orchestrator unreachable or timeout |
|
|
141
|
+
|
|
142
|
+
### Configuration
|
|
143
|
+
|
|
144
|
+
| Parameter | Default | Description |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `api_key` | `None` | Bearer token for authenticated endpoints |
|
|
147
|
+
| `base_url` | orchestrator URL | Override the orchestrator URL |
|
|
148
|
+
| `timeout` | `60.0` | Request timeout in seconds |
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# singularity-grid
|
|
2
|
+
|
|
3
|
+
Python SDK for the SGL Network confidential compute grid. Submit inference jobs to TEE-verified nodes, check grid capacity, and use the OpenAI-compatible chat completions endpoint -- all from a single package.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install singularity-grid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
To use the OpenAI compatibility helper:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install singularity-grid[openai]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
### 1. OpenAI-compatible usage (simplest)
|
|
20
|
+
|
|
21
|
+
The SGL Network orchestrator exposes an OpenAI-compatible `/v1/chat/completions` endpoint. You can use the standard `openai` package with no wrapper:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from openai import OpenAI
|
|
25
|
+
|
|
26
|
+
client = OpenAI(
|
|
27
|
+
base_url="https://sgl-network-orchestrator.ivaavimusicproductions.workers.dev/v1",
|
|
28
|
+
api_key="scg_your_api_key",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
response = client.chat.completions.create(
|
|
32
|
+
model="gemma-4-26b",
|
|
33
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
34
|
+
)
|
|
35
|
+
print(response.choices[0].message.content)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Helper function
|
|
39
|
+
|
|
40
|
+
If you prefer not to copy the URL, use the built-in helper:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from singularity_grid import create_openai_client
|
|
44
|
+
|
|
45
|
+
client = create_openai_client(api_key="scg_your_api_key")
|
|
46
|
+
|
|
47
|
+
response = client.chat.completions.create(
|
|
48
|
+
model="gemma-4-26b",
|
|
49
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
50
|
+
)
|
|
51
|
+
print(response.choices[0].message.content)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. GridClient (full grid features)
|
|
55
|
+
|
|
56
|
+
For grid-specific features like job submission, TEE attestation, capacity checks, and pricing:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from singularity_grid import GridClient
|
|
60
|
+
|
|
61
|
+
# Public endpoints -- no auth required
|
|
62
|
+
grid = GridClient()
|
|
63
|
+
capacity = grid.capacity()
|
|
64
|
+
models = grid.models()
|
|
65
|
+
pricing = grid.pricing()
|
|
66
|
+
|
|
67
|
+
print(f"Active nodes: {capacity.active_nodes}/{capacity.total_nodes}")
|
|
68
|
+
for m in models:
|
|
69
|
+
print(f" {m.id} -- {m.sgl_node_count} nodes")
|
|
70
|
+
|
|
71
|
+
# Authenticated endpoints
|
|
72
|
+
grid = GridClient(api_key="scg_your_api_key")
|
|
73
|
+
|
|
74
|
+
job = grid.submit_job(
|
|
75
|
+
model="gemma-4-26b",
|
|
76
|
+
input_payload={
|
|
77
|
+
"messages": [{"role": "user", "content": "Analyze this data"}]
|
|
78
|
+
},
|
|
79
|
+
submitter_wallet="0xYourWallet",
|
|
80
|
+
submitter_chain="base",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print(f"Job {job.job_id}: {job.status}")
|
|
84
|
+
|
|
85
|
+
# Retrieve result and attestation
|
|
86
|
+
result = grid.get_job(job.job_id)
|
|
87
|
+
attestation = grid.get_attestation(job.job_id)
|
|
88
|
+
print(f"TEE verified: {attestation.verified}")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API reference
|
|
92
|
+
|
|
93
|
+
### GridClient
|
|
94
|
+
|
|
95
|
+
| Method | Auth | Description |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `capacity()` | No | Grid-wide capacity summary |
|
|
98
|
+
| `models()` | No | Available models with pricing and TEE info |
|
|
99
|
+
| `pricing()` | No | Pricing table for all models |
|
|
100
|
+
| `submit_job(model, input_payload, ...)` | Yes | Submit a compute job |
|
|
101
|
+
| `get_job(job_id)` | Yes | Get job status and result |
|
|
102
|
+
| `get_attestation(job_id)` | Yes | Get TEE attestation proof |
|
|
103
|
+
|
|
104
|
+
### Exceptions
|
|
105
|
+
|
|
106
|
+
| Exception | When |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `SGLError` | Base class for all SDK errors |
|
|
109
|
+
| `SGLAPIError` | Non-2xx response from the API |
|
|
110
|
+
| `SGLAuthError` | 401 or 403 response |
|
|
111
|
+
| `SGLNotFoundError` | 404 response |
|
|
112
|
+
| `SGLConnectionError` | Orchestrator unreachable or timeout |
|
|
113
|
+
|
|
114
|
+
### Configuration
|
|
115
|
+
|
|
116
|
+
| Parameter | Default | Description |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `api_key` | `None` | Bearer token for authenticated endpoints |
|
|
119
|
+
| `base_url` | orchestrator URL | Override the orchestrator URL |
|
|
120
|
+
| `timeout` | `60.0` | Request timeout in seconds |
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Check grid capacity, available models, and pricing (no auth required)."""
|
|
2
|
+
|
|
3
|
+
from singularity_grid import GridClient
|
|
4
|
+
|
|
5
|
+
grid = GridClient()
|
|
6
|
+
|
|
7
|
+
# Grid capacity
|
|
8
|
+
capacity = grid.capacity()
|
|
9
|
+
print(f"Nodes: {capacity.active_nodes}/{capacity.total_nodes} active")
|
|
10
|
+
for tee in capacity.by_tee_type:
|
|
11
|
+
print(f" {tee.tee_type}: {tee.active_nodes}/{tee.total_nodes}")
|
|
12
|
+
|
|
13
|
+
# Available models
|
|
14
|
+
print("\nModels:")
|
|
15
|
+
models = grid.models()
|
|
16
|
+
for m in models:
|
|
17
|
+
tees = ", ".join(m.sgl_tee_types) if m.sgl_tee_types else "none"
|
|
18
|
+
print(f" {m.id} -- {m.sgl_node_count} nodes, TEE: {tees}")
|
|
19
|
+
|
|
20
|
+
# Pricing
|
|
21
|
+
print("\nPricing:")
|
|
22
|
+
pricing = grid.pricing()
|
|
23
|
+
for p in pricing:
|
|
24
|
+
print(
|
|
25
|
+
f" {p.model}: "
|
|
26
|
+
f"${p.price_per_1k_input_tokens_usd}/1k input, "
|
|
27
|
+
f"${p.price_per_1k_output_tokens_usd}/1k output"
|
|
28
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Use the SGL Network with the standard OpenAI Python SDK.
|
|
2
|
+
|
|
3
|
+
The SGL Network orchestrator exposes an OpenAI-compatible /v1/chat/completions
|
|
4
|
+
endpoint, so you can use the openai package directly with no code changes
|
|
5
|
+
beyond swapping the base URL and API key.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# --- Option 1: raw OpenAI SDK (no singularity-grid import needed) ----------
|
|
9
|
+
|
|
10
|
+
from openai import OpenAI
|
|
11
|
+
|
|
12
|
+
client = OpenAI(
|
|
13
|
+
base_url="https://sgl-network-orchestrator.ivaavimusicproductions.workers.dev/v1",
|
|
14
|
+
api_key="scg_your_api_key_here",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
response = client.chat.completions.create(
|
|
18
|
+
model="gemma-4-26b",
|
|
19
|
+
messages=[{"role": "user", "content": "Hello from the SGL Network!"}],
|
|
20
|
+
)
|
|
21
|
+
print(response.choices[0].message.content)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- Option 2: use the helper from singularity-grid -----------------------
|
|
25
|
+
|
|
26
|
+
from singularity_grid import create_openai_client
|
|
27
|
+
|
|
28
|
+
client2 = create_openai_client(api_key="scg_your_api_key_here")
|
|
29
|
+
|
|
30
|
+
response2 = client2.chat.completions.create(
|
|
31
|
+
model="gemma-4-26b",
|
|
32
|
+
messages=[{"role": "user", "content": "What TEE type processed this?"}],
|
|
33
|
+
)
|
|
34
|
+
print(response2.choices[0].message.content)
|
|
35
|
+
|
|
36
|
+
# List available models
|
|
37
|
+
models = client2.models.list()
|
|
38
|
+
for m in models.data:
|
|
39
|
+
print(f" {m.id} (owned by {m.owned_by})")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Quickstart: submit a job to the SGL Network and retrieve its result."""
|
|
2
|
+
|
|
3
|
+
from singularity_grid import GridClient
|
|
4
|
+
|
|
5
|
+
# Authenticated client for job submission
|
|
6
|
+
grid = GridClient(api_key="scg_your_api_key_here")
|
|
7
|
+
|
|
8
|
+
# Submit an inference job
|
|
9
|
+
job = grid.submit_job(
|
|
10
|
+
model="gemma-4-26b",
|
|
11
|
+
input_payload={
|
|
12
|
+
"messages": [{"role": "user", "content": "Explain zero-knowledge proofs in one paragraph."}]
|
|
13
|
+
},
|
|
14
|
+
submitter_wallet="0xYourWalletAddress",
|
|
15
|
+
submitter_chain="base",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
print(f"Job submitted: {job.job_id} (status: {job.status})")
|
|
19
|
+
print(f"Estimated cost: ${job.estimated_cost_usd}")
|
|
20
|
+
|
|
21
|
+
# Poll for result (in production, use a callback or webhook)
|
|
22
|
+
result = grid.get_job(job.job_id)
|
|
23
|
+
print(f"Job status: {result.status}")
|
|
24
|
+
|
|
25
|
+
if result.status == "completed":
|
|
26
|
+
print(f"Result: {result.result}")
|
|
27
|
+
|
|
28
|
+
# Verify TEE attestation
|
|
29
|
+
attestation = grid.get_attestation(job.job_id)
|
|
30
|
+
print(f"TEE type: {attestation.tee_type}")
|
|
31
|
+
print(f"Verified: {attestation.verified}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "singularity-grid"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the SGL Network confidential compute grid"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Singularity Layer", email = "dev@singularitylayer.xyz" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.24",
|
|
30
|
+
"pydantic>=2.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
openai = ["openai>=1.0"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://singularitylayer.xyz"
|
|
38
|
+
Repository = "https://github.com/SingularityLayer/sgl-network-sdk"
|
|
39
|
+
Documentation = "https://docs.singularitylayer.xyz/sdk"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""singularity-grid -- Python SDK for the SGL Network compute grid."""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
DEFAULT_BASE_URL,
|
|
5
|
+
GridClient,
|
|
6
|
+
SGLAPIError,
|
|
7
|
+
SGLAuthError,
|
|
8
|
+
SGLConnectionError,
|
|
9
|
+
SGLError,
|
|
10
|
+
SGLNotFoundError,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
AttestationProof,
|
|
14
|
+
CapacityResponse,
|
|
15
|
+
JobResponse,
|
|
16
|
+
JobResult,
|
|
17
|
+
ModelInfo,
|
|
18
|
+
ModelPricing,
|
|
19
|
+
ModelsResponse,
|
|
20
|
+
PricingInfo,
|
|
21
|
+
PricingResponse,
|
|
22
|
+
TeeCapacity,
|
|
23
|
+
)
|
|
24
|
+
from .openai_compat import create_openai_client
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Client
|
|
30
|
+
"GridClient",
|
|
31
|
+
"create_openai_client",
|
|
32
|
+
"DEFAULT_BASE_URL",
|
|
33
|
+
# Exceptions
|
|
34
|
+
"SGLError",
|
|
35
|
+
"SGLAPIError",
|
|
36
|
+
"SGLAuthError",
|
|
37
|
+
"SGLConnectionError",
|
|
38
|
+
"SGLNotFoundError",
|
|
39
|
+
# Models
|
|
40
|
+
"AttestationProof",
|
|
41
|
+
"CapacityResponse",
|
|
42
|
+
"JobResponse",
|
|
43
|
+
"JobResult",
|
|
44
|
+
"ModelInfo",
|
|
45
|
+
"ModelPricing",
|
|
46
|
+
"ModelsResponse",
|
|
47
|
+
"PricingInfo",
|
|
48
|
+
"PricingResponse",
|
|
49
|
+
"TeeCapacity",
|
|
50
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""GridClient — main entry point for the SGL Network compute grid."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
AttestationProof,
|
|
11
|
+
CapacityResponse,
|
|
12
|
+
JobResponse,
|
|
13
|
+
JobResult,
|
|
14
|
+
ModelInfo,
|
|
15
|
+
ModelsResponse,
|
|
16
|
+
PricingInfo,
|
|
17
|
+
PricingResponse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DEFAULT_BASE_URL = (
|
|
21
|
+
"https://sgl-network-orchestrator.ivaavimusicproductions.workers.dev"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DEFAULT_TIMEOUT = 60.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Exceptions
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
class SGLError(Exception):
|
|
32
|
+
"""Base exception for all SGL Network SDK errors."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SGLAPIError(SGLError):
|
|
36
|
+
"""Raised when the API returns a non-2xx status code."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
status_code: int,
|
|
41
|
+
message: str,
|
|
42
|
+
body: Optional[Dict[str, Any]] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.status_code = status_code
|
|
45
|
+
self.body = body
|
|
46
|
+
super().__init__(f"HTTP {status_code}: {message}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SGLAuthError(SGLAPIError):
|
|
50
|
+
"""Raised on 401/403 responses."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SGLNotFoundError(SGLAPIError):
|
|
54
|
+
"""Raised on 404 responses."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SGLConnectionError(SGLError):
|
|
58
|
+
"""Raised when the orchestrator is unreachable."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Client
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
class GridClient:
|
|
66
|
+
"""Client for the SGL Network orchestrator API.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
api_key:
|
|
71
|
+
Bearer token for authenticated endpoints (e.g. ``scg_...``).
|
|
72
|
+
Not required for public endpoints like ``capacity()``.
|
|
73
|
+
base_url:
|
|
74
|
+
Override the default orchestrator URL.
|
|
75
|
+
timeout:
|
|
76
|
+
Request timeout in seconds (default 60).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
api_key: Optional[str] = None,
|
|
82
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
83
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
84
|
+
) -> None:
|
|
85
|
+
self._api_key = api_key
|
|
86
|
+
self._base_url = base_url.rstrip("/")
|
|
87
|
+
headers: Dict[str, str] = {"Accept": "application/json"}
|
|
88
|
+
if api_key:
|
|
89
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
90
|
+
self._client = httpx.Client(
|
|
91
|
+
base_url=self._base_url,
|
|
92
|
+
headers=headers,
|
|
93
|
+
timeout=timeout,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# -- helpers ------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def _request(
|
|
99
|
+
self,
|
|
100
|
+
method: str,
|
|
101
|
+
path: str,
|
|
102
|
+
*,
|
|
103
|
+
json: Optional[Dict[str, Any]] = None,
|
|
104
|
+
params: Optional[Dict[str, Any]] = None,
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
"""Make an HTTP request and return the parsed JSON body."""
|
|
107
|
+
try:
|
|
108
|
+
response = self._client.request(
|
|
109
|
+
method, path, json=json, params=params
|
|
110
|
+
)
|
|
111
|
+
except httpx.ConnectError as exc:
|
|
112
|
+
raise SGLConnectionError(
|
|
113
|
+
f"Could not connect to {self._base_url}: {exc}"
|
|
114
|
+
) from exc
|
|
115
|
+
except httpx.TimeoutException as exc:
|
|
116
|
+
raise SGLConnectionError(
|
|
117
|
+
f"Request to {self._base_url}{path} timed out: {exc}"
|
|
118
|
+
) from exc
|
|
119
|
+
|
|
120
|
+
if response.status_code >= 400:
|
|
121
|
+
body: Optional[Dict[str, Any]] = None
|
|
122
|
+
message = response.text
|
|
123
|
+
try:
|
|
124
|
+
body = response.json()
|
|
125
|
+
message = body.get("error", {}).get("message", message) if isinstance(body.get("error"), dict) else body.get("error", message)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
if response.status_code in (401, 403):
|
|
130
|
+
raise SGLAuthError(response.status_code, str(message), body)
|
|
131
|
+
if response.status_code == 404:
|
|
132
|
+
raise SGLNotFoundError(response.status_code, str(message), body)
|
|
133
|
+
raise SGLAPIError(response.status_code, str(message), body)
|
|
134
|
+
|
|
135
|
+
if response.status_code == 204:
|
|
136
|
+
return {}
|
|
137
|
+
return response.json() # type: ignore[no-any-return]
|
|
138
|
+
|
|
139
|
+
# -- public endpoints (no auth required) --------------------------------
|
|
140
|
+
|
|
141
|
+
def capacity(self) -> CapacityResponse:
|
|
142
|
+
"""Return grid-wide capacity summary."""
|
|
143
|
+
data = self._request("GET", "/grid/capacity")
|
|
144
|
+
return CapacityResponse.model_validate(data)
|
|
145
|
+
|
|
146
|
+
def models(self) -> List[ModelInfo]:
|
|
147
|
+
"""Return available models with pricing and node info."""
|
|
148
|
+
data = self._request("GET", "/grid/models")
|
|
149
|
+
wrapped = ModelsResponse.model_validate(data)
|
|
150
|
+
return wrapped.models
|
|
151
|
+
|
|
152
|
+
def pricing(self) -> List[PricingInfo]:
|
|
153
|
+
"""Return the pricing table for all models."""
|
|
154
|
+
data = self._request("GET", "/grid/pricing")
|
|
155
|
+
wrapped = PricingResponse.model_validate(data)
|
|
156
|
+
return wrapped.pricing
|
|
157
|
+
|
|
158
|
+
# -- authenticated endpoints --------------------------------------------
|
|
159
|
+
|
|
160
|
+
def submit_job(
|
|
161
|
+
self,
|
|
162
|
+
model: str,
|
|
163
|
+
input_payload: Dict[str, Any],
|
|
164
|
+
*,
|
|
165
|
+
submitter_wallet: Optional[str] = None,
|
|
166
|
+
submitter_chain: Optional[str] = None,
|
|
167
|
+
) -> JobResponse:
|
|
168
|
+
"""Submit a compute job to the grid.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
model:
|
|
173
|
+
Model identifier (e.g. ``"gemma-4-26b"``).
|
|
174
|
+
input_payload:
|
|
175
|
+
The request body for the model (e.g. chat messages).
|
|
176
|
+
submitter_wallet:
|
|
177
|
+
On-chain wallet address of the submitter.
|
|
178
|
+
submitter_chain:
|
|
179
|
+
Chain identifier (e.g. ``"base"``, ``"ethereum"``).
|
|
180
|
+
"""
|
|
181
|
+
body: Dict[str, Any] = {
|
|
182
|
+
"model": model,
|
|
183
|
+
"input": input_payload,
|
|
184
|
+
}
|
|
185
|
+
if submitter_wallet is not None:
|
|
186
|
+
body["submitter_wallet"] = submitter_wallet
|
|
187
|
+
if submitter_chain is not None:
|
|
188
|
+
body["submitter_chain"] = submitter_chain
|
|
189
|
+
|
|
190
|
+
data = self._request("POST", "/grid/jobs", json=body)
|
|
191
|
+
return JobResponse.model_validate(data)
|
|
192
|
+
|
|
193
|
+
def get_job(self, job_id: str) -> JobResult:
|
|
194
|
+
"""Get the status and result of a previously submitted job."""
|
|
195
|
+
data = self._request("GET", f"/grid/jobs/{job_id}")
|
|
196
|
+
return JobResult.model_validate(data)
|
|
197
|
+
|
|
198
|
+
def get_attestation(self, job_id: str) -> AttestationProof:
|
|
199
|
+
"""Retrieve the TEE attestation proof for a completed job."""
|
|
200
|
+
data = self._request("GET", f"/grid/jobs/{job_id}/attestation")
|
|
201
|
+
return AttestationProof.model_validate(data)
|
|
202
|
+
|
|
203
|
+
# -- lifecycle ----------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def close(self) -> None:
|
|
206
|
+
"""Close the underlying HTTP client."""
|
|
207
|
+
self._client.close()
|
|
208
|
+
|
|
209
|
+
def __enter__(self) -> "GridClient":
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
def __exit__(self, *args: Any) -> None:
|
|
213
|
+
self.close()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Pydantic models for SGL Network API responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# Capacity
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
class TeeCapacity(BaseModel):
|
|
15
|
+
"""Capacity breakdown for a single TEE type."""
|
|
16
|
+
tee_type: str
|
|
17
|
+
total_nodes: int = 0
|
|
18
|
+
active_nodes: int = 0
|
|
19
|
+
available_nodes: int = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CapacityResponse(BaseModel):
|
|
23
|
+
"""Grid-wide capacity summary returned by GET /grid/capacity."""
|
|
24
|
+
total_nodes: int = 0
|
|
25
|
+
active_nodes: int = 0
|
|
26
|
+
available_nodes: int = 0
|
|
27
|
+
by_tee_type: List[TeeCapacity] = Field(default_factory=list)
|
|
28
|
+
updated_at: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Models
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
class ModelPricing(BaseModel):
|
|
36
|
+
"""Pricing for a specific model."""
|
|
37
|
+
price_per_1k_input_tokens_usd: float = 0.0
|
|
38
|
+
price_per_1k_output_tokens_usd: float = 0.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ModelInfo(BaseModel):
|
|
42
|
+
"""Model descriptor returned by GET /grid/models."""
|
|
43
|
+
id: str
|
|
44
|
+
owned_by: str = ""
|
|
45
|
+
sgl_node_count: int = 0
|
|
46
|
+
sgl_tee_types: List[str] = Field(default_factory=list)
|
|
47
|
+
sgl_pricing: Optional[ModelPricing] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ModelsResponse(BaseModel):
|
|
51
|
+
"""Wrapper for the models list."""
|
|
52
|
+
models: List[ModelInfo] = Field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Pricing
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
class PricingInfo(BaseModel):
|
|
60
|
+
"""Pricing entry for a single model."""
|
|
61
|
+
model: str
|
|
62
|
+
price_per_1k_input_tokens_usd: float = 0.0
|
|
63
|
+
price_per_1k_output_tokens_usd: float = 0.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PricingResponse(BaseModel):
|
|
67
|
+
"""Full pricing table returned by GET /grid/pricing."""
|
|
68
|
+
pricing: List[PricingInfo] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Jobs
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
class JobResponse(BaseModel):
|
|
76
|
+
"""Response from POST /grid/jobs (job submission)."""
|
|
77
|
+
job_id: str
|
|
78
|
+
status: str = "pending"
|
|
79
|
+
model: str = ""
|
|
80
|
+
node_id: Optional[str] = None
|
|
81
|
+
tee_type: Optional[str] = None
|
|
82
|
+
estimated_cost_usd: Optional[float] = None
|
|
83
|
+
created_at: Optional[str] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AttestationProof(BaseModel):
|
|
87
|
+
"""TEE attestation proof for a completed job."""
|
|
88
|
+
node_id: str = ""
|
|
89
|
+
tee_type: str = ""
|
|
90
|
+
job_id: str = ""
|
|
91
|
+
attestation_signature: str = ""
|
|
92
|
+
attestation_report: Optional[str] = None
|
|
93
|
+
verified: bool = False
|
|
94
|
+
verified_at: Optional[str] = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class JobResult(BaseModel):
|
|
98
|
+
"""Full job result returned by GET /grid/jobs/:id."""
|
|
99
|
+
id: str
|
|
100
|
+
status: str = "pending"
|
|
101
|
+
model: str = ""
|
|
102
|
+
node_id: Optional[str] = None
|
|
103
|
+
tee_type: Optional[str] = None
|
|
104
|
+
result: Optional[Dict[str, Any]] = None
|
|
105
|
+
encrypted_result: Optional[str] = None
|
|
106
|
+
attestation_proof: Optional[AttestationProof] = None
|
|
107
|
+
cost_usd: Optional[float] = None
|
|
108
|
+
created_at: Optional[str] = None
|
|
109
|
+
completed_at: Optional[str] = None
|
|
110
|
+
error: Optional[str] = None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Helper to configure the OpenAI SDK for use with the SGL Network."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .client import DEFAULT_BASE_URL
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_openai_client(
|
|
11
|
+
api_key: str = "sgl-anonymous",
|
|
12
|
+
base_url: Optional[str] = None,
|
|
13
|
+
**kwargs,
|
|
14
|
+
):
|
|
15
|
+
"""Return an ``openai.OpenAI`` instance pointed at the SGL Network.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
api_key:
|
|
20
|
+
API key for the orchestrator (e.g. ``"scg_..."``).
|
|
21
|
+
Defaults to ``"sgl-anonymous"`` for unauthenticated access.
|
|
22
|
+
base_url:
|
|
23
|
+
Override the default orchestrator URL. The ``/v1`` suffix is
|
|
24
|
+
appended automatically if not already present.
|
|
25
|
+
**kwargs:
|
|
26
|
+
Additional keyword arguments forwarded to ``openai.OpenAI()``.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
openai.OpenAI
|
|
31
|
+
A configured OpenAI client.
|
|
32
|
+
|
|
33
|
+
Raises
|
|
34
|
+
------
|
|
35
|
+
ImportError
|
|
36
|
+
If the ``openai`` package is not installed. Install it with
|
|
37
|
+
``pip install singularity-grid[openai]``.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
from openai import OpenAI # type: ignore[import-untyped]
|
|
41
|
+
except ImportError:
|
|
42
|
+
raise ImportError(
|
|
43
|
+
"The openai package is required for create_openai_client(). "
|
|
44
|
+
"Install it with: pip install singularity-grid[openai]"
|
|
45
|
+
) from None
|
|
46
|
+
|
|
47
|
+
url = base_url or DEFAULT_BASE_URL
|
|
48
|
+
if not url.endswith("/v1"):
|
|
49
|
+
url = url.rstrip("/") + "/v1"
|
|
50
|
+
|
|
51
|
+
return OpenAI(api_key=api_key, base_url=url, **kwargs)
|
|
File without changes
|