ainize 0.1.0__py3-none-any.whl
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.
- ainize/__init__.py +15 -0
- ainize/_connect.py +114 -0
- ainize-0.1.0.dist-info/METADATA +58 -0
- ainize-0.1.0.dist-info/RECORD +6 -0
- ainize-0.1.0.dist-info/WHEEL +5 -0
- ainize-0.1.0.dist-info/top_level.txt +1 -0
ainize/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Call an Ainize node with the OpenAI code you already have.
|
|
2
|
+
|
|
3
|
+
import ainize
|
|
4
|
+
|
|
5
|
+
client = ainize.connect("https://node.example", private_key=...)
|
|
6
|
+
client.chat.completions.create(model="qwen3.8-flash-next", messages=[...])
|
|
7
|
+
|
|
8
|
+
`connect()` returns a real `openai.OpenAI`. What you pay with is a deposit: send AIN or sAIN to the node and your
|
|
9
|
+
share of its throughput is your share of what everyone asking at that moment has deposited.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ._connect import await_deposit, connect, deposit_address
|
|
13
|
+
|
|
14
|
+
__all__ = ["connect", "deposit_address", "await_deposit"]
|
|
15
|
+
__version__ = "0.1.0"
|
ainize/_connect.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Point OpenAI at an Ainize node.
|
|
2
|
+
|
|
3
|
+
This library does one thing on top of `openai`: it proves which address is calling and gets a key back. It does
|
|
4
|
+
not wrap the client, subclass it, or re-export a narrowed version of it — it returns the real `openai.OpenAI`,
|
|
5
|
+
because the whole promise is that nothing after that line is different. A wrapper would have to grow a method
|
|
6
|
+
every time OpenAI's client does, and would be a second place for bugs to live.
|
|
7
|
+
|
|
8
|
+
It also never signs a transfer. `deposit_address()` says where to send AIN and `await_deposit()` waits for the
|
|
9
|
+
node to notice; moving funds stays with the wallet the person already trusts. A library that signs transfers is a
|
|
10
|
+
much larger thing to hand your private key to than one that signs a login, and the difference is not something a
|
|
11
|
+
caller can see from the import line.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
import openai
|
|
20
|
+
from eth_account import Account
|
|
21
|
+
from eth_account.messages import encode_defunct
|
|
22
|
+
|
|
23
|
+
__all__ = ["connect", "deposit_address", "await_deposit"]
|
|
24
|
+
|
|
25
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def connect(
|
|
29
|
+
node_url: str,
|
|
30
|
+
*,
|
|
31
|
+
private_key: str | None = None,
|
|
32
|
+
api_key: str | None = None,
|
|
33
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
34
|
+
) -> openai.OpenAI:
|
|
35
|
+
"""Return an `openai.OpenAI` pointed at `node_url`, signing in if it has to.
|
|
36
|
+
|
|
37
|
+
Pass `private_key` to sign in and be issued a key, or `api_key` to reuse one you already hold. The returned
|
|
38
|
+
client is the genuine article: every call, parameter and exception is OpenAI's.
|
|
39
|
+
"""
|
|
40
|
+
node_url = node_url.rstrip("/")
|
|
41
|
+
if api_key is None:
|
|
42
|
+
if private_key is None:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"connect() needs either private_key (to sign in and be issued one) or api_key (one you already hold)"
|
|
45
|
+
)
|
|
46
|
+
api_key = _sign_in(node_url, private_key, timeout=timeout)
|
|
47
|
+
return openai.OpenAI(base_url=f"{node_url}/v1", api_key=api_key)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _sign_in(node_url: str, private_key: str, *, timeout: float) -> str:
|
|
51
|
+
"""Prove the address once and leave with a key.
|
|
52
|
+
|
|
53
|
+
The message signed is the one the node issued, verbatim — it is not rebuilt here. A rebuilt message is a
|
|
54
|
+
message the client had a hand in, and the node verifies against the bytes it handed out.
|
|
55
|
+
"""
|
|
56
|
+
account = Account.from_key(private_key)
|
|
57
|
+
with httpx.Client(timeout=timeout) as http:
|
|
58
|
+
challenge = http.post(
|
|
59
|
+
f"{node_url}/v1/auth/nonce",
|
|
60
|
+
json={"address": account.address, "scheme": "eip191"},
|
|
61
|
+
)
|
|
62
|
+
challenge.raise_for_status()
|
|
63
|
+
issued_challenge = challenge.json()
|
|
64
|
+
|
|
65
|
+
signature = account.sign_message(
|
|
66
|
+
encode_defunct(text=issued_challenge["message"])
|
|
67
|
+
).signature.hex()
|
|
68
|
+
if not signature.startswith("0x"):
|
|
69
|
+
signature = f"0x{signature}"
|
|
70
|
+
|
|
71
|
+
token = http.post(
|
|
72
|
+
f"{node_url}/v1/auth/token",
|
|
73
|
+
json={"nonce": issued_challenge["nonce"], "signature": signature},
|
|
74
|
+
)
|
|
75
|
+
token.raise_for_status()
|
|
76
|
+
return token.json()["api_key"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def deposit_address(node_url: str, *, timeout: float = _DEFAULT_TIMEOUT) -> str:
|
|
80
|
+
"""Where to send AIN or sAIN to buy a share of this node's throughput."""
|
|
81
|
+
with httpx.Client(timeout=timeout) as http:
|
|
82
|
+
response = http.get(f"{node_url.rstrip('/')}/v1/account/deposit-address")
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
return response.json()["address"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def await_deposit(
|
|
88
|
+
node_url: str,
|
|
89
|
+
tx_hash: str,
|
|
90
|
+
*,
|
|
91
|
+
api_key: str,
|
|
92
|
+
timeout: float = 600.0,
|
|
93
|
+
poll_seconds: float = 5.0,
|
|
94
|
+
) -> dict:
|
|
95
|
+
"""Block until the node has credited `tx_hash`.
|
|
96
|
+
|
|
97
|
+
The node is the authority on when a transfer counts: it waits for its own confirmation depth before crediting,
|
|
98
|
+
so a transaction that a block explorer already shows is not yet a share here. Polling is honest about that;
|
|
99
|
+
guessing from the chain would not be.
|
|
100
|
+
"""
|
|
101
|
+
deadline = time.monotonic() + timeout
|
|
102
|
+
headers = {"authorization": f"Bearer {api_key}"}
|
|
103
|
+
with httpx.Client(timeout=_DEFAULT_TIMEOUT) as http:
|
|
104
|
+
while True:
|
|
105
|
+
response = http.get(
|
|
106
|
+
f"{node_url.rstrip('/')}/v1/account/deposits/{tx_hash}", headers=headers
|
|
107
|
+
)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
seen = response.json()
|
|
110
|
+
if seen.get("credited"):
|
|
111
|
+
return seen
|
|
112
|
+
if time.monotonic() > deadline:
|
|
113
|
+
raise TimeoutError(f"{tx_hash} was not credited within {timeout}s")
|
|
114
|
+
time.sleep(poll_seconds)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ainize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Call an Ainize node with ordinary OpenAI code; pay for throughput by staking AIN
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ainblockchain/ainize
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: openai>=1.40
|
|
10
|
+
Requires-Dist: eth-account>=0.13
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# ainize (Python)
|
|
16
|
+
|
|
17
|
+
Call an Ainize node with the OpenAI code you already have.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import ainize
|
|
21
|
+
|
|
22
|
+
client = ainize.connect("https://node.example", private_key="0x…")
|
|
23
|
+
|
|
24
|
+
client.chat.completions.create(
|
|
25
|
+
model="qwen3.8-flash-next",
|
|
26
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`connect()` signs one challenge to prove your address, and returns a real `openai.OpenAI` with `base_url` and
|
|
31
|
+
`api_key` already set. Everything after that line is OpenAI's — its methods, its parameters, its exceptions.
|
|
32
|
+
|
|
33
|
+
Already hold a key? `ainize.connect(url, api_key="ainize-sk-…")` skips the signing.
|
|
34
|
+
|
|
35
|
+
## What you pay with
|
|
36
|
+
|
|
37
|
+
A deposit, not a per-token charge. Send AIN or sAIN to the node; the operator holds it staked, and your share of
|
|
38
|
+
the node's throughput is your share of what everyone asking at that moment has deposited. An idle deposit costs
|
|
39
|
+
nobody anything, and the principal is not consumed.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
ainize.deposit_address("https://node.example") # where to send
|
|
43
|
+
ainize.await_deposit(url, tx_hash, api_key=client.api_key) # wait until the node has credited it
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The library never signs a transfer. It tells you where to send and waits for the node to notice; moving funds
|
|
47
|
+
stays with the wallet you already trust.
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
They drive a real node and a stub model — mocking the transport would test our idea of the node's replies rather
|
|
52
|
+
than the node's replies, and the shapes are exactly where compatibility breaks. Run them from this directory, so
|
|
53
|
+
the package on disk is the one imported:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m venv .venv && .venv/bin/pip install openai eth-account httpx pytest
|
|
57
|
+
.venv/bin/python -m pytest
|
|
58
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ainize/__init__.py,sha256=5TcxmtGyTU8G46WiflosO70D-gaJTOLGooita5z4kJY,585
|
|
2
|
+
ainize/_connect.py,sha256=PAz1TBeNxhXE7QVAotqUH3_Yx7QCkDV_vIFaOnRGX_k,4486
|
|
3
|
+
ainize-0.1.0.dist-info/METADATA,sha256=3KdfnzWxQAC22Uog9vLbYD3N29mBocltMBKSx5okg9Q,2072
|
|
4
|
+
ainize-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
ainize-0.1.0.dist-info/top_level.txt,sha256=DU7uxqX0J3XVwvbCIvPrei70QwI6StvoVg04UalgYo0,7
|
|
6
|
+
ainize-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ainize
|