ainize 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.
- ainize-0.1.0/PKG-INFO +58 -0
- ainize-0.1.0/README.md +44 -0
- ainize-0.1.0/ainize/__init__.py +15 -0
- ainize-0.1.0/ainize/_connect.py +114 -0
- ainize-0.1.0/ainize.egg-info/PKG-INFO +58 -0
- ainize-0.1.0/ainize.egg-info/SOURCES.txt +10 -0
- ainize-0.1.0/ainize.egg-info/dependency_links.txt +1 -0
- ainize-0.1.0/ainize.egg-info/requires.txt +6 -0
- ainize-0.1.0/ainize.egg-info/top_level.txt +1 -0
- ainize-0.1.0/pyproject.toml +28 -0
- ainize-0.1.0/setup.cfg +4 -0
- ainize-0.1.0/tests/test_connect.py +77 -0
ainize-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
ainize-0.1.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ainize (Python)
|
|
2
|
+
|
|
3
|
+
Call an Ainize node with the OpenAI code you already have.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import ainize
|
|
7
|
+
|
|
8
|
+
client = ainize.connect("https://node.example", private_key="0x…")
|
|
9
|
+
|
|
10
|
+
client.chat.completions.create(
|
|
11
|
+
model="qwen3.8-flash-next",
|
|
12
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`connect()` signs one challenge to prove your address, and returns a real `openai.OpenAI` with `base_url` and
|
|
17
|
+
`api_key` already set. Everything after that line is OpenAI's — its methods, its parameters, its exceptions.
|
|
18
|
+
|
|
19
|
+
Already hold a key? `ainize.connect(url, api_key="ainize-sk-…")` skips the signing.
|
|
20
|
+
|
|
21
|
+
## What you pay with
|
|
22
|
+
|
|
23
|
+
A deposit, not a per-token charge. Send AIN or sAIN to the node; the operator holds it staked, and your share of
|
|
24
|
+
the node's throughput is your share of what everyone asking at that moment has deposited. An idle deposit costs
|
|
25
|
+
nobody anything, and the principal is not consumed.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
ainize.deposit_address("https://node.example") # where to send
|
|
29
|
+
ainize.await_deposit(url, tx_hash, api_key=client.api_key) # wait until the node has credited it
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The library never signs a transfer. It tells you where to send and waits for the node to notice; moving funds
|
|
33
|
+
stays with the wallet you already trust.
|
|
34
|
+
|
|
35
|
+
## Tests
|
|
36
|
+
|
|
37
|
+
They drive a real node and a stub model — mocking the transport would test our idea of the node's replies rather
|
|
38
|
+
than the node's replies, and the shapes are exactly where compatibility breaks. Run them from this directory, so
|
|
39
|
+
the package on disk is the one imported:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m venv .venv && .venv/bin/pip install openai eth-account httpx pytest
|
|
43
|
+
.venv/bin/python -m pytest
|
|
44
|
+
```
|
|
@@ -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"
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ainize
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ainize"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Call an Ainize node with ordinary OpenAI code; pay for throughput by staking AIN"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"openai>=1.40",
|
|
14
|
+
"eth-account>=0.13",
|
|
15
|
+
"httpx>=0.27",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/ainblockchain/ainize"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=8"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
include = ["ainize*"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
ainize-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""What `import ainize` promises: after one line, it is just OpenAI.
|
|
2
|
+
|
|
3
|
+
These run the genuine `openai` client against a real node. A mocked transport would test our idea of the node's
|
|
4
|
+
replies rather than the node's replies, and the shapes are exactly where the promise breaks.
|
|
5
|
+
|
|
6
|
+
cd sdk/python && .venv/bin/python -m pytest
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import openai
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
import ainize
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_connect_returns_a_real_openai_client(node_url, wallet_key):
|
|
18
|
+
client = ainize.connect(node_url, private_key=wallet_key)
|
|
19
|
+
assert isinstance(client, openai.OpenAI)
|
|
20
|
+
assert str(client.base_url).rstrip("/").endswith("/v1")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_an_existing_key_skips_signing(node_url, issued_key):
|
|
24
|
+
client = ainize.connect(node_url, api_key=issued_key)
|
|
25
|
+
assert client.api_key == issued_key
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_neither_key_nor_private_key_is_an_error_naming_both(node_url):
|
|
29
|
+
with pytest.raises(ValueError) as raised:
|
|
30
|
+
ainize.connect(node_url)
|
|
31
|
+
assert "private_key" in str(raised.value)
|
|
32
|
+
assert "api_key" in str(raised.value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_models_are_listed_through_the_stock_client(node_url, issued_key):
|
|
36
|
+
client = ainize.connect(node_url, api_key=issued_key)
|
|
37
|
+
assert "qwen3.8-flash-next" in [m.id for m in client.models.list().data]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_chat_completion_round_trips(node_url, issued_key):
|
|
41
|
+
client = ainize.connect(node_url, api_key=issued_key)
|
|
42
|
+
out = client.chat.completions.create(
|
|
43
|
+
model="qwen3.8-flash-next", messages=[{"role": "user", "content": "ping"}]
|
|
44
|
+
)
|
|
45
|
+
assert out.choices[0].message.content == "pong"
|
|
46
|
+
assert out.id.startswith("chatcmpl-")
|
|
47
|
+
assert out.choices[0].finish_reason == "stop"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_streaming_yields_chunks(node_url, issued_key):
|
|
51
|
+
client = ainize.connect(node_url, api_key=issued_key)
|
|
52
|
+
chunks = list(
|
|
53
|
+
client.chat.completions.create(
|
|
54
|
+
model="qwen3.8-flash-next",
|
|
55
|
+
messages=[{"role": "user", "content": "ping"}],
|
|
56
|
+
stream=True,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
assert chunks, "the stream ended without a single chunk"
|
|
60
|
+
assert chunks[0].object == "chat.completion.chunk"
|
|
61
|
+
assert "".join(c.choices[0].delta.content or "" for c in chunks) == "pong"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_a_bad_key_raises_openais_own_error_type(node_url):
|
|
65
|
+
client = ainize.connect(node_url, api_key="ainize-sk-not-a-real-key")
|
|
66
|
+
with pytest.raises(openai.AuthenticationError):
|
|
67
|
+
client.chat.completions.create(
|
|
68
|
+
model="qwen3.8-flash-next", messages=[{"role": "user", "content": "ping"}]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_an_unknown_model_raises_not_found(node_url, issued_key):
|
|
73
|
+
client = ainize.connect(node_url, api_key=issued_key)
|
|
74
|
+
with pytest.raises(openai.NotFoundError):
|
|
75
|
+
client.chat.completions.create(
|
|
76
|
+
model="gpt-4", messages=[{"role": "user", "content": "ping"}]
|
|
77
|
+
)
|