singularity-grid 0.9.1__tar.gz → 0.9.2__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.9.1 → singularity_grid-0.9.2}/PKG-INFO +37 -1
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/README.md +36 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/pyproject.toml +1 -1
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/__init__.py +1 -1
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/client.py +78 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/e2e.py +77 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/.github/workflows/publish.yml +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/.gitignore +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/LICENSE +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/examples/check_capacity.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/examples/openai_compatible.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/examples/quickstart.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/models.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/openai_compat.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/pods.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/processors.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/py.typed +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/singularity_grid/vault.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/tests/test_pod_webhook.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/tests/test_processors.py +0 -0
- {singularity_grid-0.9.1 → singularity_grid-0.9.2}/tests/test_vault.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: singularity-grid
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Python SDK for the SGL Network confidential compute grid (end-to-end encrypted + streaming)
|
|
5
5
|
Project-URL: Homepage, https://singularitylayer.xyz
|
|
6
6
|
Project-URL: Repository, https://github.com/Singularity-Layer/sgl-network-sdk
|
|
@@ -119,6 +119,39 @@ attestation = grid.get_attestation(job.job_id)
|
|
|
119
119
|
print(f"TEE verified: {attestation.verified}")
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### 4. System One / Laya
|
|
123
|
+
|
|
124
|
+
Laya is served as a typed-decision model, not as chat completions.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from singularity_grid import GridClient
|
|
128
|
+
|
|
129
|
+
grid = GridClient(api_key="x402c_your_api_key")
|
|
130
|
+
|
|
131
|
+
systemone_models = grid.systemone_models()
|
|
132
|
+
print([model["id"] for model in systemone_models])
|
|
133
|
+
|
|
134
|
+
decision = grid.system_one(
|
|
135
|
+
model="convaiinnovations/laya",
|
|
136
|
+
state={"ticket": "Enterprise customer cannot access billing exports"},
|
|
137
|
+
questions={
|
|
138
|
+
"route": {
|
|
139
|
+
"type": "choice",
|
|
140
|
+
"instructions": "Choose the best team.",
|
|
141
|
+
"criteria": {
|
|
142
|
+
"billing": "Billing, invoice, refund, or account credit issue.",
|
|
143
|
+
"support": "Product defect or technical troubleshooting.",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
"urgency": {
|
|
147
|
+
"type": "score",
|
|
148
|
+
"instructions": "Score urgency from 0 to 1.",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
print(decision["answers"])
|
|
153
|
+
```
|
|
154
|
+
|
|
122
155
|
## API reference
|
|
123
156
|
|
|
124
157
|
### GridClient
|
|
@@ -127,7 +160,10 @@ print(f"TEE verified: {attestation.verified}")
|
|
|
127
160
|
|---|---|---|
|
|
128
161
|
| `capacity()` | No | Grid-wide capacity summary |
|
|
129
162
|
| `models()` | No | Available models with pricing and TEE info |
|
|
163
|
+
| `v1_models(type=None)` | No | OpenAI-compatible model list; pass `type="systemone"` for Laya |
|
|
164
|
+
| `systemone_models()` | No | List Laya/System One typed-decision models |
|
|
130
165
|
| `pricing()` | No | Pricing table for all models |
|
|
166
|
+
| `system_one(state, questions, ...)` | Yes | Call `/v1/systemone` for typed decisions |
|
|
131
167
|
| `submit_job(model, input_payload, ...)` | Yes | Submit a compute job |
|
|
132
168
|
| `get_job(job_id)` | Yes | Get job status and result |
|
|
133
169
|
| `get_attestation(job_id)` | Yes | Get TEE attestation proof |
|
|
@@ -88,6 +88,39 @@ attestation = grid.get_attestation(job.job_id)
|
|
|
88
88
|
print(f"TEE verified: {attestation.verified}")
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
### 4. System One / Laya
|
|
92
|
+
|
|
93
|
+
Laya is served as a typed-decision model, not as chat completions.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from singularity_grid import GridClient
|
|
97
|
+
|
|
98
|
+
grid = GridClient(api_key="x402c_your_api_key")
|
|
99
|
+
|
|
100
|
+
systemone_models = grid.systemone_models()
|
|
101
|
+
print([model["id"] for model in systemone_models])
|
|
102
|
+
|
|
103
|
+
decision = grid.system_one(
|
|
104
|
+
model="convaiinnovations/laya",
|
|
105
|
+
state={"ticket": "Enterprise customer cannot access billing exports"},
|
|
106
|
+
questions={
|
|
107
|
+
"route": {
|
|
108
|
+
"type": "choice",
|
|
109
|
+
"instructions": "Choose the best team.",
|
|
110
|
+
"criteria": {
|
|
111
|
+
"billing": "Billing, invoice, refund, or account credit issue.",
|
|
112
|
+
"support": "Product defect or technical troubleshooting.",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"urgency": {
|
|
116
|
+
"type": "score",
|
|
117
|
+
"instructions": "Score urgency from 0 to 1.",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
print(decision["answers"])
|
|
122
|
+
```
|
|
123
|
+
|
|
91
124
|
## API reference
|
|
92
125
|
|
|
93
126
|
### GridClient
|
|
@@ -96,7 +129,10 @@ print(f"TEE verified: {attestation.verified}")
|
|
|
96
129
|
|---|---|---|
|
|
97
130
|
| `capacity()` | No | Grid-wide capacity summary |
|
|
98
131
|
| `models()` | No | Available models with pricing and TEE info |
|
|
132
|
+
| `v1_models(type=None)` | No | OpenAI-compatible model list; pass `type="systemone"` for Laya |
|
|
133
|
+
| `systemone_models()` | No | List Laya/System One typed-decision models |
|
|
99
134
|
| `pricing()` | No | Pricing table for all models |
|
|
135
|
+
| `system_one(state, questions, ...)` | Yes | Call `/v1/systemone` for typed decisions |
|
|
100
136
|
| `submit_job(model, input_payload, ...)` | Yes | Submit a compute job |
|
|
101
137
|
| `get_job(job_id)` | Yes | Get job status and result |
|
|
102
138
|
| `get_attestation(job_id)` | Yes | Get TEE attestation proof |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "singularity-grid"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
8
8
|
description = "Python SDK for the SGL Network confidential compute grid (end-to-end encrypted + streaming)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -170,6 +170,22 @@ class GridClient:
|
|
|
170
170
|
wrapped = PricingResponse.model_validate(data)
|
|
171
171
|
return wrapped.pricing
|
|
172
172
|
|
|
173
|
+
def v1_models(self, type: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
174
|
+
"""Return OpenAI-compatible ``/v1/models`` descriptors.
|
|
175
|
+
|
|
176
|
+
Pass ``type="systemone"`` to discover Laya/Jev-style typed-decision
|
|
177
|
+
models.
|
|
178
|
+
"""
|
|
179
|
+
params: Dict[str, Any] = {}
|
|
180
|
+
if type is not None:
|
|
181
|
+
params["type"] = type
|
|
182
|
+
data = self._request("GET", "/v1/models", params=params or None)
|
|
183
|
+
return list(data.get("data", []))
|
|
184
|
+
|
|
185
|
+
def systemone_models(self) -> List[Dict[str, Any]]:
|
|
186
|
+
"""List Laya/Jev-style System One typed-decision models."""
|
|
187
|
+
return self.v1_models(type="systemone")
|
|
188
|
+
|
|
173
189
|
# -- authenticated endpoints --------------------------------------------
|
|
174
190
|
|
|
175
191
|
def submit_job(
|
|
@@ -310,6 +326,9 @@ class GridClient:
|
|
|
310
326
|
sealed = data.get("sealed_result")
|
|
311
327
|
if not sealed:
|
|
312
328
|
raise SGLAPIError(500, "No sealed result returned")
|
|
329
|
+
# Prove WHO produced this before opening it. AEAD only proves someone
|
|
330
|
+
# sealed it to our key, and the orchestrator knows that key.
|
|
331
|
+
e2e.require_verified_reply(reservation, data)
|
|
313
332
|
plain = e2e.open_output(resp_sk, resp_pub, sealed["ephemeral_public_key"], sealed["ciphertext"])
|
|
314
333
|
parsed = _json.loads(plain)
|
|
315
334
|
return {
|
|
@@ -357,6 +376,44 @@ class GridClient:
|
|
|
357
376
|
raise SGLAPIError(402, "Payment required — pass api_key (credits); the Python GridClient does not sign x402 payments.") from err
|
|
358
377
|
raise
|
|
359
378
|
|
|
379
|
+
def system_one(
|
|
380
|
+
self,
|
|
381
|
+
state: Dict[str, Any],
|
|
382
|
+
questions: Dict[str, Dict[str, Any]],
|
|
383
|
+
*,
|
|
384
|
+
model: str = "convaiinnovations/laya",
|
|
385
|
+
node: Optional[str] = None,
|
|
386
|
+
cluster: Optional[str] = None,
|
|
387
|
+
max_price: Optional[float] = None,
|
|
388
|
+
) -> Dict[str, Any]:
|
|
389
|
+
"""Call Laya/System One typed decisions via ``POST /v1/systemone``.
|
|
390
|
+
|
|
391
|
+
This is not a chat-completions model. Use it for compact typed outputs
|
|
392
|
+
such as choices, scores, and no-output-language summaries. Requires
|
|
393
|
+
``api_key`` (credits); x402 pay-per-call is not signed by this client.
|
|
394
|
+
"""
|
|
395
|
+
body: Dict[str, Any] = {
|
|
396
|
+
"model": model,
|
|
397
|
+
"state": state,
|
|
398
|
+
"questions": questions,
|
|
399
|
+
}
|
|
400
|
+
if node is not None:
|
|
401
|
+
body["node"] = node
|
|
402
|
+
if cluster is not None:
|
|
403
|
+
body["cluster"] = cluster
|
|
404
|
+
if max_price is not None:
|
|
405
|
+
body["max_price"] = max_price
|
|
406
|
+
try:
|
|
407
|
+
return self._request("POST", "/v1/systemone", json=body)
|
|
408
|
+
except SGLAPIError as err:
|
|
409
|
+
if err.status_code == 402:
|
|
410
|
+
raise SGLAPIError(402, "Payment required — pass api_key (credits); the Python GridClient does not sign x402 payments.") from err
|
|
411
|
+
raise
|
|
412
|
+
|
|
413
|
+
def systemone(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
414
|
+
"""Alias for :meth:`system_one` matching the HTTP endpoint spelling."""
|
|
415
|
+
return self.system_one(*args, **kwargs)
|
|
416
|
+
|
|
360
417
|
def embed(self, model: str, input: Any, **kwargs: Any) -> List[List[float]]:
|
|
361
418
|
"""Convenience wrapper around :meth:`embeddings` that returns just the list of
|
|
362
419
|
vectors, ordered to match ``input``."""
|
|
@@ -417,6 +474,9 @@ class GridClient:
|
|
|
417
474
|
sealed = data.get("sealed_result")
|
|
418
475
|
if not sealed:
|
|
419
476
|
raise SGLAPIError(500, "No sealed result returned")
|
|
477
|
+
# Same check on the non-streaming fallback: an orchestrator that
|
|
478
|
+
# can force this path must not get an unverified reply through it.
|
|
479
|
+
e2e.require_verified_reply(reservation, data)
|
|
420
480
|
plain = e2e.open_output(resp_sk, resp_pub, sealed["ephemeral_public_key"], sealed["ciphertext"])
|
|
421
481
|
content = _json.loads(plain).get("content", "")
|
|
422
482
|
if content:
|
|
@@ -453,6 +513,24 @@ class GridClient:
|
|
|
453
513
|
raise SGLAPIError(502, "stream chunk 0 missing ephemeral key")
|
|
454
514
|
out_key = e2e.stream_out_key(resp_sk, stream_eph)
|
|
455
515
|
is_final = chunk.get("final") is True
|
|
516
|
+
# Verify EVERY chunk before opening it. The node signs each with
|
|
517
|
+
# kind "stream:{seq}:{final}" over the chunk ciphertext, and the
|
|
518
|
+
# orchestrator relays that signature rather than consuming it.
|
|
519
|
+
# Without this a compromised relay could splice or invent chunks,
|
|
520
|
+
# i.e. edit the answer as it arrives.
|
|
521
|
+
if chunk.get("sigv") and chunk["sigv"] != "v1":
|
|
522
|
+
raise e2e.UnverifiedReply(f"unknown chunk envelope version {chunk['sigv']!r}")
|
|
523
|
+
kind = f"stream:{seq}:{1 if is_final else 0}"
|
|
524
|
+
if not e2e.verify_result_envelope(
|
|
525
|
+
reservation.get("node_ed25519_pubkey"),
|
|
526
|
+
chunk.get("job"),
|
|
527
|
+
kind,
|
|
528
|
+
chunk["ct"],
|
|
529
|
+
chunk.get("sig"),
|
|
530
|
+
):
|
|
531
|
+
raise e2e.UnverifiedReply(
|
|
532
|
+
f"stream chunk {seq} is not signed by the reserved node — refusing it."
|
|
533
|
+
)
|
|
456
534
|
text = e2e.open_stream_chunk(out_key, resp_pub, stream_eph, nonce, seq, is_final, chunk["ct"]).decode()
|
|
457
535
|
if text:
|
|
458
536
|
yield text
|
|
@@ -80,6 +80,83 @@ def seal_input(node_pub_b58: str, resp_pub_b58: str, plaintext: bytes) -> tuple[
|
|
|
80
80
|
return _b58e(nonce + ct), eph_b58
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
class UnverifiedReply(Exception):
|
|
84
|
+
"""A sealed reply could not be proven to come from the reserved node."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def verify_result_envelope(
|
|
88
|
+
node_ed25519_b58: str | None,
|
|
89
|
+
job_id: str | None,
|
|
90
|
+
kind: str,
|
|
91
|
+
ciphertext: str,
|
|
92
|
+
signature_b58: str | None,
|
|
93
|
+
) -> bool:
|
|
94
|
+
"""Verify the node's v1 envelope signature over a sealed reply.
|
|
95
|
+
|
|
96
|
+
The signature covers the PUBLIC ciphertext, not the plaintext, so it can be
|
|
97
|
+
checked exactly -- no re-serialising, no key-ordering problem. Message:
|
|
98
|
+
``sgl-result-v1\\n{job_id}\\n{kind}\\n{sha256_hex(ciphertext)}``, and the
|
|
99
|
+
ciphertext is hashed as the literal UTF-8 string, not as decoded bytes.
|
|
100
|
+
"""
|
|
101
|
+
if not node_ed25519_b58 or not signature_b58 or not job_id:
|
|
102
|
+
return False
|
|
103
|
+
try:
|
|
104
|
+
from hashlib import sha256
|
|
105
|
+
|
|
106
|
+
from nacl.signing import VerifyKey
|
|
107
|
+
|
|
108
|
+
digest = sha256(ciphertext.encode()).hexdigest()
|
|
109
|
+
msg = f"sgl-result-v1\n{job_id}\n{kind}\n{digest}".encode()
|
|
110
|
+
VerifyKey(_b58d(node_ed25519_b58)).verify(msg, _b58d(signature_b58))
|
|
111
|
+
return True
|
|
112
|
+
except Exception:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def require_verified_reply(
|
|
117
|
+
reservation: dict,
|
|
118
|
+
data: dict,
|
|
119
|
+
kind: str = "sealed",
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Refuse a sealed reply we cannot attribute to the reserved node.
|
|
122
|
+
|
|
123
|
+
Why this is mandatory rather than best-effort: ``client_response_pubkey``
|
|
124
|
+
reaches the orchestrator in cleartext, so a compromised orchestrator cannot
|
|
125
|
+
READ a prompt but CAN fabricate a reply, seal it to that key, and have it
|
|
126
|
+
decrypt cleanly. AEAD proves only that someone sealed it to us; this proves
|
|
127
|
+
WHO. Without it the grid could put words in the model's mouth -- and for an
|
|
128
|
+
agentic caller, tool calls in its hands.
|
|
129
|
+
|
|
130
|
+
A MISSING signature fails exactly like an invalid one. Every sealed reply
|
|
131
|
+
carries one, so absence means something is wrong, and "warn and continue" is
|
|
132
|
+
what let this gap survive unnoticed in the first place.
|
|
133
|
+
"""
|
|
134
|
+
sealed = data.get("sealed_result") or {}
|
|
135
|
+
version = data.get("result_envelope_version")
|
|
136
|
+
if version is not None and version != "v1":
|
|
137
|
+
raise UnverifiedReply(f"unknown result envelope version {version!r}")
|
|
138
|
+
# `job_id` is explicit on current grids; older ones only embed it in `id`.
|
|
139
|
+
job_id = data.get("job_id")
|
|
140
|
+
if not job_id:
|
|
141
|
+
raw_id = data.get("id") or ""
|
|
142
|
+
job_id = raw_id[len("chatcmpl-") :] if raw_id.startswith("chatcmpl-") else None
|
|
143
|
+
ok = verify_result_envelope(
|
|
144
|
+
reservation.get("node_ed25519_pubkey"),
|
|
145
|
+
job_id,
|
|
146
|
+
kind,
|
|
147
|
+
sealed.get("ciphertext", ""),
|
|
148
|
+
data.get("result_envelope_signature"),
|
|
149
|
+
)
|
|
150
|
+
if not ok:
|
|
151
|
+
# NEVER fall back to `result_signature`: that legacy field signs the
|
|
152
|
+
# PLAINTEXT and is always absent for sealed replies, so reaching for it
|
|
153
|
+
# would make this check silently meaningless.
|
|
154
|
+
raise UnverifiedReply(
|
|
155
|
+
"sealed reply is not signed by the reserved node — refusing it. "
|
|
156
|
+
"Update the grid if this persists."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
83
160
|
def open_output(resp_sk: bytes, resp_pub_b58: str, node_eph_b58: str, ct_b58: str) -> bytes:
|
|
84
161
|
"""Open the node's (non-stream) reply sealed to our response key."""
|
|
85
162
|
shared = bindings.crypto_scalarmult(resp_sk, _b58d(node_eph_b58))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|