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 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.
@@ -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,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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,7 @@
1
+ openai>=1.0.0
2
+
3
+ [test]
4
+ pytest
5
+ pytest-asyncio
6
+ respx
7
+ httpx
@@ -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