grid-sdk 0.1.1__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.
- grid_sdk-0.1.1/LICENSE +21 -0
- grid_sdk-0.1.1/PKG-INFO +155 -0
- grid_sdk-0.1.1/README.md +134 -0
- grid_sdk-0.1.1/pyproject.toml +31 -0
- grid_sdk-0.1.1/setup.cfg +4 -0
- grid_sdk-0.1.1/src/grid_sdk/__init__.py +29 -0
- grid_sdk-0.1.1/src/grid_sdk/client.py +78 -0
- grid_sdk-0.1.1/src/grid_sdk/grid.py +261 -0
- grid_sdk-0.1.1/src/grid_sdk.egg-info/PKG-INFO +155 -0
- grid_sdk-0.1.1/src/grid_sdk.egg-info/SOURCES.txt +13 -0
- grid_sdk-0.1.1/src/grid_sdk.egg-info/dependency_links.txt +1 -0
- grid_sdk-0.1.1/src/grid_sdk.egg-info/requires.txt +7 -0
- grid_sdk-0.1.1/src/grid_sdk.egg-info/top_level.txt +1 -0
- grid_sdk-0.1.1/tests/test_client.py +84 -0
- grid_sdk-0.1.1/tests/test_grid.py +93 -0
grid_sdk-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Power Grid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
grid_sdk-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grid-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for AI Power Grid open-model, OpenAI-compatible inference.
|
|
5
|
+
Author: AI Power Grid
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://aipowergrid.io
|
|
8
|
+
Project-URL: Documentation, https://aipowergrid.io/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/AIPowerGrid/grid-sdk-python
|
|
10
|
+
Keywords: ai,llm,inference,openai,decentralized,aipg
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: openai>=1.0.0
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
18
|
+
Requires-Dist: respx; extra == "test"
|
|
19
|
+
Requires-Dist: httpx; extra == "test"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# AI Power Grid — Python SDK
|
|
23
|
+
|
|
24
|
+
Open-model, OpenAI-compatible inference across community-operated GPUs.
|
|
25
|
+
|
|
26
|
+
> **Release status:** `grid-sdk` is staged for its initial PyPI publication.
|
|
27
|
+
> The install command below becomes available when the first verified release
|
|
28
|
+
> is published; until then, use the OpenAI SDK with the Grid base URL shown below.
|
|
29
|
+
|
|
30
|
+
The Grid API speaks the OpenAI protocol, so this SDK is a thin layer over the
|
|
31
|
+
official `openai` package: it points at the Grid, reads your key from the
|
|
32
|
+
environment, and adds Grid-specific conveniences. Everything you know from the
|
|
33
|
+
OpenAI SDK works unchanged.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install grid-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Sign in at the [Grid developer console](https://console.aipowergrid.io/dashboard/api-key),
|
|
44
|
+
create an API key, then set it as `AIPG_API_KEY`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from grid_sdk import Grid
|
|
48
|
+
|
|
49
|
+
client = Grid() # reads AIPG_API_KEY from the environment
|
|
50
|
+
|
|
51
|
+
stream = client.chat.completions.create(
|
|
52
|
+
model="gpt-oss-120b",
|
|
53
|
+
messages=[{"role": "user", "content": "Explain AI Power Grid in one line."}],
|
|
54
|
+
stream=True,
|
|
55
|
+
)
|
|
56
|
+
for chunk in stream:
|
|
57
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## See what's online
|
|
61
|
+
|
|
62
|
+
The Grid's available models change as workers connect and disconnect. Don't
|
|
63
|
+
hardcode a model blindly — ask which ones are servable right now:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = Grid()
|
|
67
|
+
print(client.online_models())
|
|
68
|
+
# ['gpt-oss-120b', 'qwen3-27b', ...]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
An empty list means no workers are connected — requests will 503 until one is.
|
|
72
|
+
|
|
73
|
+
## Credits
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
credits = client.grid.credits()
|
|
77
|
+
print(credits["total_spendable_usd"])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This is Core's canonical promotional, daily-free, and purchased account view.
|
|
81
|
+
The SDK does not maintain a local free-use counter.
|
|
82
|
+
|
|
83
|
+
## Async
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import asyncio
|
|
87
|
+
from grid_sdk import AsyncGrid
|
|
88
|
+
|
|
89
|
+
async def main():
|
|
90
|
+
client = AsyncGrid()
|
|
91
|
+
resp = await client.chat.completions.create(
|
|
92
|
+
model="gpt-oss-120b",
|
|
93
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
94
|
+
)
|
|
95
|
+
print(resp.choices[0].message.content)
|
|
96
|
+
|
|
97
|
+
asyncio.run(main())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Video, img2img, ControlNet, LoRAs — the full Grid
|
|
101
|
+
|
|
102
|
+
The OpenAI-compatible surface covers text and basic txt2img. For the full media
|
|
103
|
+
parameter surface, use `client.grid`, which calls the synchronous `/v1` image
|
|
104
|
+
and video endpoints:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
client = Grid()
|
|
108
|
+
|
|
109
|
+
# Video:
|
|
110
|
+
result = client.grid.video(
|
|
111
|
+
prompt="a timelapse of a city at night",
|
|
112
|
+
model="LTX-2.3",
|
|
113
|
+
width=768, height=512, seconds=4, fps=24,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# img2img / ControlNet / LoRAs — anything the workers support:
|
|
117
|
+
result = client.grid.image(
|
|
118
|
+
prompt="make it watercolor",
|
|
119
|
+
models=["FLUX.2 Klein 4B FP8"],
|
|
120
|
+
source_image="<base64>",
|
|
121
|
+
loras=[{"name": "watercolor", "model": 1.0}],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Or full control with a raw payload:
|
|
125
|
+
result = client.grid.generate({"prompt": "...", "models": [...], "params": {...}})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`client.grid` waits for the synchronous Grid response and returns the finished
|
|
129
|
+
OpenAI-shaped result. Use `timeout` for long media jobs; there is no SDK
|
|
130
|
+
submit/poll mode on this `/v1` client.
|
|
131
|
+
|
|
132
|
+
## It's just OpenAI underneath
|
|
133
|
+
|
|
134
|
+
`Grid` subclasses `openai.OpenAI`, so anything the OpenAI SDK does — images,
|
|
135
|
+
tool calling, structured output, the full `.chat`/`.images`/`.models` surface —
|
|
136
|
+
works here too. You can also point existing OpenAI code at the Grid by setting
|
|
137
|
+
`base_url="https://api.aipowergrid.io/v1"` if you'd rather not switch packages.
|
|
138
|
+
|
|
139
|
+
## Config
|
|
140
|
+
|
|
141
|
+
| | |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `Grid(api_key=...)` | Explicit key (overrides env) |
|
|
144
|
+
| `AIPG_API_KEY` | Env var read when no key is passed |
|
|
145
|
+
| `Grid(base_url=...)` | Override the endpoint (default `https://api.aipowergrid.io/v1`) |
|
|
146
|
+
|
|
147
|
+
## Links
|
|
148
|
+
|
|
149
|
+
- [Docs](https://aipowergrid.io/docs)
|
|
150
|
+
- [Get an API key](https://console.aipowergrid.io/dashboard/api-key)
|
|
151
|
+
- [Discord](https://discord.gg/W9D8j6HCtC)
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
grid_sdk-0.1.1/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# AI Power Grid — Python SDK
|
|
2
|
+
|
|
3
|
+
Open-model, OpenAI-compatible inference across community-operated GPUs.
|
|
4
|
+
|
|
5
|
+
> **Release status:** `grid-sdk` is staged for its initial PyPI publication.
|
|
6
|
+
> The install command below becomes available when the first verified release
|
|
7
|
+
> is published; until then, use the OpenAI SDK with the Grid base URL shown below.
|
|
8
|
+
|
|
9
|
+
The Grid API speaks the OpenAI protocol, so this SDK is a thin layer over the
|
|
10
|
+
official `openai` package: it points at the Grid, reads your key from the
|
|
11
|
+
environment, and adds Grid-specific conveniences. Everything you know from the
|
|
12
|
+
OpenAI SDK works unchanged.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install grid-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Sign in at the [Grid developer console](https://console.aipowergrid.io/dashboard/api-key),
|
|
23
|
+
create an API key, then set it as `AIPG_API_KEY`:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from grid_sdk import Grid
|
|
27
|
+
|
|
28
|
+
client = Grid() # reads AIPG_API_KEY from the environment
|
|
29
|
+
|
|
30
|
+
stream = client.chat.completions.create(
|
|
31
|
+
model="gpt-oss-120b",
|
|
32
|
+
messages=[{"role": "user", "content": "Explain AI Power Grid in one line."}],
|
|
33
|
+
stream=True,
|
|
34
|
+
)
|
|
35
|
+
for chunk in stream:
|
|
36
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## See what's online
|
|
40
|
+
|
|
41
|
+
The Grid's available models change as workers connect and disconnect. Don't
|
|
42
|
+
hardcode a model blindly — ask which ones are servable right now:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
client = Grid()
|
|
46
|
+
print(client.online_models())
|
|
47
|
+
# ['gpt-oss-120b', 'qwen3-27b', ...]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
An empty list means no workers are connected — requests will 503 until one is.
|
|
51
|
+
|
|
52
|
+
## Credits
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
credits = client.grid.credits()
|
|
56
|
+
print(credits["total_spendable_usd"])
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is Core's canonical promotional, daily-free, and purchased account view.
|
|
60
|
+
The SDK does not maintain a local free-use counter.
|
|
61
|
+
|
|
62
|
+
## Async
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
from grid_sdk import AsyncGrid
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
client = AsyncGrid()
|
|
70
|
+
resp = await client.chat.completions.create(
|
|
71
|
+
model="gpt-oss-120b",
|
|
72
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
73
|
+
)
|
|
74
|
+
print(resp.choices[0].message.content)
|
|
75
|
+
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Video, img2img, ControlNet, LoRAs — the full Grid
|
|
80
|
+
|
|
81
|
+
The OpenAI-compatible surface covers text and basic txt2img. For the full media
|
|
82
|
+
parameter surface, use `client.grid`, which calls the synchronous `/v1` image
|
|
83
|
+
and video endpoints:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
client = Grid()
|
|
87
|
+
|
|
88
|
+
# Video:
|
|
89
|
+
result = client.grid.video(
|
|
90
|
+
prompt="a timelapse of a city at night",
|
|
91
|
+
model="LTX-2.3",
|
|
92
|
+
width=768, height=512, seconds=4, fps=24,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# img2img / ControlNet / LoRAs — anything the workers support:
|
|
96
|
+
result = client.grid.image(
|
|
97
|
+
prompt="make it watercolor",
|
|
98
|
+
models=["FLUX.2 Klein 4B FP8"],
|
|
99
|
+
source_image="<base64>",
|
|
100
|
+
loras=[{"name": "watercolor", "model": 1.0}],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Or full control with a raw payload:
|
|
104
|
+
result = client.grid.generate({"prompt": "...", "models": [...], "params": {...}})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`client.grid` waits for the synchronous Grid response and returns the finished
|
|
108
|
+
OpenAI-shaped result. Use `timeout` for long media jobs; there is no SDK
|
|
109
|
+
submit/poll mode on this `/v1` client.
|
|
110
|
+
|
|
111
|
+
## It's just OpenAI underneath
|
|
112
|
+
|
|
113
|
+
`Grid` subclasses `openai.OpenAI`, so anything the OpenAI SDK does — images,
|
|
114
|
+
tool calling, structured output, the full `.chat`/`.images`/`.models` surface —
|
|
115
|
+
works here too. You can also point existing OpenAI code at the Grid by setting
|
|
116
|
+
`base_url="https://api.aipowergrid.io/v1"` if you'd rather not switch packages.
|
|
117
|
+
|
|
118
|
+
## Config
|
|
119
|
+
|
|
120
|
+
| | |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `Grid(api_key=...)` | Explicit key (overrides env) |
|
|
123
|
+
| `AIPG_API_KEY` | Env var read when no key is passed |
|
|
124
|
+
| `Grid(base_url=...)` | Override the endpoint (default `https://api.aipowergrid.io/v1`) |
|
|
125
|
+
|
|
126
|
+
## Links
|
|
127
|
+
|
|
128
|
+
- [Docs](https://aipowergrid.io/docs)
|
|
129
|
+
- [Get an API key](https://console.aipowergrid.io/dashboard/api-key)
|
|
130
|
+
- [Discord](https://discord.gg/W9D8j6HCtC)
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==82.0.1", "wheel==0.48.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "grid-sdk"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Python SDK for AI Power Grid open-model, OpenAI-compatible inference."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "AI Power Grid" }]
|
|
14
|
+
keywords = ["ai", "llm", "inference", "openai", "decentralized", "aipg"]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"openai>=1.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
test = ["pytest", "pytest-asyncio", "respx", "httpx"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://aipowergrid.io"
|
|
24
|
+
Documentation = "https://aipowergrid.io/docs"
|
|
25
|
+
Repository = "https://github.com/AIPowerGrid/grid-sdk-python"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
asyncio_mode = "auto"
|
grid_sdk-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""AI Power Grid Python SDK.
|
|
3
|
+
|
|
4
|
+
The Grid API is OpenAI-compatible, so this SDK is a thin layer over the
|
|
5
|
+
official `openai` client: it pre-points at the Grid, reads your key from the
|
|
6
|
+
environment, and adds Grid-specific conveniences (listing online models, and
|
|
7
|
+
`client.grid` for video / advanced image generation). Anything you can do
|
|
8
|
+
with the `openai` package, you can do here — same `.chat`, `.images`,
|
|
9
|
+
`.models`.
|
|
10
|
+
|
|
11
|
+
from grid_sdk import Grid
|
|
12
|
+
|
|
13
|
+
client = Grid() # reads AIPG_API_KEY from the environment
|
|
14
|
+
|
|
15
|
+
stream = client.chat.completions.create(
|
|
16
|
+
model="grid/llama-3.3-70b-versatile",
|
|
17
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
18
|
+
stream=True,
|
|
19
|
+
)
|
|
20
|
+
for chunk in stream:
|
|
21
|
+
print(chunk.choices[0].delta.content or "", end="")
|
|
22
|
+
|
|
23
|
+
`AIPG` / `AsyncAIPG` are kept as aliases of `Grid` / `AsyncGrid`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .client import AIPG, AsyncAIPG, AsyncGrid, Grid, DEFAULT_BASE_URL
|
|
27
|
+
|
|
28
|
+
__all__ = ["Grid", "AsyncGrid", "AIPG", "AsyncAIPG", "DEFAULT_BASE_URL"]
|
|
29
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Grid clients — thin subclasses of the OpenAI client.
|
|
3
|
+
|
|
4
|
+
We subclass rather than wrap so every current and future feature of the
|
|
5
|
+
`openai` package (chat, images, streaming, tool use, etc.) is available with
|
|
6
|
+
zero extra surface area. We only override the defaults (base URL, key source)
|
|
7
|
+
and add Grid-specific helpers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from openai import AsyncOpenAI, OpenAI
|
|
16
|
+
|
|
17
|
+
from .grid import AsyncGridRaw, GridRaw
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.aipowergrid.io/v1"
|
|
20
|
+
API_KEY_ENV = "AIPG_API_KEY"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_key(api_key: Optional[str]) -> str:
|
|
24
|
+
key = api_key or os.getenv(API_KEY_ENV)
|
|
25
|
+
if not key:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"No API key provided. Pass api_key=... or set the {API_KEY_ENV} "
|
|
28
|
+
f"environment variable. Create a key at https://console.aipowergrid.io/dashboard/api-key"
|
|
29
|
+
)
|
|
30
|
+
return key
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Grid(OpenAI):
|
|
34
|
+
"""Synchronous AI Power Grid client.
|
|
35
|
+
|
|
36
|
+
Drop-in for `openai.OpenAI`, pre-configured for the Grid:
|
|
37
|
+
|
|
38
|
+
client = Grid() # key from AIPG_API_KEY
|
|
39
|
+
client = Grid(api_key="grid-...") # or explicit
|
|
40
|
+
|
|
41
|
+
Use `client.chat`, `client.images`, etc. exactly as you would with the
|
|
42
|
+
OpenAI SDK. Plus `client.online_models()` for what's servable right now,
|
|
43
|
+
and `client.grid` for video / advanced image generation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, **kwargs):
|
|
47
|
+
super().__init__(api_key=_resolve_key(api_key), base_url=base_url, **kwargs)
|
|
48
|
+
# Raw-Grid access (video, img2img, ControlNet, LoRAs) beyond the
|
|
49
|
+
# OpenAI-compatible surface. See grid_sdk.grid.GridRaw.
|
|
50
|
+
self.grid = GridRaw(self.api_key, str(self.base_url))
|
|
51
|
+
|
|
52
|
+
def online_models(self) -> List[str]:
|
|
53
|
+
"""Return the model IDs currently served by connected workers.
|
|
54
|
+
|
|
55
|
+
An empty list means no workers are online right now — requests will
|
|
56
|
+
return 503 until one connects. Prefer this over a hardcoded model
|
|
57
|
+
name, since the Grid's available models shift with worker presence.
|
|
58
|
+
"""
|
|
59
|
+
return [m.id for m in self.models.list().data]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AsyncGrid(AsyncOpenAI):
|
|
63
|
+
"""Asynchronous AI Power Grid client. Async twin of :class:`Grid`."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, **kwargs):
|
|
66
|
+
super().__init__(api_key=_resolve_key(api_key), base_url=base_url, **kwargs)
|
|
67
|
+
self.grid = AsyncGridRaw(self.api_key, str(self.base_url))
|
|
68
|
+
|
|
69
|
+
async def online_models(self) -> List[str]:
|
|
70
|
+
"""Async variant of :meth:`Grid.online_models`."""
|
|
71
|
+
models = await self.models.list()
|
|
72
|
+
return [m.id for m in models.data]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Backwards-friendly aliases. `Grid` is the preferred name; `AIPG` is kept so
|
|
76
|
+
# existing references and the brand both resolve.
|
|
77
|
+
AIPG = Grid
|
|
78
|
+
AsyncAIPG = AsyncGrid
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Raw Grid media access — video + advanced image beyond OpenAI-compat chat.
|
|
3
|
+
|
|
4
|
+
The OpenAI-compatible ``/v1`` endpoints cover text and basic txt2img. This adds
|
|
5
|
+
the grid's SYNCHRONOUS media endpoints — ``/v1/images/generations`` (full param
|
|
6
|
+
surface: img2img, LoRAs, styles, samplers) and ``/v1/videos/generations`` — as
|
|
7
|
+
``client.grid``. Both are synchronous: the call returns the finished result
|
|
8
|
+
(hosted media.aipg.art URLs), no submit/poll. (Replaces the retired horde
|
|
9
|
+
``/api/v2`` async queue.)
|
|
10
|
+
|
|
11
|
+
client = AIPG()
|
|
12
|
+
|
|
13
|
+
vid = client.grid.video("a city timelapse", model="LTX-2.3",
|
|
14
|
+
width=768, height=512, seconds=4)
|
|
15
|
+
print(vid["data"][0]["url"])
|
|
16
|
+
|
|
17
|
+
img = client.grid.image("make it watercolor", model="FLUX.2 Klein 4B FP8",
|
|
18
|
+
source_image="<base64>", strength=0.6,
|
|
19
|
+
loras=[{"name": "watercolor", "model": 1.0}])
|
|
20
|
+
print(img["data"][0]["url"])
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GridError(RuntimeError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _v1_base(base_url: str) -> str:
|
|
35
|
+
"""The client base already ends in /v1; just strip trailing slashes."""
|
|
36
|
+
return str(base_url).rstrip("/")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _image_body(
|
|
40
|
+
prompt: str,
|
|
41
|
+
model: Optional[str],
|
|
42
|
+
models: Optional[List[str]],
|
|
43
|
+
width: int,
|
|
44
|
+
height: int,
|
|
45
|
+
steps: Optional[int],
|
|
46
|
+
cfg_scale: Optional[float],
|
|
47
|
+
sampler_name: Optional[str],
|
|
48
|
+
n: int,
|
|
49
|
+
source_image: Optional[str],
|
|
50
|
+
strength: Optional[float],
|
|
51
|
+
negative_prompt: Optional[str],
|
|
52
|
+
loras: Optional[list],
|
|
53
|
+
style: Optional[str],
|
|
54
|
+
params: Dict[str, Any],
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
body: Dict[str, Any] = {
|
|
57
|
+
"model": model or (models[0] if models else None),
|
|
58
|
+
"prompt": prompt,
|
|
59
|
+
"n": n,
|
|
60
|
+
"size": f"{width}x{height}",
|
|
61
|
+
}
|
|
62
|
+
if steps is not None:
|
|
63
|
+
body["steps"] = steps
|
|
64
|
+
if cfg_scale is not None:
|
|
65
|
+
body["cfg_scale"] = cfg_scale
|
|
66
|
+
if sampler_name is not None:
|
|
67
|
+
body["sampler"] = sampler_name
|
|
68
|
+
if negative_prompt is not None:
|
|
69
|
+
body["negative_prompt"] = negative_prompt
|
|
70
|
+
if loras is not None:
|
|
71
|
+
body["loras"] = loras
|
|
72
|
+
if style is not None:
|
|
73
|
+
body["style"] = style
|
|
74
|
+
if source_image is not None:
|
|
75
|
+
body["image"] = source_image
|
|
76
|
+
if strength is not None:
|
|
77
|
+
body["strength"] = strength
|
|
78
|
+
body.update(params)
|
|
79
|
+
return body
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _video_body(
|
|
83
|
+
prompt: str,
|
|
84
|
+
model: Optional[str],
|
|
85
|
+
models: Optional[List[str]],
|
|
86
|
+
width: int,
|
|
87
|
+
height: int,
|
|
88
|
+
seconds: Optional[float],
|
|
89
|
+
fps: Optional[int],
|
|
90
|
+
source_image: Optional[str],
|
|
91
|
+
params: Dict[str, Any],
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
body: Dict[str, Any] = {
|
|
94
|
+
"model": model or (models[0] if models else None),
|
|
95
|
+
"prompt": prompt,
|
|
96
|
+
"size": f"{width}x{height}",
|
|
97
|
+
}
|
|
98
|
+
if seconds is not None:
|
|
99
|
+
body["seconds"] = seconds
|
|
100
|
+
if fps is not None:
|
|
101
|
+
body["fps"] = fps
|
|
102
|
+
if source_image is not None:
|
|
103
|
+
body["image"] = source_image
|
|
104
|
+
body.update(params)
|
|
105
|
+
return body
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class GridRaw:
|
|
109
|
+
"""Synchronous raw-Grid media client. Reached via ``AIPG().grid``."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, api_key: str, base_url: str, timeout: float = 300.0):
|
|
112
|
+
self._headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
113
|
+
self._base = _v1_base(base_url)
|
|
114
|
+
self._timeout = timeout
|
|
115
|
+
|
|
116
|
+
def _post(self, path: str, body: Dict[str, Any], timeout: float) -> Dict[str, Any]:
|
|
117
|
+
with httpx.Client(timeout=timeout) as c:
|
|
118
|
+
r = c.post(f"{self._base}{path}", headers=self._headers, json=body)
|
|
119
|
+
if r.status_code != 200:
|
|
120
|
+
raise GridError(f"{path} failed [{r.status_code}]: {r.text[:300]}")
|
|
121
|
+
return r.json()
|
|
122
|
+
|
|
123
|
+
def _get(self, path: str, timeout: float = 30.0) -> Dict[str, Any]:
|
|
124
|
+
with httpx.Client(timeout=timeout) as c:
|
|
125
|
+
r = c.get(f"{self._base}{path}", headers=self._headers)
|
|
126
|
+
if r.status_code != 200:
|
|
127
|
+
raise GridError(f"{path} failed [{r.status_code}]: {r.text[:300]}")
|
|
128
|
+
return r.json()
|
|
129
|
+
|
|
130
|
+
def credits(self, *, timeout: float = 30.0) -> Dict[str, Any]:
|
|
131
|
+
"""Return promotional, daily-free, purchased, and spendable credit pockets."""
|
|
132
|
+
return self._get("/account/credits", timeout)
|
|
133
|
+
|
|
134
|
+
def generate(
|
|
135
|
+
self, body: Dict[str, Any], *, endpoint: str = "/images/generations", timeout: float = 300.0
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
"""Raw passthrough: POST an arbitrary body to a media endpoint
|
|
138
|
+
(default ``/images/generations``). Returns the OpenAI-shaped response."""
|
|
139
|
+
return self._post(endpoint, body, timeout)
|
|
140
|
+
|
|
141
|
+
def image(
|
|
142
|
+
self,
|
|
143
|
+
prompt: str,
|
|
144
|
+
*,
|
|
145
|
+
model: Optional[str] = None,
|
|
146
|
+
models: Optional[List[str]] = None,
|
|
147
|
+
width: int = 1024,
|
|
148
|
+
height: int = 1024,
|
|
149
|
+
steps: Optional[int] = None,
|
|
150
|
+
cfg_scale: Optional[float] = None,
|
|
151
|
+
sampler_name: Optional[str] = None,
|
|
152
|
+
n: int = 1,
|
|
153
|
+
source_image: Optional[str] = None,
|
|
154
|
+
strength: Optional[float] = None,
|
|
155
|
+
negative_prompt: Optional[str] = None,
|
|
156
|
+
loras: Optional[list] = None,
|
|
157
|
+
style: Optional[str] = None,
|
|
158
|
+
timeout: float = 300.0,
|
|
159
|
+
**params: Any,
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""Image generation with the full param surface (img2img, LoRAs, styles).
|
|
162
|
+
``models=[id]`` is accepted for back-compat; ``model=id`` is preferred.
|
|
163
|
+
Extra keywords (seed, scheduler, …) flow through to the request body."""
|
|
164
|
+
body = _image_body(
|
|
165
|
+
prompt, model, models, width, height, steps, cfg_scale, sampler_name,
|
|
166
|
+
n, source_image, strength, negative_prompt, loras, style, params,
|
|
167
|
+
)
|
|
168
|
+
return self._post("/images/generations", body, timeout)
|
|
169
|
+
|
|
170
|
+
def video(
|
|
171
|
+
self,
|
|
172
|
+
prompt: str,
|
|
173
|
+
*,
|
|
174
|
+
model: Optional[str] = None,
|
|
175
|
+
models: Optional[List[str]] = None,
|
|
176
|
+
width: int = 768,
|
|
177
|
+
height: int = 512,
|
|
178
|
+
seconds: Optional[float] = None,
|
|
179
|
+
fps: Optional[int] = None,
|
|
180
|
+
source_image: Optional[str] = None,
|
|
181
|
+
timeout: float = 600.0,
|
|
182
|
+
**params: Any,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""Video generation (txt2video / img2video)."""
|
|
185
|
+
body = _video_body(prompt, model, models, width, height, seconds, fps, source_image, params)
|
|
186
|
+
return self._post("/videos/generations", body, timeout)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class AsyncGridRaw:
|
|
190
|
+
"""Asynchronous raw-Grid media client. Reached via ``AsyncAIPG().grid``."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, api_key: str, base_url: str, timeout: float = 300.0):
|
|
193
|
+
self._headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
194
|
+
self._base = _v1_base(base_url)
|
|
195
|
+
self._timeout = timeout
|
|
196
|
+
|
|
197
|
+
async def _post(self, path: str, body: Dict[str, Any], timeout: float) -> Dict[str, Any]:
|
|
198
|
+
async with httpx.AsyncClient(timeout=timeout) as c:
|
|
199
|
+
r = await c.post(f"{self._base}{path}", headers=self._headers, json=body)
|
|
200
|
+
if r.status_code != 200:
|
|
201
|
+
raise GridError(f"{path} failed [{r.status_code}]: {r.text[:300]}")
|
|
202
|
+
return r.json()
|
|
203
|
+
|
|
204
|
+
async def _get(self, path: str, timeout: float = 30.0) -> Dict[str, Any]:
|
|
205
|
+
async with httpx.AsyncClient(timeout=timeout) as c:
|
|
206
|
+
r = await c.get(f"{self._base}{path}", headers=self._headers)
|
|
207
|
+
if r.status_code != 200:
|
|
208
|
+
raise GridError(f"{path} failed [{r.status_code}]: {r.text[:300]}")
|
|
209
|
+
return r.json()
|
|
210
|
+
|
|
211
|
+
async def credits(self, *, timeout: float = 30.0) -> Dict[str, Any]:
|
|
212
|
+
"""Async variant of :meth:`GridRaw.credits`."""
|
|
213
|
+
return await self._get("/account/credits", timeout)
|
|
214
|
+
|
|
215
|
+
async def generate(
|
|
216
|
+
self, body: Dict[str, Any], *, endpoint: str = "/images/generations", timeout: float = 300.0
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
return await self._post(endpoint, body, timeout)
|
|
219
|
+
|
|
220
|
+
async def image(
|
|
221
|
+
self,
|
|
222
|
+
prompt: str,
|
|
223
|
+
*,
|
|
224
|
+
model: Optional[str] = None,
|
|
225
|
+
models: Optional[List[str]] = None,
|
|
226
|
+
width: int = 1024,
|
|
227
|
+
height: int = 1024,
|
|
228
|
+
steps: Optional[int] = None,
|
|
229
|
+
cfg_scale: Optional[float] = None,
|
|
230
|
+
sampler_name: Optional[str] = None,
|
|
231
|
+
n: int = 1,
|
|
232
|
+
source_image: Optional[str] = None,
|
|
233
|
+
strength: Optional[float] = None,
|
|
234
|
+
negative_prompt: Optional[str] = None,
|
|
235
|
+
loras: Optional[list] = None,
|
|
236
|
+
style: Optional[str] = None,
|
|
237
|
+
timeout: float = 300.0,
|
|
238
|
+
**params: Any,
|
|
239
|
+
) -> Dict[str, Any]:
|
|
240
|
+
body = _image_body(
|
|
241
|
+
prompt, model, models, width, height, steps, cfg_scale, sampler_name,
|
|
242
|
+
n, source_image, strength, negative_prompt, loras, style, params,
|
|
243
|
+
)
|
|
244
|
+
return await self._post("/images/generations", body, timeout)
|
|
245
|
+
|
|
246
|
+
async def video(
|
|
247
|
+
self,
|
|
248
|
+
prompt: str,
|
|
249
|
+
*,
|
|
250
|
+
model: Optional[str] = None,
|
|
251
|
+
models: Optional[List[str]] = None,
|
|
252
|
+
width: int = 768,
|
|
253
|
+
height: int = 512,
|
|
254
|
+
seconds: Optional[float] = None,
|
|
255
|
+
fps: Optional[int] = None,
|
|
256
|
+
source_image: Optional[str] = None,
|
|
257
|
+
timeout: float = 600.0,
|
|
258
|
+
**params: Any,
|
|
259
|
+
) -> Dict[str, Any]:
|
|
260
|
+
body = _video_body(prompt, model, models, width, height, seconds, fps, source_image, params)
|
|
261
|
+
return await self._post("/videos/generations", body, timeout)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grid-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for AI Power Grid open-model, OpenAI-compatible inference.
|
|
5
|
+
Author: AI Power Grid
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://aipowergrid.io
|
|
8
|
+
Project-URL: Documentation, https://aipowergrid.io/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/AIPowerGrid/grid-sdk-python
|
|
10
|
+
Keywords: ai,llm,inference,openai,decentralized,aipg
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: openai>=1.0.0
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
18
|
+
Requires-Dist: respx; extra == "test"
|
|
19
|
+
Requires-Dist: httpx; extra == "test"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# AI Power Grid — Python SDK
|
|
23
|
+
|
|
24
|
+
Open-model, OpenAI-compatible inference across community-operated GPUs.
|
|
25
|
+
|
|
26
|
+
> **Release status:** `grid-sdk` is staged for its initial PyPI publication.
|
|
27
|
+
> The install command below becomes available when the first verified release
|
|
28
|
+
> is published; until then, use the OpenAI SDK with the Grid base URL shown below.
|
|
29
|
+
|
|
30
|
+
The Grid API speaks the OpenAI protocol, so this SDK is a thin layer over the
|
|
31
|
+
official `openai` package: it points at the Grid, reads your key from the
|
|
32
|
+
environment, and adds Grid-specific conveniences. Everything you know from the
|
|
33
|
+
OpenAI SDK works unchanged.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install grid-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Sign in at the [Grid developer console](https://console.aipowergrid.io/dashboard/api-key),
|
|
44
|
+
create an API key, then set it as `AIPG_API_KEY`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from grid_sdk import Grid
|
|
48
|
+
|
|
49
|
+
client = Grid() # reads AIPG_API_KEY from the environment
|
|
50
|
+
|
|
51
|
+
stream = client.chat.completions.create(
|
|
52
|
+
model="gpt-oss-120b",
|
|
53
|
+
messages=[{"role": "user", "content": "Explain AI Power Grid in one line."}],
|
|
54
|
+
stream=True,
|
|
55
|
+
)
|
|
56
|
+
for chunk in stream:
|
|
57
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## See what's online
|
|
61
|
+
|
|
62
|
+
The Grid's available models change as workers connect and disconnect. Don't
|
|
63
|
+
hardcode a model blindly — ask which ones are servable right now:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = Grid()
|
|
67
|
+
print(client.online_models())
|
|
68
|
+
# ['gpt-oss-120b', 'qwen3-27b', ...]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
An empty list means no workers are connected — requests will 503 until one is.
|
|
72
|
+
|
|
73
|
+
## Credits
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
credits = client.grid.credits()
|
|
77
|
+
print(credits["total_spendable_usd"])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This is Core's canonical promotional, daily-free, and purchased account view.
|
|
81
|
+
The SDK does not maintain a local free-use counter.
|
|
82
|
+
|
|
83
|
+
## Async
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import asyncio
|
|
87
|
+
from grid_sdk import AsyncGrid
|
|
88
|
+
|
|
89
|
+
async def main():
|
|
90
|
+
client = AsyncGrid()
|
|
91
|
+
resp = await client.chat.completions.create(
|
|
92
|
+
model="gpt-oss-120b",
|
|
93
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
94
|
+
)
|
|
95
|
+
print(resp.choices[0].message.content)
|
|
96
|
+
|
|
97
|
+
asyncio.run(main())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Video, img2img, ControlNet, LoRAs — the full Grid
|
|
101
|
+
|
|
102
|
+
The OpenAI-compatible surface covers text and basic txt2img. For the full media
|
|
103
|
+
parameter surface, use `client.grid`, which calls the synchronous `/v1` image
|
|
104
|
+
and video endpoints:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
client = Grid()
|
|
108
|
+
|
|
109
|
+
# Video:
|
|
110
|
+
result = client.grid.video(
|
|
111
|
+
prompt="a timelapse of a city at night",
|
|
112
|
+
model="LTX-2.3",
|
|
113
|
+
width=768, height=512, seconds=4, fps=24,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# img2img / ControlNet / LoRAs — anything the workers support:
|
|
117
|
+
result = client.grid.image(
|
|
118
|
+
prompt="make it watercolor",
|
|
119
|
+
models=["FLUX.2 Klein 4B FP8"],
|
|
120
|
+
source_image="<base64>",
|
|
121
|
+
loras=[{"name": "watercolor", "model": 1.0}],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Or full control with a raw payload:
|
|
125
|
+
result = client.grid.generate({"prompt": "...", "models": [...], "params": {...}})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`client.grid` waits for the synchronous Grid response and returns the finished
|
|
129
|
+
OpenAI-shaped result. Use `timeout` for long media jobs; there is no SDK
|
|
130
|
+
submit/poll mode on this `/v1` client.
|
|
131
|
+
|
|
132
|
+
## It's just OpenAI underneath
|
|
133
|
+
|
|
134
|
+
`Grid` subclasses `openai.OpenAI`, so anything the OpenAI SDK does — images,
|
|
135
|
+
tool calling, structured output, the full `.chat`/`.images`/`.models` surface —
|
|
136
|
+
works here too. You can also point existing OpenAI code at the Grid by setting
|
|
137
|
+
`base_url="https://api.aipowergrid.io/v1"` if you'd rather not switch packages.
|
|
138
|
+
|
|
139
|
+
## Config
|
|
140
|
+
|
|
141
|
+
| | |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `Grid(api_key=...)` | Explicit key (overrides env) |
|
|
144
|
+
| `AIPG_API_KEY` | Env var read when no key is passed |
|
|
145
|
+
| `Grid(base_url=...)` | Override the endpoint (default `https://api.aipowergrid.io/v1`) |
|
|
146
|
+
|
|
147
|
+
## Links
|
|
148
|
+
|
|
149
|
+
- [Docs](https://aipowergrid.io/docs)
|
|
150
|
+
- [Get an API key](https://console.aipowergrid.io/dashboard/api-key)
|
|
151
|
+
- [Discord](https://discord.gg/W9D8j6HCtC)
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/grid_sdk/__init__.py
|
|
5
|
+
src/grid_sdk/client.py
|
|
6
|
+
src/grid_sdk/grid.py
|
|
7
|
+
src/grid_sdk.egg-info/PKG-INFO
|
|
8
|
+
src/grid_sdk.egg-info/SOURCES.txt
|
|
9
|
+
src/grid_sdk.egg-info/dependency_links.txt
|
|
10
|
+
src/grid_sdk.egg-info/requires.txt
|
|
11
|
+
src/grid_sdk.egg-info/top_level.txt
|
|
12
|
+
tests/test_client.py
|
|
13
|
+
tests/test_grid.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
grid_sdk
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Tests for the AIPG Python SDK.
|
|
3
|
+
|
|
4
|
+
These don't hit the network — they verify the SDK's own behavior: key
|
|
5
|
+
resolution, default base URL, that it really is an OpenAI client, and that
|
|
6
|
+
online_models() maps the models response to a list of IDs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from unittest.mock import MagicMock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from grid_sdk import AIPG, AsyncAIPG, DEFAULT_BASE_URL
|
|
16
|
+
from openai import AsyncOpenAI, OpenAI
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── key resolution + config ──
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_explicit_key_is_used():
|
|
23
|
+
client = AIPG(api_key="grid-explicit")
|
|
24
|
+
assert client.api_key == "grid-explicit"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_key_falls_back_to_env(monkeypatch):
|
|
28
|
+
monkeypatch.setenv("AIPG_API_KEY", "grid-from-env")
|
|
29
|
+
client = AIPG()
|
|
30
|
+
assert client.api_key == "grid-from-env"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_missing_key_raises_with_helpful_message(monkeypatch):
|
|
34
|
+
monkeypatch.delenv("AIPG_API_KEY", raising=False)
|
|
35
|
+
with pytest.raises(ValueError) as exc:
|
|
36
|
+
AIPG()
|
|
37
|
+
assert "AIPG_API_KEY" in str(exc.value)
|
|
38
|
+
assert "console.aipowergrid.io/dashboard/api-key" in str(exc.value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_default_base_url_points_at_grid():
|
|
42
|
+
client = AIPG(api_key="k")
|
|
43
|
+
assert str(client.base_url).rstrip("/") == DEFAULT_BASE_URL.rstrip("/")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_base_url_override():
|
|
47
|
+
client = AIPG(api_key="k", base_url="http://localhost:9999/v1")
|
|
48
|
+
assert "localhost:9999" in str(client.base_url)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_is_an_openai_client():
|
|
52
|
+
# Subclassing keeps all OpenAI client features (chat, images, streaming).
|
|
53
|
+
assert isinstance(AIPG(api_key="k"), OpenAI)
|
|
54
|
+
assert isinstance(AsyncAIPG(api_key="k"), AsyncOpenAI)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── online_models() ──
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_online_models_maps_to_ids():
|
|
61
|
+
client = AIPG(api_key="k")
|
|
62
|
+
fake_models = SimpleNamespace(
|
|
63
|
+
data=[SimpleNamespace(id="grid/llama-3.3-70b-versatile"), SimpleNamespace(id="grid/qwen3-32b")]
|
|
64
|
+
)
|
|
65
|
+
client.models.list = MagicMock(return_value=fake_models)
|
|
66
|
+
|
|
67
|
+
assert client.online_models() == ["grid/llama-3.3-70b-versatile", "grid/qwen3-32b"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_online_models_empty_when_no_workers():
|
|
71
|
+
client = AIPG(api_key="k")
|
|
72
|
+
client.models.list = MagicMock(return_value=SimpleNamespace(data=[]))
|
|
73
|
+
assert client.online_models() == []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_async_online_models_maps_to_ids():
|
|
78
|
+
client = AsyncAIPG(api_key="k")
|
|
79
|
+
|
|
80
|
+
async def fake_list():
|
|
81
|
+
return SimpleNamespace(data=[SimpleNamespace(id="grid/qwen3-32b")])
|
|
82
|
+
|
|
83
|
+
client.models.list = fake_list
|
|
84
|
+
assert await client.online_models() == ["grid/qwen3-32b"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Tests for the raw-Grid media client (client.grid) on /v1.
|
|
3
|
+
|
|
4
|
+
Verifies the image/video builders POST the right OpenAI-shaped body to the
|
|
5
|
+
synchronous /v1 media endpoints (img2img, LoRAs, passthrough params). Network
|
|
6
|
+
calls are stubbed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
10
|
+
|
|
11
|
+
from grid_sdk import AIPG
|
|
12
|
+
from grid_sdk.grid import AsyncGridRaw, GridRaw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_client_exposes_grid():
|
|
16
|
+
client = AIPG(api_key="k")
|
|
17
|
+
assert isinstance(client.grid, GridRaw)
|
|
18
|
+
assert client.grid._base == "https://api.aipowergrid.io/v1"
|
|
19
|
+
assert client.grid._headers["Authorization"] == "Bearer k"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_credits_uses_canonical_account_endpoint():
|
|
23
|
+
grid = GridRaw("k", "https://api.aipowergrid.io/v1")
|
|
24
|
+
grid._get = MagicMock(return_value={"promotional": {}, "free": {}, "paid": {}})
|
|
25
|
+
result = grid.credits()
|
|
26
|
+
grid._get.assert_called_once_with("/account/credits", 30.0)
|
|
27
|
+
assert set(result) == {"promotional", "free", "paid"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def test_async_credits_uses_canonical_account_endpoint():
|
|
31
|
+
grid = AsyncGridRaw("k", "https://api.aipowergrid.io/v1")
|
|
32
|
+
grid._get = AsyncMock(return_value={"total_spendable_usd": 0.15})
|
|
33
|
+
result = await grid.credits()
|
|
34
|
+
grid._get.assert_awaited_once_with("/account/credits", 30.0)
|
|
35
|
+
assert result["total_spendable_usd"] == 0.15
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _capture(grid: GridRaw):
|
|
39
|
+
"""Replace _post with a capturing stub; return the captured call."""
|
|
40
|
+
cap = {}
|
|
41
|
+
grid._post = MagicMock(
|
|
42
|
+
side_effect=lambda path, body, timeout: cap.update({"path": path, "body": body})
|
|
43
|
+
or {"data": [{"url": "https://media.aipg.art/x.webp"}]}
|
|
44
|
+
)
|
|
45
|
+
return cap
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_image_posts_openai_shape():
|
|
49
|
+
grid = GridRaw("k", "https://api.aipowergrid.io/v1")
|
|
50
|
+
cap = _capture(grid)
|
|
51
|
+
grid.image("a cat", model="FLUX.2 Klein 4B FP8", width=512, height=768, steps=20, cfg_scale=3.5)
|
|
52
|
+
assert cap["path"] == "/images/generations"
|
|
53
|
+
b = cap["body"]
|
|
54
|
+
assert b["model"] == "FLUX.2 Klein 4B FP8"
|
|
55
|
+
assert b["prompt"] == "a cat"
|
|
56
|
+
assert b["size"] == "512x768"
|
|
57
|
+
assert b["steps"] == 20
|
|
58
|
+
assert b["cfg_scale"] == 3.5
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_models_list_back_compat():
|
|
62
|
+
grid = GridRaw("k", "https://api.aipowergrid.io/v1")
|
|
63
|
+
cap = _capture(grid)
|
|
64
|
+
grid.image("x", models=["z-image-turbo"])
|
|
65
|
+
assert cap["body"]["model"] == "z-image-turbo"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_image_img2img_fields():
|
|
69
|
+
grid = GridRaw("k", "https://api.aipowergrid.io/v1")
|
|
70
|
+
cap = _capture(grid)
|
|
71
|
+
grid.image("watercolor", model="m", source_image="BASE64", strength=0.6)
|
|
72
|
+
assert cap["body"]["image"] == "BASE64"
|
|
73
|
+
assert cap["body"]["strength"] == 0.6
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_image_loras_and_passthrough():
|
|
77
|
+
grid = GridRaw("k", "https://api.aipowergrid.io/v1")
|
|
78
|
+
cap = _capture(grid)
|
|
79
|
+
grid.image("x", model="m", loras=[{"name": "watercolor", "model": 1.0}], seed=42)
|
|
80
|
+
assert cap["body"]["loras"] == [{"name": "watercolor", "model": 1.0}]
|
|
81
|
+
assert cap["body"]["seed"] == 42
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_video_posts_to_videos_endpoint():
|
|
85
|
+
grid = GridRaw("k", "https://api.aipowergrid.io/v1")
|
|
86
|
+
cap = _capture(grid)
|
|
87
|
+
grid.video("a timelapse", model="LTX-2.3", width=768, height=512, seconds=4, fps=24)
|
|
88
|
+
assert cap["path"] == "/videos/generations"
|
|
89
|
+
b = cap["body"]
|
|
90
|
+
assert b["model"] == "LTX-2.3"
|
|
91
|
+
assert b["size"] == "768x512"
|
|
92
|
+
assert b["seconds"] == 4
|
|
93
|
+
assert b["fps"] == 24
|