zkai 0.5.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.
- zkai-0.5.0/PKG-INFO +143 -0
- zkai-0.5.0/README.md +115 -0
- zkai-0.5.0/pyproject.toml +50 -0
- zkai-0.5.0/setup.cfg +4 -0
- zkai-0.5.0/zkai/__init__.py +10 -0
- zkai-0.5.0/zkai/attestation.py +102 -0
- zkai-0.5.0/zkai/bridge.py +28 -0
- zkai-0.5.0/zkai/client.py +286 -0
- zkai-0.5.0/zkai/crypto.py +60 -0
- zkai-0.5.0/zkai/langchain.py +63 -0
- zkai-0.5.0/zkai/payment.py +38 -0
- zkai-0.5.0/zkai/provider.py +142 -0
- zkai-0.5.0/zkai.egg-info/PKG-INFO +143 -0
- zkai-0.5.0/zkai.egg-info/SOURCES.txt +15 -0
- zkai-0.5.0/zkai.egg-info/dependency_links.txt +1 -0
- zkai-0.5.0/zkai.egg-info/requires.txt +6 -0
- zkai-0.5.0/zkai.egg-info/top_level.txt +1 -0
zkai-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zkai
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Private, verifiable AI inference on 0G chain — drop-in OpenAI-compatible SDK
|
|
5
|
+
Author-email: ZKai <team@zkai.network>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://zkai-ether-og.vercel.app
|
|
8
|
+
Project-URL: Repository, https://github.com/skyyycodes/zkai-eth
|
|
9
|
+
Project-URL: Issues, https://github.com/skyyycodes/zkai-eth/issues
|
|
10
|
+
Keywords: ai,inference,openai,llm,tee,tdx,attestation,blockchain,0g,verifiable,privacy,encryption
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: cryptography>=43.0
|
|
24
|
+
Requires-Dist: requests>=2.32
|
|
25
|
+
Requires-Dist: pydantic>=2.9
|
|
26
|
+
Provides-Extra: langchain
|
|
27
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
28
|
+
|
|
29
|
+
# zkai
|
|
30
|
+
|
|
31
|
+
OpenAI-compatible Python SDK for [ZKai](https://zkai-ether-og.vercel.app) — private, verifiable AI inference on 0G chain.
|
|
32
|
+
|
|
33
|
+
ZKai sends your prompt through a Trusted Execution Environment (Intel TDX), encrypts it client-side so even the gateway cannot read it, and anchors a SHA-256 attestation of every inference on the 0G chain. You get back a normal OpenAI-style response plus an on-chain receipt.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install zkai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Optional LangChain adapter:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "zkai[langchain]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from zkai import ZKai
|
|
51
|
+
|
|
52
|
+
client = ZKai(api_key="zkai-...") # get one at https://zkai-ether-og.vercel.app
|
|
53
|
+
|
|
54
|
+
response = client.chat.completions.create(
|
|
55
|
+
model="qwen2.5:1.5b",
|
|
56
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
print(response.choices[0].message.content)
|
|
60
|
+
print("Attestation hash:", response.attestation_hash)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The response is decrypted locally — the gateway sees only ciphertext.
|
|
64
|
+
The attestation hash is anchored on 0G mainnet and can be verified independently.
|
|
65
|
+
|
|
66
|
+
## How it works (gateway mode, default)
|
|
67
|
+
|
|
68
|
+
1. The SDK fetches the enclave's X25519 public key from the gateway.
|
|
69
|
+
2. It encrypts your prompt locally with ECDH + ChaCha20-Poly1305.
|
|
70
|
+
3. It sends only the ciphertext to the gateway.
|
|
71
|
+
4. The gateway routes the opaque blob to a TDX-sealed enclave running the requested model.
|
|
72
|
+
5. The enclave decrypts inside sealed memory, runs the model, encrypts the response, and emits a SHA-256 attestation hash.
|
|
73
|
+
6. The attestation lands on the on-chain `AttestationRegistry` contract.
|
|
74
|
+
7. The SDK decrypts the response locally and returns it to you.
|
|
75
|
+
|
|
76
|
+
End-to-end, the gateway never sees plaintext.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
ZKai(
|
|
82
|
+
api_key="zkai-...", # issued from the dashboard
|
|
83
|
+
base_url="https://zkai-ether-og.vercel.app", # default
|
|
84
|
+
encrypted=True, # default; set False to use the legacy plaintext path
|
|
85
|
+
provider_endpoint=None, # set to bypass gateway and hit a provider directly
|
|
86
|
+
skip_attestation=False, # only for development; do not disable in prod
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Argument | Type | Default | Description |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `api_key` | `str | None` | `None` | Sent as `X-API-Key`. Required for the hosted gateway. |
|
|
93
|
+
| `base_url` | `str | None` | `https://zkai-ether-og.vercel.app` | Override to point at a self-hosted gateway. |
|
|
94
|
+
| `encrypted` | `bool` | `True` | When `True`, prompts are encrypted client-side. Set `False` for legacy clients. |
|
|
95
|
+
| `provider_endpoint` | `str | None` | `None` | When set, bypasses the gateway and talks directly to a provider's `/infer` endpoint. |
|
|
96
|
+
| `skip_attestation` | `bool` | `False` | Disables attestation verification. Useful only for dev loops. |
|
|
97
|
+
|
|
98
|
+
## OpenAI compatibility
|
|
99
|
+
|
|
100
|
+
The response object mirrors OpenAI's chat completion shape:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
response.id
|
|
104
|
+
response.model
|
|
105
|
+
response.choices[0].message.content
|
|
106
|
+
response.choices[0].finish_reason
|
|
107
|
+
response.usage.prompt_tokens
|
|
108
|
+
response.usage.completion_tokens
|
|
109
|
+
|
|
110
|
+
# ZKai-specific:
|
|
111
|
+
response.attestation_hash # on-chain commitment
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Drop-in replacement for most OpenAI SDK call sites — just swap the client class.
|
|
115
|
+
|
|
116
|
+
## On-chain components (0G mainnet, chain ID 16661)
|
|
117
|
+
|
|
118
|
+
| Contract | Address |
|
|
119
|
+
|---|---|
|
|
120
|
+
| ProviderRegistry | `0x6D400F5D1DcCaA3e98E3dE17322aA23DE38bAC99` |
|
|
121
|
+
| PaymentEscrow | `0xb2C7c0F7a4C2877319E8Ed1Fae0bf3C705b6Fc4C` |
|
|
122
|
+
| AttestationRegistry | `0x8c8Ae0A113084268D181fd1cf23d611DC2EAa2B2` |
|
|
123
|
+
|
|
124
|
+
Verify any attestation hash on the [0G explorer](https://chainscan.0g.ai).
|
|
125
|
+
|
|
126
|
+
## Running your own provider
|
|
127
|
+
|
|
128
|
+
See the companion [`zkai-cli`](https://pypi.org/project/zkai-cli/) package:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install zkai-cli
|
|
132
|
+
zkai init && zkai start && zkai register
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Links
|
|
136
|
+
|
|
137
|
+
- Dashboard — https://zkai-ether-og.vercel.app
|
|
138
|
+
- Repository — https://github.com/skyyycodes/zkai-eth
|
|
139
|
+
- Provider CLI — https://pypi.org/project/zkai-cli/
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
zkai-0.5.0/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# zkai
|
|
2
|
+
|
|
3
|
+
OpenAI-compatible Python SDK for [ZKai](https://zkai-ether-og.vercel.app) — private, verifiable AI inference on 0G chain.
|
|
4
|
+
|
|
5
|
+
ZKai sends your prompt through a Trusted Execution Environment (Intel TDX), encrypts it client-side so even the gateway cannot read it, and anchors a SHA-256 attestation of every inference on the 0G chain. You get back a normal OpenAI-style response plus an on-chain receipt.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install zkai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional LangChain adapter:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install "zkai[langchain]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from zkai import ZKai
|
|
23
|
+
|
|
24
|
+
client = ZKai(api_key="zkai-...") # get one at https://zkai-ether-og.vercel.app
|
|
25
|
+
|
|
26
|
+
response = client.chat.completions.create(
|
|
27
|
+
model="qwen2.5:1.5b",
|
|
28
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
print(response.choices[0].message.content)
|
|
32
|
+
print("Attestation hash:", response.attestation_hash)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The response is decrypted locally — the gateway sees only ciphertext.
|
|
36
|
+
The attestation hash is anchored on 0G mainnet and can be verified independently.
|
|
37
|
+
|
|
38
|
+
## How it works (gateway mode, default)
|
|
39
|
+
|
|
40
|
+
1. The SDK fetches the enclave's X25519 public key from the gateway.
|
|
41
|
+
2. It encrypts your prompt locally with ECDH + ChaCha20-Poly1305.
|
|
42
|
+
3. It sends only the ciphertext to the gateway.
|
|
43
|
+
4. The gateway routes the opaque blob to a TDX-sealed enclave running the requested model.
|
|
44
|
+
5. The enclave decrypts inside sealed memory, runs the model, encrypts the response, and emits a SHA-256 attestation hash.
|
|
45
|
+
6. The attestation lands on the on-chain `AttestationRegistry` contract.
|
|
46
|
+
7. The SDK decrypts the response locally and returns it to you.
|
|
47
|
+
|
|
48
|
+
End-to-end, the gateway never sees plaintext.
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
ZKai(
|
|
54
|
+
api_key="zkai-...", # issued from the dashboard
|
|
55
|
+
base_url="https://zkai-ether-og.vercel.app", # default
|
|
56
|
+
encrypted=True, # default; set False to use the legacy plaintext path
|
|
57
|
+
provider_endpoint=None, # set to bypass gateway and hit a provider directly
|
|
58
|
+
skip_attestation=False, # only for development; do not disable in prod
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Argument | Type | Default | Description |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `api_key` | `str | None` | `None` | Sent as `X-API-Key`. Required for the hosted gateway. |
|
|
65
|
+
| `base_url` | `str | None` | `https://zkai-ether-og.vercel.app` | Override to point at a self-hosted gateway. |
|
|
66
|
+
| `encrypted` | `bool` | `True` | When `True`, prompts are encrypted client-side. Set `False` for legacy clients. |
|
|
67
|
+
| `provider_endpoint` | `str | None` | `None` | When set, bypasses the gateway and talks directly to a provider's `/infer` endpoint. |
|
|
68
|
+
| `skip_attestation` | `bool` | `False` | Disables attestation verification. Useful only for dev loops. |
|
|
69
|
+
|
|
70
|
+
## OpenAI compatibility
|
|
71
|
+
|
|
72
|
+
The response object mirrors OpenAI's chat completion shape:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
response.id
|
|
76
|
+
response.model
|
|
77
|
+
response.choices[0].message.content
|
|
78
|
+
response.choices[0].finish_reason
|
|
79
|
+
response.usage.prompt_tokens
|
|
80
|
+
response.usage.completion_tokens
|
|
81
|
+
|
|
82
|
+
# ZKai-specific:
|
|
83
|
+
response.attestation_hash # on-chain commitment
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Drop-in replacement for most OpenAI SDK call sites — just swap the client class.
|
|
87
|
+
|
|
88
|
+
## On-chain components (0G mainnet, chain ID 16661)
|
|
89
|
+
|
|
90
|
+
| Contract | Address |
|
|
91
|
+
|---|---|
|
|
92
|
+
| ProviderRegistry | `0x6D400F5D1DcCaA3e98E3dE17322aA23DE38bAC99` |
|
|
93
|
+
| PaymentEscrow | `0xb2C7c0F7a4C2877319E8Ed1Fae0bf3C705b6Fc4C` |
|
|
94
|
+
| AttestationRegistry | `0x8c8Ae0A113084268D181fd1cf23d611DC2EAa2B2` |
|
|
95
|
+
|
|
96
|
+
Verify any attestation hash on the [0G explorer](https://chainscan.0g.ai).
|
|
97
|
+
|
|
98
|
+
## Running your own provider
|
|
99
|
+
|
|
100
|
+
See the companion [`zkai-cli`](https://pypi.org/project/zkai-cli/) package:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install zkai-cli
|
|
104
|
+
zkai init && zkai start && zkai register
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Links
|
|
108
|
+
|
|
109
|
+
- Dashboard — https://zkai-ether-og.vercel.app
|
|
110
|
+
- Repository — https://github.com/skyyycodes/zkai-eth
|
|
111
|
+
- Provider CLI — https://pypi.org/project/zkai-cli/
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zkai"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Private, verifiable AI inference on 0G chain — drop-in OpenAI-compatible SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "ZKai", email = "team@zkai.network" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai", "inference", "openai", "llm", "tee", "tdx", "attestation",
|
|
17
|
+
"blockchain", "0g", "verifiable", "privacy", "encryption",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
29
|
+
"Topic :: Security :: Cryptography",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"cryptography>=43.0",
|
|
33
|
+
"requests>=2.32",
|
|
34
|
+
"pydantic>=2.9",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
langchain = [
|
|
39
|
+
"langchain-core>=0.3",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://zkai-ether-og.vercel.app"
|
|
44
|
+
Repository = "https://github.com/skyyycodes/zkai-eth"
|
|
45
|
+
Issues = "https://github.com/skyyycodes/zkai-eth/issues"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["."]
|
|
49
|
+
include = ["zkai*"]
|
|
50
|
+
exclude = ["zkai.egg-info*"]
|
zkai-0.5.0/setup.cfg
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .client import ZKai, ChatCompletion, ZKaiAuthError
|
|
2
|
+
from .attestation import ZKaiAttestationError
|
|
3
|
+
|
|
4
|
+
def __getattr__(name):
|
|
5
|
+
if name == "ChatZKai":
|
|
6
|
+
from .langchain import ChatZKai
|
|
7
|
+
return ChatZKai
|
|
8
|
+
raise AttributeError(f"module 'zkai' has no attribute {name!r}")
|
|
9
|
+
|
|
10
|
+
__all__ = ["ZKai", "ChatCompletion", "ZKaiAuthError", "ZKaiAttestationError", "ChatZKai"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Attestation verification.
|
|
3
|
+
SDK calls this silently after every inference — user never thinks about it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
INDEXER_URL = "https://indexer.preprod.midnight.network/api/v3/graphql"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ZKaiAttestationError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def verify(
|
|
18
|
+
provider_url: str,
|
|
19
|
+
received_attestation_hash: str,
|
|
20
|
+
on_chain_hash: str | None = None,
|
|
21
|
+
attestation_contract: str | None = None,
|
|
22
|
+
job_id: str | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Fetch attestation from provider, hash it, compare to:
|
|
26
|
+
1. The hash included in the /infer response
|
|
27
|
+
2. The hash anchored on-chain (if attestation_contract + job_id provided)
|
|
28
|
+
|
|
29
|
+
Raises ZKaiAttestationError if anything doesn't match.
|
|
30
|
+
"""
|
|
31
|
+
resp = requests.get(f"{provider_url}/attestation", timeout=10)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
attestation = resp.json()
|
|
34
|
+
|
|
35
|
+
# Recompute hash of the report (excluding the hash field itself)
|
|
36
|
+
report_copy = {k: v for k, v in attestation.items() if k not in ("report_hash", "signature")}
|
|
37
|
+
report_bytes = json.dumps(report_copy, sort_keys=True).encode()
|
|
38
|
+
computed_hash = hashlib.sha256(report_bytes).hexdigest()
|
|
39
|
+
|
|
40
|
+
if computed_hash != received_attestation_hash:
|
|
41
|
+
raise ZKaiAttestationError(
|
|
42
|
+
f"Attestation hash mismatch.\n"
|
|
43
|
+
f" From provider response: {received_attestation_hash}\n"
|
|
44
|
+
f" Recomputed: {computed_hash}\n"
|
|
45
|
+
f" Provider may have tampered with the report."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Fetch on-chain hash if not provided directly
|
|
49
|
+
if on_chain_hash is None and attestation_contract and job_id:
|
|
50
|
+
on_chain_hash = _fetch_on_chain_hash(attestation_contract, job_id)
|
|
51
|
+
|
|
52
|
+
if on_chain_hash and computed_hash != on_chain_hash:
|
|
53
|
+
raise ZKaiAttestationError(
|
|
54
|
+
f"Attestation does not match on-chain anchor.\n"
|
|
55
|
+
f" On-chain: {on_chain_hash}\n"
|
|
56
|
+
f" Computed: {computed_hash}\n"
|
|
57
|
+
f" Model hash: {attestation.get('model_hash', 'unknown')}\n"
|
|
58
|
+
f" Possible tampered model or manifest."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return attestation
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _fetch_on_chain_hash(attestation_contract: str, job_id: str) -> str | None:
|
|
65
|
+
"""Query Midnight indexer for the attestation hash stored for a given job_id."""
|
|
66
|
+
query = """
|
|
67
|
+
query GetAttestation($address: String!) {
|
|
68
|
+
contract(address: $address) {
|
|
69
|
+
state {
|
|
70
|
+
... on ContractState {
|
|
71
|
+
ledger {
|
|
72
|
+
... on ZkaiAttestationRegistryLedger {
|
|
73
|
+
att_hash { entries { key value } }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
resp = requests.post(
|
|
83
|
+
INDEXER_URL,
|
|
84
|
+
json={"query": query, "variables": {"address": attestation_contract}},
|
|
85
|
+
timeout=15,
|
|
86
|
+
)
|
|
87
|
+
resp.raise_for_status()
|
|
88
|
+
data = resp.json()
|
|
89
|
+
entries = (
|
|
90
|
+
data.get("data", {})
|
|
91
|
+
.get("contract", {})
|
|
92
|
+
.get("state", {})
|
|
93
|
+
.get("ledger", {})
|
|
94
|
+
.get("att_hash", {})
|
|
95
|
+
.get("entries", [])
|
|
96
|
+
)
|
|
97
|
+
for entry in entries:
|
|
98
|
+
if entry["key"] == job_id:
|
|
99
|
+
return entry["value"]
|
|
100
|
+
return None
|
|
101
|
+
except Exception:
|
|
102
|
+
return None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP client for the ZKai bridge server (Node.js).
|
|
3
|
+
The bridge holds the Midnight wallet and translates Python calls into on-chain transactions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BridgeClient:
|
|
10
|
+
def __init__(self, base_url: str = "http://localhost:7300"):
|
|
11
|
+
self.base_url = base_url.rstrip("/")
|
|
12
|
+
|
|
13
|
+
def call(self, path: str, payload: dict) -> dict:
|
|
14
|
+
resp = requests.post(
|
|
15
|
+
f"{self.base_url}{path}",
|
|
16
|
+
json=payload,
|
|
17
|
+
timeout=120, # ZK proofs can take 30-60s
|
|
18
|
+
)
|
|
19
|
+
resp.raise_for_status()
|
|
20
|
+
data = resp.json()
|
|
21
|
+
if "error" in data:
|
|
22
|
+
raise RuntimeError(f"Bridge error on {path}: {data['error']}")
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
def health(self) -> dict:
|
|
26
|
+
resp = requests.get(f"{self.base_url}/health", timeout=10)
|
|
27
|
+
resp.raise_for_status()
|
|
28
|
+
return resp.json()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZKai client — OpenAI-compatible interface with end-to-end encryption.
|
|
3
|
+
|
|
4
|
+
Modes:
|
|
5
|
+
- Encrypted gateway (default): prompt encrypted client-side with the
|
|
6
|
+
enclave's X25519 pubkey; gateway sees only ciphertext.
|
|
7
|
+
- Plain gateway: legacy fallback for compatibility.
|
|
8
|
+
- Direct provider: bypass gateway entirely.
|
|
9
|
+
|
|
10
|
+
Change 2 lines, everything else stays the same.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
from . import crypto, provider as provider_mod, attestation as att_mod
|
|
17
|
+
from .attestation import ZKaiAttestationError
|
|
18
|
+
|
|
19
|
+
# Default gateway — consumers send requests here, gateway picks a provider
|
|
20
|
+
GATEWAY_URL = "https://zkai-ether-og.vercel.app"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── OpenAI-compatible response types ────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Message:
|
|
27
|
+
role: str
|
|
28
|
+
content: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Choice:
|
|
33
|
+
index: int
|
|
34
|
+
message: Message
|
|
35
|
+
finish_reason: str = "stop"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ChatCompletion:
|
|
40
|
+
id: str
|
|
41
|
+
object: str
|
|
42
|
+
model: str
|
|
43
|
+
choices: list[Choice]
|
|
44
|
+
usage: dict = field(default_factory=dict)
|
|
45
|
+
attestation_hash: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Main client ──────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
class ZKai:
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
api_key: str | None = None,
|
|
54
|
+
base_url: str | None = None,
|
|
55
|
+
# legacy / advanced: bypass gateway and talk to a provider directly
|
|
56
|
+
provider_endpoint: str | None = None,
|
|
57
|
+
max_price: float | None = None,
|
|
58
|
+
min_reputation: float = 0.0,
|
|
59
|
+
registry_contract: str | None = None,
|
|
60
|
+
attestation_contract: str | None = None,
|
|
61
|
+
skip_attestation: bool = False,
|
|
62
|
+
# End-to-end encryption via gateway (default ON). Set False to use
|
|
63
|
+
# the legacy plaintext-to-gateway path.
|
|
64
|
+
encrypted: bool = True,
|
|
65
|
+
):
|
|
66
|
+
self._api_key = api_key
|
|
67
|
+
self._base_url = (base_url or GATEWAY_URL).rstrip("/")
|
|
68
|
+
self._provider_endpoint = provider_endpoint
|
|
69
|
+
self._max_price = max_price
|
|
70
|
+
self._min_reputation = min_reputation
|
|
71
|
+
self._registry_contract = registry_contract
|
|
72
|
+
self._attestation_contract = attestation_contract
|
|
73
|
+
self._skip_attestation = skip_attestation
|
|
74
|
+
self._encrypted = encrypted
|
|
75
|
+
self.chat = _Chat(self)
|
|
76
|
+
|
|
77
|
+
def _infer(self, model: str, messages: list[dict]) -> ChatCompletion:
|
|
78
|
+
if self._provider_endpoint:
|
|
79
|
+
return self._infer_direct(model, messages)
|
|
80
|
+
if self._encrypted:
|
|
81
|
+
return self._infer_via_gateway_encrypted(model, messages)
|
|
82
|
+
return self._infer_via_gateway_plain(model, messages)
|
|
83
|
+
|
|
84
|
+
# ── Encrypted gateway path (default) ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def _infer_via_gateway_encrypted(self, model: str, messages: list[dict]) -> ChatCompletion:
|
|
87
|
+
"""
|
|
88
|
+
End-to-end encrypted via the ZKai gateway.
|
|
89
|
+
|
|
90
|
+
Flow:
|
|
91
|
+
1. GET /api/providers/pubkey?model=... → enclave pubkey + provider_id
|
|
92
|
+
2. Generate ephemeral X25519 keypair locally
|
|
93
|
+
3. Encrypt prompt with ECDH(ephemeral_priv, enclave_pub) → ciphertext
|
|
94
|
+
4. POST /api/v1/encrypted-chat with { provider_id, client_pubkey, encrypted_prompt }
|
|
95
|
+
5. Decrypt response with ECDH(ephemeral_priv, enclave_pub)
|
|
96
|
+
|
|
97
|
+
Gateway never sees plaintext.
|
|
98
|
+
"""
|
|
99
|
+
headers = {"Content-Type": "application/json"}
|
|
100
|
+
if self._api_key:
|
|
101
|
+
headers["X-API-Key"] = self._api_key
|
|
102
|
+
|
|
103
|
+
# 1. Fetch enclave pubkey for this model
|
|
104
|
+
pk_resp = requests.get(
|
|
105
|
+
f"{self._base_url}/api/providers/pubkey",
|
|
106
|
+
params={"model": model},
|
|
107
|
+
headers=headers,
|
|
108
|
+
timeout=15,
|
|
109
|
+
)
|
|
110
|
+
if pk_resp.status_code == 503:
|
|
111
|
+
raise ZKaiNoProviderError("No providers available for this model.")
|
|
112
|
+
pk_resp.raise_for_status()
|
|
113
|
+
pk_data = pk_resp.json()
|
|
114
|
+
enclave_pubkey = pk_data["pubkey"]
|
|
115
|
+
provider_id = pk_data["provider_id"]
|
|
116
|
+
|
|
117
|
+
# 2-3. Generate ephemeral keypair + encrypt
|
|
118
|
+
prompt = _messages_to_prompt(messages)
|
|
119
|
+
our_priv, our_pub = crypto.generate_keypair()
|
|
120
|
+
encrypted_prompt = crypto.encrypt(prompt, enclave_pubkey, our_priv)
|
|
121
|
+
|
|
122
|
+
# 4. Send encrypted blob via gateway
|
|
123
|
+
resp = requests.post(
|
|
124
|
+
f"{self._base_url}/api/v1/encrypted-chat",
|
|
125
|
+
json={
|
|
126
|
+
"provider_id": provider_id,
|
|
127
|
+
"client_pubkey": our_pub,
|
|
128
|
+
"encrypted_prompt": encrypted_prompt,
|
|
129
|
+
"model": model,
|
|
130
|
+
},
|
|
131
|
+
headers=headers,
|
|
132
|
+
timeout=120,
|
|
133
|
+
)
|
|
134
|
+
if resp.status_code == 401:
|
|
135
|
+
raise ZKaiAuthError("Invalid or missing API key. Get one at https://zkai-ether-og.vercel.app")
|
|
136
|
+
if resp.status_code == 503:
|
|
137
|
+
raise ZKaiNoProviderError("No providers available for this model.")
|
|
138
|
+
resp.raise_for_status()
|
|
139
|
+
data = resp.json()
|
|
140
|
+
|
|
141
|
+
# 5. Decrypt response locally
|
|
142
|
+
response_text = crypto.decrypt(data["encrypted_response"], enclave_pubkey, our_priv)
|
|
143
|
+
token_count = len(response_text.split())
|
|
144
|
+
|
|
145
|
+
return ChatCompletion(
|
|
146
|
+
id=f"zkai-{data['job_id'][:8]}",
|
|
147
|
+
object="chat.completion",
|
|
148
|
+
model=model,
|
|
149
|
+
choices=[Choice(
|
|
150
|
+
index=0,
|
|
151
|
+
message=Message(role="assistant", content=response_text),
|
|
152
|
+
finish_reason="stop",
|
|
153
|
+
)],
|
|
154
|
+
usage={"prompt_tokens": len(prompt.split()), "completion_tokens": token_count},
|
|
155
|
+
attestation_hash=data.get("attestation_hash"),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# ── Plain gateway path (legacy) ──────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
def _infer_via_gateway_plain(self, model: str, messages: list[dict]) -> ChatCompletion:
|
|
161
|
+
"""Send plaintext request to the ZKai gateway (legacy)."""
|
|
162
|
+
headers = {"Content-Type": "application/json"}
|
|
163
|
+
if self._api_key:
|
|
164
|
+
headers["X-API-Key"] = self._api_key
|
|
165
|
+
|
|
166
|
+
resp = requests.post(
|
|
167
|
+
f"{self._base_url}/api/v1/chat/completions",
|
|
168
|
+
json={"model": model, "messages": messages},
|
|
169
|
+
headers=headers,
|
|
170
|
+
timeout=120,
|
|
171
|
+
)
|
|
172
|
+
if resp.status_code == 401:
|
|
173
|
+
raise ZKaiAuthError("Invalid or missing API key.")
|
|
174
|
+
if resp.status_code == 503:
|
|
175
|
+
raise ZKaiNoProviderError("No providers available for this model.")
|
|
176
|
+
resp.raise_for_status()
|
|
177
|
+
data = resp.json()
|
|
178
|
+
|
|
179
|
+
choice = data["choices"][0]
|
|
180
|
+
xz = data.get("x_zkai", {})
|
|
181
|
+
return ChatCompletion(
|
|
182
|
+
id=data.get("id", "zkai-gateway"),
|
|
183
|
+
object="chat.completion",
|
|
184
|
+
model=data.get("model", model),
|
|
185
|
+
choices=[Choice(
|
|
186
|
+
index=0,
|
|
187
|
+
message=Message(role="assistant", content=choice["message"]["content"]),
|
|
188
|
+
finish_reason=choice.get("finish_reason", "stop"),
|
|
189
|
+
)],
|
|
190
|
+
usage=data.get("usage", {}),
|
|
191
|
+
attestation_hash=xz.get("attestation_hash"),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# ── Direct provider path (advanced) ──────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
def _infer_direct(self, model: str, messages: list[dict]) -> ChatCompletion:
|
|
197
|
+
"""Bypass gateway — encrypt and send directly to a provider enclave."""
|
|
198
|
+
prompt = _messages_to_prompt(messages)
|
|
199
|
+
|
|
200
|
+
p = provider_mod.Provider(
|
|
201
|
+
id="direct",
|
|
202
|
+
endpoint=self._provider_endpoint,
|
|
203
|
+
pubkey="",
|
|
204
|
+
model=model,
|
|
205
|
+
price_per_token=0.0,
|
|
206
|
+
reputation=1.0,
|
|
207
|
+
stake=0.0,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
tee_pubkey = provider_mod.fetch_pubkey(p)
|
|
211
|
+
our_private, our_public = crypto.generate_keypair()
|
|
212
|
+
encrypted_prompt = crypto.encrypt(prompt, tee_pubkey, our_private)
|
|
213
|
+
|
|
214
|
+
headers = {"X-API-Key": self._api_key} if self._api_key else {}
|
|
215
|
+
resp = requests.post(
|
|
216
|
+
f"{p.endpoint}/infer",
|
|
217
|
+
json={"client_pubkey": our_public, "encrypted_prompt": encrypted_prompt},
|
|
218
|
+
headers=headers,
|
|
219
|
+
timeout=120,
|
|
220
|
+
)
|
|
221
|
+
if resp.status_code == 401:
|
|
222
|
+
raise ZKaiAuthError("Invalid or missing API key.")
|
|
223
|
+
resp.raise_for_status()
|
|
224
|
+
data = resp.json()
|
|
225
|
+
|
|
226
|
+
job_id = data["job_id"]
|
|
227
|
+
|
|
228
|
+
if not self._skip_attestation:
|
|
229
|
+
att_mod.verify(
|
|
230
|
+
provider_url=p.endpoint,
|
|
231
|
+
received_attestation_hash=data["attestation_hash"],
|
|
232
|
+
attestation_contract=self._attestation_contract,
|
|
233
|
+
job_id=job_id,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
response_text = crypto.decrypt(data["encrypted_response"], tee_pubkey, our_private)
|
|
237
|
+
token_count = len(response_text.split())
|
|
238
|
+
|
|
239
|
+
return ChatCompletion(
|
|
240
|
+
id=f"zkai-{job_id[:8]}",
|
|
241
|
+
object="chat.completion",
|
|
242
|
+
model=model,
|
|
243
|
+
choices=[Choice(index=0, message=Message(role="assistant", content=response_text))],
|
|
244
|
+
usage={"prompt_tokens": len(prompt.split()), "completion_tokens": token_count},
|
|
245
|
+
attestation_hash=data.get("attestation_hash"),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _Chat:
|
|
250
|
+
def __init__(self, client: ZKai):
|
|
251
|
+
self.completions = _Completions(client)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class _Completions:
|
|
255
|
+
def __init__(self, client: ZKai):
|
|
256
|
+
self._client = client
|
|
257
|
+
|
|
258
|
+
def create(self, model: str, messages: list[dict], **kwargs) -> ChatCompletion:
|
|
259
|
+
return self._client._infer(model, messages)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ── Exceptions ───────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
class ZKaiAuthError(Exception):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class ZKaiNoProviderError(Exception):
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def _messages_to_prompt(messages: list[dict]) -> str:
|
|
275
|
+
parts = []
|
|
276
|
+
for m in messages:
|
|
277
|
+
role = m.get("role", "user")
|
|
278
|
+
content = m.get("content", "")
|
|
279
|
+
if role == "system":
|
|
280
|
+
parts.append(f"System: {content}")
|
|
281
|
+
elif role == "user":
|
|
282
|
+
parts.append(f"User: {content}")
|
|
283
|
+
elif role == "assistant":
|
|
284
|
+
parts.append(f"Assistant: {content}")
|
|
285
|
+
parts.append("Assistant:")
|
|
286
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client-side encryption primitives.
|
|
3
|
+
Matches the enclave's decrypt logic exactly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption
|
|
11
|
+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_keypair() -> tuple[str, str]:
|
|
15
|
+
"""Generate ephemeral X25519 keypair. Returns (private_hex, public_hex)."""
|
|
16
|
+
private_key = X25519PrivateKey.generate()
|
|
17
|
+
private_bytes = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
|
|
18
|
+
public_bytes = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
19
|
+
return private_bytes.hex(), public_bytes.hex()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def encrypt(plaintext: str, recipient_pubkey_hex: str, our_private_hex: str) -> str:
|
|
23
|
+
"""
|
|
24
|
+
ECDH + ChaCha20-Poly1305 encrypt.
|
|
25
|
+
Returns hex(nonce + ciphertext + tag).
|
|
26
|
+
"""
|
|
27
|
+
recipient_pubkey = X25519PublicKey.from_public_bytes(bytes.fromhex(recipient_pubkey_hex))
|
|
28
|
+
our_private = X25519PrivateKey.from_private_bytes(bytes.fromhex(our_private_hex))
|
|
29
|
+
|
|
30
|
+
shared_secret = our_private.exchange(recipient_pubkey)
|
|
31
|
+
key = _derive_key(shared_secret)
|
|
32
|
+
|
|
33
|
+
nonce = os.urandom(12)
|
|
34
|
+
chacha = ChaCha20Poly1305(key)
|
|
35
|
+
ciphertext = chacha.encrypt(nonce, plaintext.encode("utf-8"), None)
|
|
36
|
+
return (nonce + ciphertext).hex()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decrypt(encrypted_hex: str, sender_pubkey_hex: str, our_private_hex: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
ECDH + ChaCha20-Poly1305 decrypt.
|
|
42
|
+
encrypted_hex = hex(nonce + ciphertext + tag)
|
|
43
|
+
"""
|
|
44
|
+
sender_pubkey = X25519PublicKey.from_public_bytes(bytes.fromhex(sender_pubkey_hex))
|
|
45
|
+
our_private = X25519PrivateKey.from_private_bytes(bytes.fromhex(our_private_hex))
|
|
46
|
+
|
|
47
|
+
shared_secret = our_private.exchange(sender_pubkey)
|
|
48
|
+
key = _derive_key(shared_secret)
|
|
49
|
+
|
|
50
|
+
data = bytes.fromhex(encrypted_hex)
|
|
51
|
+
nonce = data[:12]
|
|
52
|
+
payload = data[12:]
|
|
53
|
+
|
|
54
|
+
chacha = ChaCha20Poly1305(key)
|
|
55
|
+
plaintext = chacha.decrypt(nonce, payload, None)
|
|
56
|
+
return plaintext.decode("utf-8")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _derive_key(shared_secret: bytes) -> bytes:
|
|
60
|
+
return hashlib.sha256(shared_secret).digest()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain adapter — one line change from ChatOpenAI.
|
|
3
|
+
|
|
4
|
+
Before:
|
|
5
|
+
from langchain_openai import ChatOpenAI
|
|
6
|
+
llm = ChatOpenAI(model="gpt-4")
|
|
7
|
+
|
|
8
|
+
After:
|
|
9
|
+
from zkai import ChatZKai
|
|
10
|
+
llm = ChatZKai(model="qwen2.5-1.5b", api_key="your-key")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, List, Optional
|
|
14
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
15
|
+
from langchain_core.messages import BaseMessage, AIMessage
|
|
16
|
+
from langchain_core.outputs import ChatGeneration, ChatResult
|
|
17
|
+
|
|
18
|
+
from .client import ZKai
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChatZKai(BaseChatModel):
|
|
22
|
+
model: str = "qwen2.5-1.5b"
|
|
23
|
+
api_key: Optional[str] = None
|
|
24
|
+
max_price: Optional[float] = None
|
|
25
|
+
min_reputation: float = 0.0
|
|
26
|
+
registry_contract: Optional[str] = None
|
|
27
|
+
attestation_contract: Optional[str] = None
|
|
28
|
+
skip_attestation: bool = False
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def _llm_type(self) -> str:
|
|
32
|
+
return "zkai"
|
|
33
|
+
|
|
34
|
+
def _get_client(self) -> ZKai:
|
|
35
|
+
return ZKai(
|
|
36
|
+
api_key=self.api_key,
|
|
37
|
+
max_price=self.max_price,
|
|
38
|
+
min_reputation=self.min_reputation,
|
|
39
|
+
registry_contract=self.registry_contract,
|
|
40
|
+
attestation_contract=self.attestation_contract,
|
|
41
|
+
skip_attestation=self.skip_attestation,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _generate(self, messages: List[BaseMessage], **kwargs: Any) -> ChatResult:
|
|
45
|
+
openai_messages = [
|
|
46
|
+
{"role": _lc_role(m), "content": m.content}
|
|
47
|
+
for m in messages
|
|
48
|
+
]
|
|
49
|
+
completion = self._get_client().chat.completions.create(
|
|
50
|
+
model=self.model,
|
|
51
|
+
messages=openai_messages,
|
|
52
|
+
)
|
|
53
|
+
text = completion.choices[0].message.content
|
|
54
|
+
return ChatResult(generations=[ChatGeneration(message=AIMessage(content=text))])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _lc_role(message: BaseMessage) -> str:
|
|
58
|
+
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
|
59
|
+
if isinstance(message, SystemMessage):
|
|
60
|
+
return "system"
|
|
61
|
+
if isinstance(message, AIMessage):
|
|
62
|
+
return "assistant"
|
|
63
|
+
return "user"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Midnight payment escrow integration via ZKai bridge server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .bridge import BridgeClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PaymentClient:
|
|
9
|
+
def __init__(self, wallet_key: str | None = None, bridge_url: str | None = None):
|
|
10
|
+
self.wallet_key = wallet_key
|
|
11
|
+
self._bridge = BridgeClient(bridge_url) if bridge_url else None
|
|
12
|
+
|
|
13
|
+
def create_job(self, provider_id: str, token_budget: int, job_id: str) -> str:
|
|
14
|
+
"""Lock DUST in escrow. Returns the same job_id."""
|
|
15
|
+
if self._bridge is None:
|
|
16
|
+
return job_id # local dev no-op
|
|
17
|
+
|
|
18
|
+
self._bridge.call("/payment/create-job", {
|
|
19
|
+
"job_id": job_id,
|
|
20
|
+
"provider_id": provider_id,
|
|
21
|
+
"amount": str(token_budget),
|
|
22
|
+
})
|
|
23
|
+
return job_id
|
|
24
|
+
|
|
25
|
+
def complete_job(self, job_id: str, attestation_hash: str, token_count: int):
|
|
26
|
+
"""Release escrow payment to provider."""
|
|
27
|
+
if self._bridge is None:
|
|
28
|
+
return
|
|
29
|
+
self._bridge.call("/payment/complete-job", {
|
|
30
|
+
"job_id": job_id,
|
|
31
|
+
"attestation_hash": attestation_hash,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
def dispute_job(self, job_id: str):
|
|
35
|
+
"""Trigger refund — called automatically on attestation failure."""
|
|
36
|
+
if self._bridge is None:
|
|
37
|
+
return
|
|
38
|
+
self._bridge.call("/payment/dispute-job", {"job_id": job_id})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider discovery and selection.
|
|
3
|
+
- No registry_contract: local dev stub (localhost:8080)
|
|
4
|
+
- With registry_contract: queries Midnight indexer GraphQL for on-chain providers
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
INDEXER_URL = "https://indexer.preprod.midnight.network/api/v3/graphql"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Provider:
|
|
15
|
+
id: str
|
|
16
|
+
endpoint: str
|
|
17
|
+
pubkey: str
|
|
18
|
+
model: str
|
|
19
|
+
price_per_token: float
|
|
20
|
+
reputation: float
|
|
21
|
+
stake: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def select_provider(
|
|
25
|
+
model: str,
|
|
26
|
+
max_price: float | None = None,
|
|
27
|
+
min_reputation: float = 0.0,
|
|
28
|
+
registry_contract: str | None = None,
|
|
29
|
+
) -> Provider:
|
|
30
|
+
"""Pick the best available provider. Ranked by reputation desc, price asc."""
|
|
31
|
+
providers = _get_providers(registry_contract)
|
|
32
|
+
|
|
33
|
+
candidates = [
|
|
34
|
+
p for p in providers
|
|
35
|
+
if p.model == model
|
|
36
|
+
and (max_price is None or p.price_per_token <= max_price)
|
|
37
|
+
and p.reputation >= min_reputation
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
if not candidates:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
f"No providers available for model '{model}' "
|
|
43
|
+
f"within price/reputation constraints."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
candidates.sort(key=lambda p: (-p.reputation, p.price_per_token))
|
|
47
|
+
return candidates[0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fetch_pubkey(provider: Provider) -> str:
|
|
51
|
+
"""Fetch current TEE pubkey from provider (may rotate on restart)."""
|
|
52
|
+
resp = requests.get(f"{provider.endpoint}/pubkey", timeout=10)
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
return resp.json()["pubkey"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_providers(registry_contract: str | None) -> list[Provider]:
|
|
58
|
+
if registry_contract is None:
|
|
59
|
+
return _get_providers_stub()
|
|
60
|
+
return _get_providers_from_chain(registry_contract)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_providers_from_chain(registry_contract: str) -> list[Provider]:
|
|
64
|
+
"""
|
|
65
|
+
Query Midnight indexer GraphQL for active providers in the ProviderRegistry.
|
|
66
|
+
The contract's export ledger maps are publicly readable.
|
|
67
|
+
"""
|
|
68
|
+
query = """
|
|
69
|
+
query GetContractState($address: String!) {
|
|
70
|
+
contract(address: $address) {
|
|
71
|
+
state {
|
|
72
|
+
... on ContractState {
|
|
73
|
+
ledger {
|
|
74
|
+
... on ZkaiProviderRegistryLedger {
|
|
75
|
+
provider_active { entries { key value } }
|
|
76
|
+
provider_endpoint { entries { key value } }
|
|
77
|
+
provider_model { entries { key value } }
|
|
78
|
+
provider_price { entries { key value } }
|
|
79
|
+
provider_reputation { entries { key value } }
|
|
80
|
+
provider_pubkey { entries { key value } }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
resp = requests.post(
|
|
90
|
+
INDEXER_URL,
|
|
91
|
+
json={"query": query, "variables": {"address": registry_contract}},
|
|
92
|
+
timeout=15,
|
|
93
|
+
)
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
data = resp.json()
|
|
96
|
+
ledger = data.get("data", {}).get("contract", {}).get("state", {}).get("ledger", {})
|
|
97
|
+
|
|
98
|
+
if not ledger:
|
|
99
|
+
# Indexer schema may differ — fall back to stub with a warning
|
|
100
|
+
print("Warning: Could not parse on-chain providers, falling back to stub")
|
|
101
|
+
return _get_providers_stub()
|
|
102
|
+
|
|
103
|
+
active_entries = {e["key"]: e["value"] for e in ledger.get("provider_active", {}).get("entries", [])}
|
|
104
|
+
endpoint_entries = {e["key"]: e["value"] for e in ledger.get("provider_endpoint", {}).get("entries", [])}
|
|
105
|
+
model_entries = {e["key"]: e["value"] for e in ledger.get("provider_model", {}).get("entries", [])}
|
|
106
|
+
price_entries = {e["key"]: e["value"] for e in ledger.get("provider_price", {}).get("entries", [])}
|
|
107
|
+
rep_entries = {e["key"]: e["value"] for e in ledger.get("provider_reputation", {}).get("entries", [])}
|
|
108
|
+
pubkey_entries = {e["key"]: e["value"] for e in ledger.get("provider_pubkey", {}).get("entries", [])}
|
|
109
|
+
|
|
110
|
+
providers = []
|
|
111
|
+
for pid, active in active_entries.items():
|
|
112
|
+
if not active:
|
|
113
|
+
continue
|
|
114
|
+
providers.append(Provider(
|
|
115
|
+
id=pid,
|
|
116
|
+
endpoint=endpoint_entries.get(pid, ""),
|
|
117
|
+
pubkey=pubkey_entries.get(pid, ""),
|
|
118
|
+
model=model_entries.get(pid, ""),
|
|
119
|
+
price_per_token=float(price_entries.get(pid, 0)),
|
|
120
|
+
reputation=float(rep_entries.get(pid, 500000)) / 1_000_000,
|
|
121
|
+
stake=0.0,
|
|
122
|
+
))
|
|
123
|
+
return providers if providers else _get_providers_stub()
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"Warning: Failed to fetch on-chain providers ({e}), falling back to stub")
|
|
127
|
+
return _get_providers_stub()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _get_providers_stub() -> list[Provider]:
|
|
131
|
+
"""Local dev stub — points to localhost enclave."""
|
|
132
|
+
return [
|
|
133
|
+
Provider(
|
|
134
|
+
id="local-dev",
|
|
135
|
+
endpoint="http://localhost:8080",
|
|
136
|
+
pubkey="",
|
|
137
|
+
model="qwen2.5-1.5b",
|
|
138
|
+
price_per_token=0.0,
|
|
139
|
+
reputation=1.0,
|
|
140
|
+
stake=0.0,
|
|
141
|
+
)
|
|
142
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zkai
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Private, verifiable AI inference on 0G chain — drop-in OpenAI-compatible SDK
|
|
5
|
+
Author-email: ZKai <team@zkai.network>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://zkai-ether-og.vercel.app
|
|
8
|
+
Project-URL: Repository, https://github.com/skyyycodes/zkai-eth
|
|
9
|
+
Project-URL: Issues, https://github.com/skyyycodes/zkai-eth/issues
|
|
10
|
+
Keywords: ai,inference,openai,llm,tee,tdx,attestation,blockchain,0g,verifiable,privacy,encryption
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: cryptography>=43.0
|
|
24
|
+
Requires-Dist: requests>=2.32
|
|
25
|
+
Requires-Dist: pydantic>=2.9
|
|
26
|
+
Provides-Extra: langchain
|
|
27
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
28
|
+
|
|
29
|
+
# zkai
|
|
30
|
+
|
|
31
|
+
OpenAI-compatible Python SDK for [ZKai](https://zkai-ether-og.vercel.app) — private, verifiable AI inference on 0G chain.
|
|
32
|
+
|
|
33
|
+
ZKai sends your prompt through a Trusted Execution Environment (Intel TDX), encrypts it client-side so even the gateway cannot read it, and anchors a SHA-256 attestation of every inference on the 0G chain. You get back a normal OpenAI-style response plus an on-chain receipt.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install zkai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Optional LangChain adapter:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "zkai[langchain]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from zkai import ZKai
|
|
51
|
+
|
|
52
|
+
client = ZKai(api_key="zkai-...") # get one at https://zkai-ether-og.vercel.app
|
|
53
|
+
|
|
54
|
+
response = client.chat.completions.create(
|
|
55
|
+
model="qwen2.5:1.5b",
|
|
56
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
print(response.choices[0].message.content)
|
|
60
|
+
print("Attestation hash:", response.attestation_hash)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The response is decrypted locally — the gateway sees only ciphertext.
|
|
64
|
+
The attestation hash is anchored on 0G mainnet and can be verified independently.
|
|
65
|
+
|
|
66
|
+
## How it works (gateway mode, default)
|
|
67
|
+
|
|
68
|
+
1. The SDK fetches the enclave's X25519 public key from the gateway.
|
|
69
|
+
2. It encrypts your prompt locally with ECDH + ChaCha20-Poly1305.
|
|
70
|
+
3. It sends only the ciphertext to the gateway.
|
|
71
|
+
4. The gateway routes the opaque blob to a TDX-sealed enclave running the requested model.
|
|
72
|
+
5. The enclave decrypts inside sealed memory, runs the model, encrypts the response, and emits a SHA-256 attestation hash.
|
|
73
|
+
6. The attestation lands on the on-chain `AttestationRegistry` contract.
|
|
74
|
+
7. The SDK decrypts the response locally and returns it to you.
|
|
75
|
+
|
|
76
|
+
End-to-end, the gateway never sees plaintext.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
ZKai(
|
|
82
|
+
api_key="zkai-...", # issued from the dashboard
|
|
83
|
+
base_url="https://zkai-ether-og.vercel.app", # default
|
|
84
|
+
encrypted=True, # default; set False to use the legacy plaintext path
|
|
85
|
+
provider_endpoint=None, # set to bypass gateway and hit a provider directly
|
|
86
|
+
skip_attestation=False, # only for development; do not disable in prod
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Argument | Type | Default | Description |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `api_key` | `str | None` | `None` | Sent as `X-API-Key`. Required for the hosted gateway. |
|
|
93
|
+
| `base_url` | `str | None` | `https://zkai-ether-og.vercel.app` | Override to point at a self-hosted gateway. |
|
|
94
|
+
| `encrypted` | `bool` | `True` | When `True`, prompts are encrypted client-side. Set `False` for legacy clients. |
|
|
95
|
+
| `provider_endpoint` | `str | None` | `None` | When set, bypasses the gateway and talks directly to a provider's `/infer` endpoint. |
|
|
96
|
+
| `skip_attestation` | `bool` | `False` | Disables attestation verification. Useful only for dev loops. |
|
|
97
|
+
|
|
98
|
+
## OpenAI compatibility
|
|
99
|
+
|
|
100
|
+
The response object mirrors OpenAI's chat completion shape:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
response.id
|
|
104
|
+
response.model
|
|
105
|
+
response.choices[0].message.content
|
|
106
|
+
response.choices[0].finish_reason
|
|
107
|
+
response.usage.prompt_tokens
|
|
108
|
+
response.usage.completion_tokens
|
|
109
|
+
|
|
110
|
+
# ZKai-specific:
|
|
111
|
+
response.attestation_hash # on-chain commitment
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Drop-in replacement for most OpenAI SDK call sites — just swap the client class.
|
|
115
|
+
|
|
116
|
+
## On-chain components (0G mainnet, chain ID 16661)
|
|
117
|
+
|
|
118
|
+
| Contract | Address |
|
|
119
|
+
|---|---|
|
|
120
|
+
| ProviderRegistry | `0x6D400F5D1DcCaA3e98E3dE17322aA23DE38bAC99` |
|
|
121
|
+
| PaymentEscrow | `0xb2C7c0F7a4C2877319E8Ed1Fae0bf3C705b6Fc4C` |
|
|
122
|
+
| AttestationRegistry | `0x8c8Ae0A113084268D181fd1cf23d611DC2EAa2B2` |
|
|
123
|
+
|
|
124
|
+
Verify any attestation hash on the [0G explorer](https://chainscan.0g.ai).
|
|
125
|
+
|
|
126
|
+
## Running your own provider
|
|
127
|
+
|
|
128
|
+
See the companion [`zkai-cli`](https://pypi.org/project/zkai-cli/) package:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install zkai-cli
|
|
132
|
+
zkai init && zkai start && zkai register
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Links
|
|
136
|
+
|
|
137
|
+
- Dashboard — https://zkai-ether-og.vercel.app
|
|
138
|
+
- Repository — https://github.com/skyyycodes/zkai-eth
|
|
139
|
+
- Provider CLI — https://pypi.org/project/zkai-cli/
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
zkai/__init__.py
|
|
4
|
+
zkai/attestation.py
|
|
5
|
+
zkai/bridge.py
|
|
6
|
+
zkai/client.py
|
|
7
|
+
zkai/crypto.py
|
|
8
|
+
zkai/langchain.py
|
|
9
|
+
zkai/payment.py
|
|
10
|
+
zkai/provider.py
|
|
11
|
+
zkai.egg-info/PKG-INFO
|
|
12
|
+
zkai.egg-info/SOURCES.txt
|
|
13
|
+
zkai.egg-info/dependency_links.txt
|
|
14
|
+
zkai.egg-info/requires.txt
|
|
15
|
+
zkai.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zkai
|