cyberian-client 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.
- cyberian_client-0.1.0/PKG-INFO +179 -0
- cyberian_client-0.1.0/README.md +151 -0
- cyberian_client-0.1.0/pyproject.toml +43 -0
- cyberian_client-0.1.0/setup.cfg +4 -0
- cyberian_client-0.1.0/src/cyberian/__init__.py +30 -0
- cyberian_client-0.1.0/src/cyberian/client.py +348 -0
- cyberian_client-0.1.0/src/cyberian/exceptions.py +91 -0
- cyberian_client-0.1.0/src/cyberian_client.egg-info/PKG-INFO +179 -0
- cyberian_client-0.1.0/src/cyberian_client.egg-info/SOURCES.txt +11 -0
- cyberian_client-0.1.0/src/cyberian_client.egg-info/dependency_links.txt +1 -0
- cyberian_client-0.1.0/src/cyberian_client.egg-info/requires.txt +6 -0
- cyberian_client-0.1.0/src/cyberian_client.egg-info/top_level.txt +1 -0
- cyberian_client-0.1.0/tests/test_client.py +197 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyberian-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Cyberian Systems verified-inference API
|
|
5
|
+
Author-email: Cyberian Systems <philippe@cyberiansystems.ai>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://cyberiansystems.ai
|
|
8
|
+
Project-URL: Documentation, https://cyberiansystems.ai/docs
|
|
9
|
+
Project-URL: Support, https://cyberiansystems.ai/docs
|
|
10
|
+
Keywords: ai,ml,verified-inference,merkle,embeddings,cyberian
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Other/Proprietary 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: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: httpx<1.0,>=0.27.0
|
|
24
|
+
Requires-Dist: numpy<3.0,>=1.26.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# cyberian-client
|
|
30
|
+
|
|
31
|
+
Python client for the [Cyberian Systems](https://cyberiansystems.ai) verified-inference API.
|
|
32
|
+
|
|
33
|
+
Each call returns embeddings + a cryptographic receipt that proves an independent prover
|
|
34
|
+
re-executed a sample of your batch and got bitwise-identical results. The receipt is
|
|
35
|
+
self-verifying: any third party can validate it without access to the platform.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install cyberian-client
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.9+. Pulls in `httpx` and `numpy`.
|
|
44
|
+
|
|
45
|
+
## Get an API key
|
|
46
|
+
|
|
47
|
+
[https://cyberiansystems.ai/signup](https://cyberiansystems.ai/signup) — 14-day free
|
|
48
|
+
trial, no card required. The plaintext key is shown once at signup; save it. We store
|
|
49
|
+
only its SHA-256.
|
|
50
|
+
|
|
51
|
+
## Quickstart — one shot
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from cyberian import Client
|
|
55
|
+
|
|
56
|
+
client = Client(api_key="cyb_trial_…") # or set CYBERIAN_API_KEY in env and pass it in
|
|
57
|
+
|
|
58
|
+
result = client.submit_and_wait(
|
|
59
|
+
spec_yaml=open("spec.yaml").read(),
|
|
60
|
+
input_texts=[
|
|
61
|
+
"The property title search revealed no encumbrances.",
|
|
62
|
+
"Quarterly compliance attestation per SOX 404.",
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
print("receipt_hash:", result.receipt["receipt_hash"])
|
|
67
|
+
print("verified: ", result.verification["valid"]) # True
|
|
68
|
+
print("shape: ", result.embeddings.shape) # (2, 384) for BGE-small
|
|
69
|
+
print("first vec: ", result.embeddings[0, :8])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Step-by-step — when you need finer control
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
job = client.submit_job(spec_yaml="spec.yaml", input_texts=["a", "b", "c"])
|
|
76
|
+
|
|
77
|
+
# Poll yourself, or use wait_for_completion:
|
|
78
|
+
final = client.wait_for_completion(job["id"], poll_interval_sec=2.0, timeout_sec=600)
|
|
79
|
+
|
|
80
|
+
receipt = client.get_receipt(final["receipt_id"])
|
|
81
|
+
verification = client.verify_receipt(final["receipt_id"])
|
|
82
|
+
embeddings = client.get_job_output(final["id"]) # numpy ndarray, float32
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Verification as a Service (VaaS)
|
|
86
|
+
|
|
87
|
+
You ran inference on your own infrastructure. Cyberian re-runs it and certifies the
|
|
88
|
+
SHA-256 commitment of your output matches. Useful for compliance — you keep the
|
|
89
|
+
inference, we provide independent auditable verification.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import hashlib
|
|
93
|
+
|
|
94
|
+
# Your in-house inference output:
|
|
95
|
+
my_embeddings_bytes = my_pipeline.run(["text"]).astype("float32").tobytes()
|
|
96
|
+
my_commitment = hashlib.sha256(my_embeddings_bytes).hexdigest()
|
|
97
|
+
|
|
98
|
+
receipt = client.verify_outputs(
|
|
99
|
+
spec_yaml="spec.yaml",
|
|
100
|
+
input_texts=["text"],
|
|
101
|
+
claimed_output_commitment=my_commitment,
|
|
102
|
+
)
|
|
103
|
+
# Raises VerificationFailedError if our prover's re-execution doesn't match yours.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Phase 2: VaaS is single-chunk only — `len(input_texts) <= spec.chunking.chunk_size`.
|
|
107
|
+
|
|
108
|
+
## Account management
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
info = client.get_account()
|
|
112
|
+
print(info["tier"], info["effective_tier"], info["subscription_status"])
|
|
113
|
+
print(info["chunks_consumed_period"], "/", info["limits"]["chunks_per_period"])
|
|
114
|
+
|
|
115
|
+
# Mint an additional key (old one stays valid until you revoke it)
|
|
116
|
+
new_key = client.rotate_key()
|
|
117
|
+
|
|
118
|
+
# Revoke a specific key by its 12-char key_id (the prefix shown in /account)
|
|
119
|
+
client.revoke_key(key_id="a1b2c3d4e5f6")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Error handling
|
|
123
|
+
|
|
124
|
+
Typed exceptions; catch the most specific one you care about.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from cyberian import (
|
|
128
|
+
Client,
|
|
129
|
+
AuthError, # 401, 403
|
|
130
|
+
TrialExpiredError, # 402 — email upgrade@cyberiansystems.ai
|
|
131
|
+
QuotaError, # 402 / 413
|
|
132
|
+
RateLimitError, # 429 — has .retry_after_sec
|
|
133
|
+
ServiceBusyError, # 503 — has .retry_after_sec
|
|
134
|
+
JobFailedError, # job ended in FAILED state during wait_for_completion
|
|
135
|
+
VerificationFailedError, # /verify returned valid=False
|
|
136
|
+
ApiError, # any other 4xx/5xx
|
|
137
|
+
CyberianError, # base of everything above
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
result = client.submit_and_wait(spec_yaml=…, input_texts=[…])
|
|
142
|
+
except RateLimitError as exc:
|
|
143
|
+
time.sleep(exc.retry_after_sec)
|
|
144
|
+
result = client.submit_and_wait(spec_yaml=…, input_texts=[…])
|
|
145
|
+
except TrialExpiredError:
|
|
146
|
+
print("Email upgrade@cyberiansystems.ai for an extension or enterprise tier.")
|
|
147
|
+
raise
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Trial limits (Phase 2)
|
|
151
|
+
|
|
152
|
+
| | trial (default) | free (post-trial) |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| Period | 14 days | 30 days |
|
|
155
|
+
| Chunks per period | 400 | 100 |
|
|
156
|
+
| Chunks per day | 100 | 25 |
|
|
157
|
+
| Requests per minute | 60 | 10 |
|
|
158
|
+
| Max chunks per request | 100 | 50 |
|
|
159
|
+
| Max input texts | 1000 | 500 |
|
|
160
|
+
|
|
161
|
+
A "chunk" is one ONNX inference unit, sized by your CES `chunking.chunk_size`. With
|
|
162
|
+
`chunk_size: 1` each input text is its own chunk; with `chunk_size: 100` a 1000-text
|
|
163
|
+
batch is 10 chunks. Counting chunks (not requests) means the quota tracks actual
|
|
164
|
+
compute consumption.
|
|
165
|
+
|
|
166
|
+
## Documentation
|
|
167
|
+
|
|
168
|
+
- API + SDK reference: <https://cyberiansystems.ai/docs>
|
|
169
|
+
- Trial terms: <https://cyberiansystems.ai/terms>
|
|
170
|
+
- Privacy: <https://cyberiansystems.ai/privacy>
|
|
171
|
+
- Issues / support: support@cyberiansystems.ai
|
|
172
|
+
- Upgrade / enterprise inquiries: upgrade@cyberiansystems.ai
|
|
173
|
+
|
|
174
|
+
## Patents
|
|
175
|
+
|
|
176
|
+
The verification mechanism (Canonical Execution Specification, Merkle receipts,
|
|
177
|
+
sample-and-replay verification with game-theoretic deterrence, the four-tuple binding)
|
|
178
|
+
is covered by a U.S. provisional patent. Use of this client does not grant any patent
|
|
179
|
+
license beyond what's needed to call the API as documented.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# cyberian-client
|
|
2
|
+
|
|
3
|
+
Python client for the [Cyberian Systems](https://cyberiansystems.ai) verified-inference API.
|
|
4
|
+
|
|
5
|
+
Each call returns embeddings + a cryptographic receipt that proves an independent prover
|
|
6
|
+
re-executed a sample of your batch and got bitwise-identical results. The receipt is
|
|
7
|
+
self-verifying: any third party can validate it without access to the platform.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install cyberian-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.9+. Pulls in `httpx` and `numpy`.
|
|
16
|
+
|
|
17
|
+
## Get an API key
|
|
18
|
+
|
|
19
|
+
[https://cyberiansystems.ai/signup](https://cyberiansystems.ai/signup) — 14-day free
|
|
20
|
+
trial, no card required. The plaintext key is shown once at signup; save it. We store
|
|
21
|
+
only its SHA-256.
|
|
22
|
+
|
|
23
|
+
## Quickstart — one shot
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from cyberian import Client
|
|
27
|
+
|
|
28
|
+
client = Client(api_key="cyb_trial_…") # or set CYBERIAN_API_KEY in env and pass it in
|
|
29
|
+
|
|
30
|
+
result = client.submit_and_wait(
|
|
31
|
+
spec_yaml=open("spec.yaml").read(),
|
|
32
|
+
input_texts=[
|
|
33
|
+
"The property title search revealed no encumbrances.",
|
|
34
|
+
"Quarterly compliance attestation per SOX 404.",
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
print("receipt_hash:", result.receipt["receipt_hash"])
|
|
39
|
+
print("verified: ", result.verification["valid"]) # True
|
|
40
|
+
print("shape: ", result.embeddings.shape) # (2, 384) for BGE-small
|
|
41
|
+
print("first vec: ", result.embeddings[0, :8])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Step-by-step — when you need finer control
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
job = client.submit_job(spec_yaml="spec.yaml", input_texts=["a", "b", "c"])
|
|
48
|
+
|
|
49
|
+
# Poll yourself, or use wait_for_completion:
|
|
50
|
+
final = client.wait_for_completion(job["id"], poll_interval_sec=2.0, timeout_sec=600)
|
|
51
|
+
|
|
52
|
+
receipt = client.get_receipt(final["receipt_id"])
|
|
53
|
+
verification = client.verify_receipt(final["receipt_id"])
|
|
54
|
+
embeddings = client.get_job_output(final["id"]) # numpy ndarray, float32
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Verification as a Service (VaaS)
|
|
58
|
+
|
|
59
|
+
You ran inference on your own infrastructure. Cyberian re-runs it and certifies the
|
|
60
|
+
SHA-256 commitment of your output matches. Useful for compliance — you keep the
|
|
61
|
+
inference, we provide independent auditable verification.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import hashlib
|
|
65
|
+
|
|
66
|
+
# Your in-house inference output:
|
|
67
|
+
my_embeddings_bytes = my_pipeline.run(["text"]).astype("float32").tobytes()
|
|
68
|
+
my_commitment = hashlib.sha256(my_embeddings_bytes).hexdigest()
|
|
69
|
+
|
|
70
|
+
receipt = client.verify_outputs(
|
|
71
|
+
spec_yaml="spec.yaml",
|
|
72
|
+
input_texts=["text"],
|
|
73
|
+
claimed_output_commitment=my_commitment,
|
|
74
|
+
)
|
|
75
|
+
# Raises VerificationFailedError if our prover's re-execution doesn't match yours.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Phase 2: VaaS is single-chunk only — `len(input_texts) <= spec.chunking.chunk_size`.
|
|
79
|
+
|
|
80
|
+
## Account management
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
info = client.get_account()
|
|
84
|
+
print(info["tier"], info["effective_tier"], info["subscription_status"])
|
|
85
|
+
print(info["chunks_consumed_period"], "/", info["limits"]["chunks_per_period"])
|
|
86
|
+
|
|
87
|
+
# Mint an additional key (old one stays valid until you revoke it)
|
|
88
|
+
new_key = client.rotate_key()
|
|
89
|
+
|
|
90
|
+
# Revoke a specific key by its 12-char key_id (the prefix shown in /account)
|
|
91
|
+
client.revoke_key(key_id="a1b2c3d4e5f6")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Error handling
|
|
95
|
+
|
|
96
|
+
Typed exceptions; catch the most specific one you care about.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from cyberian import (
|
|
100
|
+
Client,
|
|
101
|
+
AuthError, # 401, 403
|
|
102
|
+
TrialExpiredError, # 402 — email upgrade@cyberiansystems.ai
|
|
103
|
+
QuotaError, # 402 / 413
|
|
104
|
+
RateLimitError, # 429 — has .retry_after_sec
|
|
105
|
+
ServiceBusyError, # 503 — has .retry_after_sec
|
|
106
|
+
JobFailedError, # job ended in FAILED state during wait_for_completion
|
|
107
|
+
VerificationFailedError, # /verify returned valid=False
|
|
108
|
+
ApiError, # any other 4xx/5xx
|
|
109
|
+
CyberianError, # base of everything above
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
result = client.submit_and_wait(spec_yaml=…, input_texts=[…])
|
|
114
|
+
except RateLimitError as exc:
|
|
115
|
+
time.sleep(exc.retry_after_sec)
|
|
116
|
+
result = client.submit_and_wait(spec_yaml=…, input_texts=[…])
|
|
117
|
+
except TrialExpiredError:
|
|
118
|
+
print("Email upgrade@cyberiansystems.ai for an extension or enterprise tier.")
|
|
119
|
+
raise
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Trial limits (Phase 2)
|
|
123
|
+
|
|
124
|
+
| | trial (default) | free (post-trial) |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Period | 14 days | 30 days |
|
|
127
|
+
| Chunks per period | 400 | 100 |
|
|
128
|
+
| Chunks per day | 100 | 25 |
|
|
129
|
+
| Requests per minute | 60 | 10 |
|
|
130
|
+
| Max chunks per request | 100 | 50 |
|
|
131
|
+
| Max input texts | 1000 | 500 |
|
|
132
|
+
|
|
133
|
+
A "chunk" is one ONNX inference unit, sized by your CES `chunking.chunk_size`. With
|
|
134
|
+
`chunk_size: 1` each input text is its own chunk; with `chunk_size: 100` a 1000-text
|
|
135
|
+
batch is 10 chunks. Counting chunks (not requests) means the quota tracks actual
|
|
136
|
+
compute consumption.
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
- API + SDK reference: <https://cyberiansystems.ai/docs>
|
|
141
|
+
- Trial terms: <https://cyberiansystems.ai/terms>
|
|
142
|
+
- Privacy: <https://cyberiansystems.ai/privacy>
|
|
143
|
+
- Issues / support: support@cyberiansystems.ai
|
|
144
|
+
- Upgrade / enterprise inquiries: upgrade@cyberiansystems.ai
|
|
145
|
+
|
|
146
|
+
## Patents
|
|
147
|
+
|
|
148
|
+
The verification mechanism (Canonical Execution Specification, Merkle receipts,
|
|
149
|
+
sample-and-replay verification with game-theoretic deterrence, the four-tuple binding)
|
|
150
|
+
is covered by a U.S. provisional patent. Use of this client does not grant any patent
|
|
151
|
+
license beyond what's needed to call the API as documented.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cyberian-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for the Cyberian Systems verified-inference API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "Proprietary" }
|
|
8
|
+
authors = [{ name = "Cyberian Systems", email = "philippe@cyberiansystems.ai" }]
|
|
9
|
+
keywords = ["ai", "ml", "verified-inference", "merkle", "embeddings", "cyberian"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: Other/Proprietary License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"httpx>=0.27.0,<1.0",
|
|
24
|
+
"numpy>=1.26.0,<3.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.0",
|
|
30
|
+
"pytest-asyncio>=0.23",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://cyberiansystems.ai"
|
|
35
|
+
Documentation = "https://cyberiansystems.ai/docs"
|
|
36
|
+
Support = "https://cyberiansystems.ai/docs"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["setuptools>=69.0"]
|
|
40
|
+
build-backend = "setuptools.build_meta"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Cyberian Systems — Python client for the verified-inference API."""
|
|
2
|
+
|
|
3
|
+
from .client import Client, JobResult
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
ApiError,
|
|
6
|
+
AuthError,
|
|
7
|
+
CyberianError,
|
|
8
|
+
JobFailedError,
|
|
9
|
+
QuotaError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ServiceBusyError,
|
|
12
|
+
TrialExpiredError,
|
|
13
|
+
VerificationFailedError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Client",
|
|
20
|
+
"JobResult",
|
|
21
|
+
"CyberianError",
|
|
22
|
+
"AuthError",
|
|
23
|
+
"TrialExpiredError",
|
|
24
|
+
"QuotaError",
|
|
25
|
+
"RateLimitError",
|
|
26
|
+
"ServiceBusyError",
|
|
27
|
+
"JobFailedError",
|
|
28
|
+
"VerificationFailedError",
|
|
29
|
+
"ApiError",
|
|
30
|
+
]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cyberian Systems client — Phase 2 SDK.
|
|
3
|
+
|
|
4
|
+
The Bearer key is the credential. There's no separate login; the key
|
|
5
|
+
is granted by /auth/register (use the /signup page on the website) or
|
|
6
|
+
issued by an operator.
|
|
7
|
+
|
|
8
|
+
Quick start:
|
|
9
|
+
|
|
10
|
+
from cyberian import Client
|
|
11
|
+
|
|
12
|
+
c = Client(api_key="cyb_trial_…")
|
|
13
|
+
|
|
14
|
+
# One-shot: submit, wait, fetch receipt + outputs
|
|
15
|
+
result = c.submit_and_wait(
|
|
16
|
+
spec_yaml=open("spec.yaml").read(),
|
|
17
|
+
input_texts=["sentence one", "sentence two"],
|
|
18
|
+
)
|
|
19
|
+
print(result.receipt["receipt_hash"])
|
|
20
|
+
print(result.embeddings.shape) # (2, 384) for BGE-small
|
|
21
|
+
|
|
22
|
+
# Or step-by-step
|
|
23
|
+
job = c.submit_job(spec_yaml=…, input_texts=[…])
|
|
24
|
+
final = c.wait_for_completion(job["id"])
|
|
25
|
+
receipt = c.get_receipt(final["receipt_id"])
|
|
26
|
+
verification = c.verify_receipt(final["receipt_id"])
|
|
27
|
+
embeddings = c.get_job_output(final["id"]) # numpy ndarray
|
|
28
|
+
|
|
29
|
+
# VaaS — verify a commitment for inference you ran yourself
|
|
30
|
+
receipt = c.verify_outputs(
|
|
31
|
+
spec_yaml=…,
|
|
32
|
+
input_texts=[…],
|
|
33
|
+
claimed_output_commitment="sha256-hex-of-your-output",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Account info, key management
|
|
37
|
+
info = c.get_account()
|
|
38
|
+
new_key = c.rotate_key()
|
|
39
|
+
c.revoke_key(key_id="abc123def456")
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import time
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
import httpx
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
from .exceptions import (
|
|
52
|
+
ApiError,
|
|
53
|
+
AuthError,
|
|
54
|
+
CyberianError,
|
|
55
|
+
JobFailedError,
|
|
56
|
+
QuotaError,
|
|
57
|
+
RateLimitError,
|
|
58
|
+
ServiceBusyError,
|
|
59
|
+
TrialExpiredError,
|
|
60
|
+
VerificationFailedError,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
DEFAULT_API_URL = "https://api.cyberiansystems.ai"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class JobResult:
|
|
68
|
+
"""Returned by submit_and_wait. Bundles the three things customers
|
|
69
|
+
most often need at the end of a job: the final job state, the full
|
|
70
|
+
receipt, and the decoded output embeddings."""
|
|
71
|
+
|
|
72
|
+
job: dict[str, Any]
|
|
73
|
+
receipt: dict[str, Any]
|
|
74
|
+
embeddings: np.ndarray
|
|
75
|
+
verification: dict[str, Any] | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Client:
|
|
79
|
+
"""Synchronous Cyberian Systems API client."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
api_key: str,
|
|
84
|
+
*,
|
|
85
|
+
base_url: str = DEFAULT_API_URL,
|
|
86
|
+
timeout: float = 60.0,
|
|
87
|
+
http_client: httpx.Client | None = None,
|
|
88
|
+
):
|
|
89
|
+
if not api_key or not api_key.startswith("cyb_"):
|
|
90
|
+
raise AuthError(
|
|
91
|
+
"api_key must be a Cyberian Bearer key (starts with cyb_). "
|
|
92
|
+
"Mint one via the /signup page or via /auth/register."
|
|
93
|
+
)
|
|
94
|
+
self.base_url = base_url.rstrip("/")
|
|
95
|
+
self._owns_http = http_client is None
|
|
96
|
+
self._http = http_client or httpx.Client(
|
|
97
|
+
base_url=self.base_url,
|
|
98
|
+
timeout=timeout,
|
|
99
|
+
headers={
|
|
100
|
+
"Authorization": f"Bearer {api_key}",
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ─── core HTTP wrapper ────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def _request(
|
|
108
|
+
self,
|
|
109
|
+
method: str,
|
|
110
|
+
path: str,
|
|
111
|
+
*,
|
|
112
|
+
json: Any = None,
|
|
113
|
+
expect_json: bool = True,
|
|
114
|
+
) -> Any:
|
|
115
|
+
try:
|
|
116
|
+
resp = self._http.request(method, path, json=json)
|
|
117
|
+
except httpx.RequestError as exc:
|
|
118
|
+
raise ApiError(f"network error talking to {path}: {exc}") from exc
|
|
119
|
+
|
|
120
|
+
if resp.status_code < 400:
|
|
121
|
+
if not expect_json:
|
|
122
|
+
return resp
|
|
123
|
+
if resp.status_code == 204 or not resp.content:
|
|
124
|
+
return None
|
|
125
|
+
return resp.json()
|
|
126
|
+
|
|
127
|
+
# 4xx/5xx — translate to typed exceptions.
|
|
128
|
+
try:
|
|
129
|
+
body = resp.json()
|
|
130
|
+
except Exception:
|
|
131
|
+
body = {"raw": resp.text}
|
|
132
|
+
|
|
133
|
+
msg = body.get("message") or body.get("error") or f"HTTP {resp.status_code}"
|
|
134
|
+
retry_after = int(resp.headers.get("retry-after", 0) or 0) or body.get("retry_after_sec", 0)
|
|
135
|
+
|
|
136
|
+
if resp.status_code == 401 or resp.status_code == 403:
|
|
137
|
+
raise AuthError(msg, status_code=resp.status_code, body=body)
|
|
138
|
+
if resp.status_code == 402:
|
|
139
|
+
err = (body.get("error") or "").lower()
|
|
140
|
+
if "trial" in err:
|
|
141
|
+
raise TrialExpiredError(msg, status_code=402, body=body)
|
|
142
|
+
raise QuotaError(msg, status_code=402, body=body)
|
|
143
|
+
if resp.status_code == 413:
|
|
144
|
+
raise QuotaError(msg, status_code=413, body=body)
|
|
145
|
+
if resp.status_code == 429:
|
|
146
|
+
raise RateLimitError(msg, retry_after_sec=retry_after or 60, status_code=429, body=body)
|
|
147
|
+
if resp.status_code == 503:
|
|
148
|
+
raise ServiceBusyError(msg, retry_after_sec=retry_after or 30, status_code=503, body=body)
|
|
149
|
+
raise ApiError(msg, status_code=resp.status_code, body=body)
|
|
150
|
+
|
|
151
|
+
# ─── public surface ──────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def health(self) -> dict[str, Any]:
|
|
154
|
+
"""GET /health — connectivity probe; doesn't require auth."""
|
|
155
|
+
return self._request("GET", "/health")
|
|
156
|
+
|
|
157
|
+
def get_account(self) -> dict[str, Any]:
|
|
158
|
+
"""GET /account — tier, trial status, usage, key list."""
|
|
159
|
+
return self._request("GET", "/account")
|
|
160
|
+
|
|
161
|
+
def rotate_key(self) -> str:
|
|
162
|
+
"""POST /auth/keys/rotate — returns the NEW plaintext key. Old key
|
|
163
|
+
remains valid until you call revoke_key with its key_id."""
|
|
164
|
+
body = self._request("POST", "/auth/keys/rotate")
|
|
165
|
+
return body["api_key"]
|
|
166
|
+
|
|
167
|
+
def revoke_key(self, *, key_id: str) -> None:
|
|
168
|
+
"""POST /auth/keys/revoke — invalidate the key with the given 12-
|
|
169
|
+
char key_id prefix. Idempotent."""
|
|
170
|
+
self._request("POST", "/auth/keys/revoke", json={"key_id": key_id})
|
|
171
|
+
|
|
172
|
+
def submit_job(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
spec_yaml: str | Path,
|
|
176
|
+
input_texts: list[str],
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""POST /jobs — submit a verified inference batch."""
|
|
179
|
+
spec_text = _read_spec(spec_yaml)
|
|
180
|
+
return self._request(
|
|
181
|
+
"POST",
|
|
182
|
+
"/jobs",
|
|
183
|
+
json={"spec_yaml": spec_text, "input_texts": input_texts},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def get_job(self, job_id: str) -> dict[str, Any]:
|
|
187
|
+
"""GET /jobs/:id — current job status."""
|
|
188
|
+
return self._request("GET", f"/jobs/{job_id}")
|
|
189
|
+
|
|
190
|
+
def get_job_results(self, job_id: str) -> dict[str, Any]:
|
|
191
|
+
"""GET /jobs/:id/results — per-chunk progress (status, commitments)."""
|
|
192
|
+
return self._request("GET", f"/jobs/{job_id}/results")
|
|
193
|
+
|
|
194
|
+
def get_job_output(self, job_id: str) -> np.ndarray:
|
|
195
|
+
"""GET /jobs/:id/output — decoded float32 embeddings as a 2D ndarray.
|
|
196
|
+
|
|
197
|
+
Shape is (total_input_texts, output_dimensions). Only available
|
|
198
|
+
after the job reaches SETTLED.
|
|
199
|
+
"""
|
|
200
|
+
resp = self._request("GET", f"/jobs/{job_id}/output", expect_json=False)
|
|
201
|
+
shape_h = resp.headers.get("x-cyberian-shape", "")
|
|
202
|
+
try:
|
|
203
|
+
rows, cols = (int(p.strip()) for p in shape_h.split(","))
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
raise ApiError(f"unexpected x-cyberian-shape header: {shape_h!r}") from exc
|
|
206
|
+
return np.frombuffer(resp.content, dtype=np.float32).reshape(rows, cols)
|
|
207
|
+
|
|
208
|
+
def get_receipt(self, receipt_id: str) -> dict[str, Any]:
|
|
209
|
+
"""GET /receipts/:id — full Merkle receipt JSON."""
|
|
210
|
+
return self._request("GET", f"/receipts/{receipt_id}")
|
|
211
|
+
|
|
212
|
+
def verify_receipt(self, receipt_id: str) -> dict[str, Any]:
|
|
213
|
+
"""GET /receipts/:id/verify — coordinator re-verifies the receipt's
|
|
214
|
+
self-referential hash and the input/output Merkle roots from
|
|
215
|
+
stored chunk commitments. Returns
|
|
216
|
+
{valid, checks: {receipt_hash, input_root, output_root, proof_root}, errors}.
|
|
217
|
+
"""
|
|
218
|
+
return self._request("GET", f"/receipts/{receipt_id}/verify")
|
|
219
|
+
|
|
220
|
+
def wait_for_completion(
|
|
221
|
+
self,
|
|
222
|
+
job_id: str,
|
|
223
|
+
*,
|
|
224
|
+
poll_interval_sec: float = 2.0,
|
|
225
|
+
timeout_sec: float = 600.0,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Poll GET /jobs/:id until SETTLED, FAILED, or REFUNDED, or timeout.
|
|
228
|
+
|
|
229
|
+
Raises JobFailedError on FAILED. Returns the final job dict.
|
|
230
|
+
"""
|
|
231
|
+
deadline = time.monotonic() + timeout_sec
|
|
232
|
+
terminal = {"SETTLED", "FAILED", "REFUNDED"}
|
|
233
|
+
while True:
|
|
234
|
+
job = self.get_job(job_id)
|
|
235
|
+
if job["status"] in terminal:
|
|
236
|
+
if job["status"] == "FAILED":
|
|
237
|
+
raise JobFailedError(job_id, body=job)
|
|
238
|
+
return job
|
|
239
|
+
if time.monotonic() > deadline:
|
|
240
|
+
raise CyberianError(
|
|
241
|
+
f"Job {job_id} did not reach a terminal state in {timeout_sec}s "
|
|
242
|
+
f"(last status: {job['status']})"
|
|
243
|
+
)
|
|
244
|
+
time.sleep(poll_interval_sec)
|
|
245
|
+
|
|
246
|
+
def submit_and_wait(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
spec_yaml: str | Path,
|
|
250
|
+
input_texts: list[str],
|
|
251
|
+
poll_interval_sec: float = 2.0,
|
|
252
|
+
timeout_sec: float = 600.0,
|
|
253
|
+
verify_receipt: bool = True,
|
|
254
|
+
) -> JobResult:
|
|
255
|
+
"""One-shot helper: submit_job → wait_for_completion → get_receipt
|
|
256
|
+
→ get_job_output → optionally verify_receipt. Most customers will
|
|
257
|
+
only ever call this method.
|
|
258
|
+
"""
|
|
259
|
+
job = self.submit_job(spec_yaml=spec_yaml, input_texts=input_texts)
|
|
260
|
+
final = self.wait_for_completion(
|
|
261
|
+
job["id"],
|
|
262
|
+
poll_interval_sec=poll_interval_sec,
|
|
263
|
+
timeout_sec=timeout_sec,
|
|
264
|
+
)
|
|
265
|
+
receipt = self.get_receipt(final["receipt_id"])
|
|
266
|
+
embeddings = self.get_job_output(final["id"])
|
|
267
|
+
verification = self.verify_receipt(final["receipt_id"]) if verify_receipt else None
|
|
268
|
+
if verification and not verification.get("valid", False):
|
|
269
|
+
raise VerificationFailedError(
|
|
270
|
+
receipt_id=final["receipt_id"],
|
|
271
|
+
checks=verification.get("checks", {}),
|
|
272
|
+
errors=verification.get("errors", []),
|
|
273
|
+
)
|
|
274
|
+
return JobResult(
|
|
275
|
+
job=final,
|
|
276
|
+
receipt=receipt,
|
|
277
|
+
embeddings=embeddings,
|
|
278
|
+
verification=verification,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# ─── VaaS ────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
def verify_outputs(
|
|
284
|
+
self,
|
|
285
|
+
*,
|
|
286
|
+
spec_yaml: str | Path,
|
|
287
|
+
input_texts: list[str],
|
|
288
|
+
claimed_output_commitment: str,
|
|
289
|
+
) -> dict[str, Any]:
|
|
290
|
+
"""POST /verify — Verification as a Service. You ran inference on
|
|
291
|
+
your own infrastructure; we re-execute on ours and confirm your
|
|
292
|
+
SHA-256 commitment matches.
|
|
293
|
+
|
|
294
|
+
Phase 2 supports single-chunk only — input_texts.length must fit
|
|
295
|
+
in one chunk per the spec's chunk_size.
|
|
296
|
+
|
|
297
|
+
Returns the receipt on success (HTTP 200). Raises
|
|
298
|
+
VerificationFailedError on commitment mismatch (HTTP 422).
|
|
299
|
+
"""
|
|
300
|
+
spec_text = _read_spec(spec_yaml)
|
|
301
|
+
try:
|
|
302
|
+
return self._request(
|
|
303
|
+
"POST",
|
|
304
|
+
"/verify",
|
|
305
|
+
json={
|
|
306
|
+
"spec_yaml": spec_text,
|
|
307
|
+
"input_texts": input_texts,
|
|
308
|
+
"claimed_output_commitment": claimed_output_commitment,
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
except ApiError as exc:
|
|
312
|
+
if exc.status_code == 422:
|
|
313
|
+
raise VerificationFailedError(
|
|
314
|
+
receipt_id="(no receipt issued)",
|
|
315
|
+
checks={"output_match": False},
|
|
316
|
+
errors=[exc.body.get("reason") if isinstance(exc.body, dict) else str(exc.body)],
|
|
317
|
+
) from exc
|
|
318
|
+
raise
|
|
319
|
+
|
|
320
|
+
# ─── lifecycle ───────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
def close(self) -> None:
|
|
323
|
+
if self._owns_http:
|
|
324
|
+
self._http.close()
|
|
325
|
+
|
|
326
|
+
def __enter__(self) -> "Client":
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def __exit__(self, *_: object) -> None:
|
|
330
|
+
self.close()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _read_spec(spec: str | Path) -> str:
|
|
334
|
+
"""Accept either a YAML string or a path to a YAML file."""
|
|
335
|
+
if isinstance(spec, Path):
|
|
336
|
+
return spec.read_text()
|
|
337
|
+
if isinstance(spec, str):
|
|
338
|
+
# Heuristic: treat as a path if it looks file-y AND the file exists.
|
|
339
|
+
# Otherwise treat as inline YAML.
|
|
340
|
+
if "\n" not in spec and len(spec) < 4096:
|
|
341
|
+
try:
|
|
342
|
+
p = Path(spec)
|
|
343
|
+
if p.exists() and p.is_file():
|
|
344
|
+
return p.read_text()
|
|
345
|
+
except (OSError, ValueError):
|
|
346
|
+
pass
|
|
347
|
+
return spec
|
|
348
|
+
raise TypeError(f"spec_yaml must be str or Path, got {type(spec).__name__}")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception types raised by the Cyberian Systems client.
|
|
3
|
+
|
|
4
|
+
Catch hierarchy (most-to-least specific):
|
|
5
|
+
|
|
6
|
+
CyberianError base
|
|
7
|
+
├── AuthError 401, 403
|
|
8
|
+
├── TrialExpiredError 402 trial_expired / quota_exceeded
|
|
9
|
+
├── QuotaError 402 quota / 413 request_too_large
|
|
10
|
+
├── RateLimitError 429 (any flavor) — has .retry_after_sec
|
|
11
|
+
├── ServiceBusyError 503 — has .retry_after_sec
|
|
12
|
+
├── JobFailedError job reached FAILED status during wait
|
|
13
|
+
├── VerificationFailedError /verify returned valid=False
|
|
14
|
+
└── ApiError any other 4xx/5xx
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CyberianError(Exception):
|
|
22
|
+
"""Base class for all Cyberian client errors."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str, *, status_code: int | None = None, body: Any = None):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.body = body
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthError(CyberianError):
|
|
31
|
+
"""The Bearer key was missing, malformed, invalid, or revoked."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TrialExpiredError(CyberianError):
|
|
35
|
+
"""The account's trial has ended without an upgrade. Email upgrade@cyberiansystems.ai."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class QuotaError(CyberianError):
|
|
39
|
+
"""A period or per-request quota was exceeded."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RateLimitError(CyberianError):
|
|
43
|
+
"""The per-minute or daily rate limit was hit. Retry after `retry_after_sec`."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
message: str,
|
|
48
|
+
*,
|
|
49
|
+
retry_after_sec: int = 60,
|
|
50
|
+
status_code: int | None = None,
|
|
51
|
+
body: Any = None,
|
|
52
|
+
):
|
|
53
|
+
super().__init__(message, status_code=status_code, body=body)
|
|
54
|
+
self.retry_after_sec = retry_after_sec
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ServiceBusyError(CyberianError):
|
|
58
|
+
"""The platform is at capacity. Retry after `retry_after_sec`."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
message: str,
|
|
63
|
+
*,
|
|
64
|
+
retry_after_sec: int = 30,
|
|
65
|
+
status_code: int | None = None,
|
|
66
|
+
body: Any = None,
|
|
67
|
+
):
|
|
68
|
+
super().__init__(message, status_code=status_code, body=body)
|
|
69
|
+
self.retry_after_sec = retry_after_sec
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class JobFailedError(CyberianError):
|
|
73
|
+
"""A job reached FAILED status during wait_for_completion."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, job_id: str, body: Any = None):
|
|
76
|
+
super().__init__(f"Job {job_id} ended in FAILED state", body=body)
|
|
77
|
+
self.job_id = job_id
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class VerificationFailedError(CyberianError):
|
|
81
|
+
"""The receipt's /verify call returned valid=False."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, receipt_id: str, checks: dict[str, bool], errors: list[str]):
|
|
84
|
+
super().__init__(f"Receipt {receipt_id} failed verification: {errors}")
|
|
85
|
+
self.receipt_id = receipt_id
|
|
86
|
+
self.checks = checks
|
|
87
|
+
self.errors = errors
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ApiError(CyberianError):
|
|
91
|
+
"""Any other unexpected HTTP error from the API."""
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyberian-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Cyberian Systems verified-inference API
|
|
5
|
+
Author-email: Cyberian Systems <philippe@cyberiansystems.ai>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://cyberiansystems.ai
|
|
8
|
+
Project-URL: Documentation, https://cyberiansystems.ai/docs
|
|
9
|
+
Project-URL: Support, https://cyberiansystems.ai/docs
|
|
10
|
+
Keywords: ai,ml,verified-inference,merkle,embeddings,cyberian
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Other/Proprietary 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: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: httpx<1.0,>=0.27.0
|
|
24
|
+
Requires-Dist: numpy<3.0,>=1.26.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# cyberian-client
|
|
30
|
+
|
|
31
|
+
Python client for the [Cyberian Systems](https://cyberiansystems.ai) verified-inference API.
|
|
32
|
+
|
|
33
|
+
Each call returns embeddings + a cryptographic receipt that proves an independent prover
|
|
34
|
+
re-executed a sample of your batch and got bitwise-identical results. The receipt is
|
|
35
|
+
self-verifying: any third party can validate it without access to the platform.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install cyberian-client
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.9+. Pulls in `httpx` and `numpy`.
|
|
44
|
+
|
|
45
|
+
## Get an API key
|
|
46
|
+
|
|
47
|
+
[https://cyberiansystems.ai/signup](https://cyberiansystems.ai/signup) — 14-day free
|
|
48
|
+
trial, no card required. The plaintext key is shown once at signup; save it. We store
|
|
49
|
+
only its SHA-256.
|
|
50
|
+
|
|
51
|
+
## Quickstart — one shot
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from cyberian import Client
|
|
55
|
+
|
|
56
|
+
client = Client(api_key="cyb_trial_…") # or set CYBERIAN_API_KEY in env and pass it in
|
|
57
|
+
|
|
58
|
+
result = client.submit_and_wait(
|
|
59
|
+
spec_yaml=open("spec.yaml").read(),
|
|
60
|
+
input_texts=[
|
|
61
|
+
"The property title search revealed no encumbrances.",
|
|
62
|
+
"Quarterly compliance attestation per SOX 404.",
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
print("receipt_hash:", result.receipt["receipt_hash"])
|
|
67
|
+
print("verified: ", result.verification["valid"]) # True
|
|
68
|
+
print("shape: ", result.embeddings.shape) # (2, 384) for BGE-small
|
|
69
|
+
print("first vec: ", result.embeddings[0, :8])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Step-by-step — when you need finer control
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
job = client.submit_job(spec_yaml="spec.yaml", input_texts=["a", "b", "c"])
|
|
76
|
+
|
|
77
|
+
# Poll yourself, or use wait_for_completion:
|
|
78
|
+
final = client.wait_for_completion(job["id"], poll_interval_sec=2.0, timeout_sec=600)
|
|
79
|
+
|
|
80
|
+
receipt = client.get_receipt(final["receipt_id"])
|
|
81
|
+
verification = client.verify_receipt(final["receipt_id"])
|
|
82
|
+
embeddings = client.get_job_output(final["id"]) # numpy ndarray, float32
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Verification as a Service (VaaS)
|
|
86
|
+
|
|
87
|
+
You ran inference on your own infrastructure. Cyberian re-runs it and certifies the
|
|
88
|
+
SHA-256 commitment of your output matches. Useful for compliance — you keep the
|
|
89
|
+
inference, we provide independent auditable verification.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import hashlib
|
|
93
|
+
|
|
94
|
+
# Your in-house inference output:
|
|
95
|
+
my_embeddings_bytes = my_pipeline.run(["text"]).astype("float32").tobytes()
|
|
96
|
+
my_commitment = hashlib.sha256(my_embeddings_bytes).hexdigest()
|
|
97
|
+
|
|
98
|
+
receipt = client.verify_outputs(
|
|
99
|
+
spec_yaml="spec.yaml",
|
|
100
|
+
input_texts=["text"],
|
|
101
|
+
claimed_output_commitment=my_commitment,
|
|
102
|
+
)
|
|
103
|
+
# Raises VerificationFailedError if our prover's re-execution doesn't match yours.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Phase 2: VaaS is single-chunk only — `len(input_texts) <= spec.chunking.chunk_size`.
|
|
107
|
+
|
|
108
|
+
## Account management
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
info = client.get_account()
|
|
112
|
+
print(info["tier"], info["effective_tier"], info["subscription_status"])
|
|
113
|
+
print(info["chunks_consumed_period"], "/", info["limits"]["chunks_per_period"])
|
|
114
|
+
|
|
115
|
+
# Mint an additional key (old one stays valid until you revoke it)
|
|
116
|
+
new_key = client.rotate_key()
|
|
117
|
+
|
|
118
|
+
# Revoke a specific key by its 12-char key_id (the prefix shown in /account)
|
|
119
|
+
client.revoke_key(key_id="a1b2c3d4e5f6")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Error handling
|
|
123
|
+
|
|
124
|
+
Typed exceptions; catch the most specific one you care about.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from cyberian import (
|
|
128
|
+
Client,
|
|
129
|
+
AuthError, # 401, 403
|
|
130
|
+
TrialExpiredError, # 402 — email upgrade@cyberiansystems.ai
|
|
131
|
+
QuotaError, # 402 / 413
|
|
132
|
+
RateLimitError, # 429 — has .retry_after_sec
|
|
133
|
+
ServiceBusyError, # 503 — has .retry_after_sec
|
|
134
|
+
JobFailedError, # job ended in FAILED state during wait_for_completion
|
|
135
|
+
VerificationFailedError, # /verify returned valid=False
|
|
136
|
+
ApiError, # any other 4xx/5xx
|
|
137
|
+
CyberianError, # base of everything above
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
result = client.submit_and_wait(spec_yaml=…, input_texts=[…])
|
|
142
|
+
except RateLimitError as exc:
|
|
143
|
+
time.sleep(exc.retry_after_sec)
|
|
144
|
+
result = client.submit_and_wait(spec_yaml=…, input_texts=[…])
|
|
145
|
+
except TrialExpiredError:
|
|
146
|
+
print("Email upgrade@cyberiansystems.ai for an extension or enterprise tier.")
|
|
147
|
+
raise
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Trial limits (Phase 2)
|
|
151
|
+
|
|
152
|
+
| | trial (default) | free (post-trial) |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| Period | 14 days | 30 days |
|
|
155
|
+
| Chunks per period | 400 | 100 |
|
|
156
|
+
| Chunks per day | 100 | 25 |
|
|
157
|
+
| Requests per minute | 60 | 10 |
|
|
158
|
+
| Max chunks per request | 100 | 50 |
|
|
159
|
+
| Max input texts | 1000 | 500 |
|
|
160
|
+
|
|
161
|
+
A "chunk" is one ONNX inference unit, sized by your CES `chunking.chunk_size`. With
|
|
162
|
+
`chunk_size: 1` each input text is its own chunk; with `chunk_size: 100` a 1000-text
|
|
163
|
+
batch is 10 chunks. Counting chunks (not requests) means the quota tracks actual
|
|
164
|
+
compute consumption.
|
|
165
|
+
|
|
166
|
+
## Documentation
|
|
167
|
+
|
|
168
|
+
- API + SDK reference: <https://cyberiansystems.ai/docs>
|
|
169
|
+
- Trial terms: <https://cyberiansystems.ai/terms>
|
|
170
|
+
- Privacy: <https://cyberiansystems.ai/privacy>
|
|
171
|
+
- Issues / support: support@cyberiansystems.ai
|
|
172
|
+
- Upgrade / enterprise inquiries: upgrade@cyberiansystems.ai
|
|
173
|
+
|
|
174
|
+
## Patents
|
|
175
|
+
|
|
176
|
+
The verification mechanism (Canonical Execution Specification, Merkle receipts,
|
|
177
|
+
sample-and-replay verification with game-theoretic deterrence, the four-tuple binding)
|
|
178
|
+
is covered by a U.S. provisional patent. Use of this client does not grant any patent
|
|
179
|
+
license beyond what's needed to call the API as documented.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/cyberian/__init__.py
|
|
4
|
+
src/cyberian/client.py
|
|
5
|
+
src/cyberian/exceptions.py
|
|
6
|
+
src/cyberian_client.egg-info/PKG-INFO
|
|
7
|
+
src/cyberian_client.egg-info/SOURCES.txt
|
|
8
|
+
src/cyberian_client.egg-info/dependency_links.txt
|
|
9
|
+
src/cyberian_client.egg-info/requires.txt
|
|
10
|
+
src/cyberian_client.egg-info/top_level.txt
|
|
11
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cyberian
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smoke tests for the Cyberian Client.
|
|
3
|
+
|
|
4
|
+
Uses httpx.MockTransport to fake the API. We're not testing the
|
|
5
|
+
coordinator's logic here (the TS test suite owns that) — just that
|
|
6
|
+
the client constructs the right requests and parses the right
|
|
7
|
+
responses.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from cyberian import (
|
|
18
|
+
ApiError,
|
|
19
|
+
AuthError,
|
|
20
|
+
Client,
|
|
21
|
+
JobFailedError,
|
|
22
|
+
QuotaError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
ServiceBusyError,
|
|
25
|
+
TrialExpiredError,
|
|
26
|
+
VerificationFailedError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_client(handler):
|
|
31
|
+
transport = httpx.MockTransport(handler)
|
|
32
|
+
http = httpx.Client(
|
|
33
|
+
base_url="https://api.test",
|
|
34
|
+
transport=transport,
|
|
35
|
+
headers={"Authorization": "Bearer cyb_test_" + "a" * 32, "Content-Type": "application/json"},
|
|
36
|
+
)
|
|
37
|
+
return Client(api_key="cyb_test_" + "a" * 32, base_url="https://api.test", http_client=http)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_client_rejects_non_cyb_key():
|
|
41
|
+
with pytest.raises(AuthError):
|
|
42
|
+
Client(api_key="not-a-cyberian-key")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_health():
|
|
46
|
+
def handler(req):
|
|
47
|
+
assert req.url.path == "/health"
|
|
48
|
+
return httpx.Response(200, json={"ok": True, "service": "coordinator"})
|
|
49
|
+
|
|
50
|
+
c = make_client(handler)
|
|
51
|
+
assert c.health() == {"ok": True, "service": "coordinator"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_get_account():
|
|
55
|
+
def handler(req):
|
|
56
|
+
assert req.url.path == "/account"
|
|
57
|
+
assert req.headers["authorization"].startswith("Bearer cyb_test_")
|
|
58
|
+
return httpx.Response(200, json={"tier": "free", "subscription_status": "trial"})
|
|
59
|
+
|
|
60
|
+
c = make_client(handler)
|
|
61
|
+
info = c.get_account()
|
|
62
|
+
assert info["tier"] == "free"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_submit_job_serializes_body():
|
|
66
|
+
captured = {}
|
|
67
|
+
|
|
68
|
+
def handler(req):
|
|
69
|
+
captured["body"] = json.loads(req.content)
|
|
70
|
+
return httpx.Response(201, json={"id": "abc-123", "status": "EXECUTING"})
|
|
71
|
+
|
|
72
|
+
c = make_client(handler)
|
|
73
|
+
out = c.submit_job(spec_yaml="version: 1.0", input_texts=["a", "b"])
|
|
74
|
+
assert out["id"] == "abc-123"
|
|
75
|
+
assert captured["body"] == {"spec_yaml": "version: 1.0", "input_texts": ["a", "b"]}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_get_job_output_decodes_to_ndarray():
|
|
79
|
+
# Pretend the server returned 6 floats = 3 rows × 2 cols.
|
|
80
|
+
raw = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], dtype=np.float32).tobytes()
|
|
81
|
+
|
|
82
|
+
def handler(req):
|
|
83
|
+
assert req.url.path == "/jobs/job-99/output"
|
|
84
|
+
return httpx.Response(
|
|
85
|
+
200,
|
|
86
|
+
content=raw,
|
|
87
|
+
headers={
|
|
88
|
+
"x-cyberian-shape": "3,2",
|
|
89
|
+
"x-cyberian-dtype": "float32",
|
|
90
|
+
"content-type": "application/octet-stream",
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
c = make_client(handler)
|
|
95
|
+
arr = c.get_job_output("job-99")
|
|
96
|
+
assert arr.shape == (3, 2)
|
|
97
|
+
assert arr.dtype == np.float32
|
|
98
|
+
assert arr[1, 0] == 3.0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_wait_for_completion_terminates_on_settled():
|
|
102
|
+
seq = iter(["EXECUTING", "PROVING", "SETTLED"])
|
|
103
|
+
|
|
104
|
+
def handler(req):
|
|
105
|
+
return httpx.Response(200, json={"id": "j", "status": next(seq), "receipt_id": "r"})
|
|
106
|
+
|
|
107
|
+
c = make_client(handler)
|
|
108
|
+
final = c.wait_for_completion("j", poll_interval_sec=0.001, timeout_sec=2.0)
|
|
109
|
+
assert final["status"] == "SETTLED"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_wait_for_completion_raises_on_failed():
|
|
113
|
+
def handler(req):
|
|
114
|
+
return httpx.Response(200, json={"id": "j", "status": "FAILED"})
|
|
115
|
+
|
|
116
|
+
c = make_client(handler)
|
|
117
|
+
with pytest.raises(JobFailedError):
|
|
118
|
+
c.wait_for_completion("j", poll_interval_sec=0.001, timeout_sec=2.0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_402_trial_expired_raises_typed():
|
|
122
|
+
def handler(req):
|
|
123
|
+
return httpx.Response(
|
|
124
|
+
402,
|
|
125
|
+
json={"error": "trial_expired", "message": "Your trial has ended"},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
c = make_client(handler)
|
|
129
|
+
with pytest.raises(TrialExpiredError):
|
|
130
|
+
c.submit_job(spec_yaml="x", input_texts=["a"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_402_quota_raises_quota_error():
|
|
134
|
+
def handler(req):
|
|
135
|
+
return httpx.Response(402, json={"error": "quota_exceeded", "message": "out of chunks"})
|
|
136
|
+
|
|
137
|
+
c = make_client(handler)
|
|
138
|
+
with pytest.raises(QuotaError):
|
|
139
|
+
c.submit_job(spec_yaml="x", input_texts=["a"])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_413_oversize_raises_quota_error():
|
|
143
|
+
def handler(req):
|
|
144
|
+
return httpx.Response(413, json={"error": "request_too_large"})
|
|
145
|
+
|
|
146
|
+
c = make_client(handler)
|
|
147
|
+
with pytest.raises(QuotaError):
|
|
148
|
+
c.submit_job(spec_yaml="x", input_texts=["a"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_429_rate_limited_carries_retry_after():
|
|
152
|
+
def handler(req):
|
|
153
|
+
return httpx.Response(
|
|
154
|
+
429,
|
|
155
|
+
headers={"retry-after": "42"},
|
|
156
|
+
json={"error": "rate_limited"},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
c = make_client(handler)
|
|
160
|
+
with pytest.raises(RateLimitError) as ei:
|
|
161
|
+
c.submit_job(spec_yaml="x", input_texts=["a"])
|
|
162
|
+
assert ei.value.retry_after_sec == 42
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_503_busy_carries_retry_after():
|
|
166
|
+
def handler(req):
|
|
167
|
+
return httpx.Response(
|
|
168
|
+
503,
|
|
169
|
+
headers={"retry-after": "20"},
|
|
170
|
+
json={"error": "service_busy"},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
c = make_client(handler)
|
|
174
|
+
with pytest.raises(ServiceBusyError) as ei:
|
|
175
|
+
c.submit_job(spec_yaml="x", input_texts=["a"])
|
|
176
|
+
assert ei.value.retry_after_sec == 20
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_verify_outputs_422_raises_verification_failed():
|
|
180
|
+
def handler(req):
|
|
181
|
+
return httpx.Response(
|
|
182
|
+
422,
|
|
183
|
+
json={"error": "verification_failed", "reason": "commitments differ"},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
c = make_client(handler)
|
|
187
|
+
with pytest.raises(VerificationFailedError):
|
|
188
|
+
c.verify_outputs(spec_yaml="x", input_texts=["a"], claimed_output_commitment="0" * 64)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_unknown_error_falls_through_to_apierror():
|
|
192
|
+
def handler(req):
|
|
193
|
+
return httpx.Response(418, json={"error": "i_am_a_teapot"})
|
|
194
|
+
|
|
195
|
+
c = make_client(handler)
|
|
196
|
+
with pytest.raises(ApiError):
|
|
197
|
+
c.submit_job(spec_yaml="x", input_texts=["a"])
|