Metadata-Version: 2.5
Name: mage-space
Version: 0.0.1
Summary: The official Python SDK for the Mage API: generate images, video, and audio with Mage models.
Project-URL: Homepage, https://www.mage.space
Project-URL: Documentation, https://docs.mage.space/api/overview
Project-URL: Repository, https://github.com/mage-space/mage-python
Project-URL: Issues, https://github.com/mage-space/mage-python/issues
Project-URL: Changelog, https://github.com/mage-space/mage-python/blob/main/CHANGELOG.md
Author-email: Mage <mage@mage.space>
License-Expression: MIT
License-File: LICENSE
Keywords: ai,api,audio generation,image generation,mage,mage.space,sdk,video generation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Video
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.25
Requires-Dist: typing-extensions>=4.7
Description-Content-Type: text/markdown

# Mage Python SDK

> **Beta.** This SDK is at 0.x: names and behaviour may change between minor versions until 1.0. The [Mage API](https://docs.mage.space/api/overview) itself is versioned separately and is stable within `v1`.

The official Python client for the [Mage API](https://docs.mage.space/api/overview). Generate images, video, and audio with Mage models from your own code. Every request is paid in Gems from your Mage account.

- Sync (`Mage`) and asyncio (`AsyncMage`) clients on [httpx](https://www.python-httpx.org/)
- `run()` submits a generation and waits for the result, backing off as the API recommends
- Safe retries: every submit carries an idempotency key, so a retry is never charged twice
- Uploads of large files through signed URLs in one call
- Typed request and response bodies for every model, generated from the API's OpenAPI spec

## Install

```bash
pip install mage-space
# or
uv add mage-space
```

Python 3.10 or newer.

## Quick start

Create a key in [API → API Keys](https://www.mage.space/api?tab=api-keys) and export it:

```bash
export MAGE_API_KEY="mage_sk_..."
```

```python
from mage_space import Mage

mage = Mage()

request = mage.run("mango", {"prompt": "A lighthouse at dawn, 35mm film", "aspect_ratio": "16:9"})
print(request["result"]["url"])
```

Keys belong to your account and spend your Gems. Keep them on your servers: the API sends no CORS headers, and a key shipped inside an app can be extracted.

## Generating

`run()` is `generate()` followed by `requests.wait()`. Use the parts when you want the request id before the output is ready:

```python
request = mage.generate(
    "cherry",
    {
        "prompt": "Waves rolling onto a black sand beach at sunset",
        "resolution": "720p",
        "duration": "5",
    },
)
print(request["request_id"], request["status"])  # ... in_progress

final = mage.requests.wait(
    request,
    timeout=900,  # seconds; then MageTimeoutError, and the request keeps running
    on_update=lambda r: print(r["status"]),
)
if final["status"] == "completed":
    print(final["result"]["url"])
```

`wait()` polls with growing intervals (2 s, then ×1.5 up to 15 s, with jitter). `run()` raises `MageGenerationError` when the request fails or is cancelled, so what it returns always has a `result`. Cancel a live request with `mage.requests.cancel(request_id)`; Gems are not returned for a cancelled request.

Results are kept for 30 days. Download what you want to keep.

### Async

```python
import asyncio

from mage_space import AsyncMage


async def main() -> None:
    async with AsyncMage() as mage:
        request = await mage.run("seed_audio", {"prompt": "Rain on a tin roof", "duration": "10"})
        print(request["result"]["url"])


asyncio.run(main())
```

`AsyncMage` has the same methods. Its `on_update` may be a plain function or a coroutine function.

## Models

The first argument is the model's architecture id, as in its endpoint path: `mango` for images, `cherry` for video, `seed_audio` for audio, and [many more](https://docs.mage.space/api/models/overview). Choose a variant with `model_id`:

```python
mage.run("mango", {"prompt": "...", "model_id": "mango-v3s"})
```

Each model has a TypedDict in `mage_space.types` with its fields, allowed values, and defaults. Annotate a config with it to have your type checker check it:

```python
from mage_space.types import CherryConfig

config: CherryConfig = {"prompt": "A paper boat in a gutter stream", "resolution": "1080p"}
mage.run("cherry", config)
```

Fields a model's schema does not list pass through to the model unchanged, and a model newer than your SDK works by its id. For the live catalog with each model's options and price, call `mage.architectures.list()`. `mage_space.ARCHITECTURES` holds the name, output type, and request JSON Schema of every model this SDK version was generated from.

## Inputs and uploads

Media fields (`image`, `additional_images`, `first_image`, `last_image`, `video`, `videos`) take an `https` URL or a data URL. Request bodies are capped at 4.5 MB, so upload larger files first and send the returned URL:

```python
photo = mage.uploads.upload("photo.jpg")  # a path, bytes, or a binary file object; 100 MB max
mage.run("kiwi", {"prompt": "The camera slowly pushes in", "first_image": photo["url"]})

clip = mage.uploads.upload("clip.mp4")
mage.run("cherry", {"prompt": "Restyle this clip as watercolor", "videos": [clip["url"]]})
```

The content type comes from the file name; pass `content_type=` for raw bytes. Uploaded files are deleted after 30 days (`url_expires_at`).

## Characters and references

Save a character or a reference once, then mention it by `@handle` in any prompt:

```python
ana = mage.characters.create(name="Ana", handle="ana", image="https://example.com/ana.png")
coat = mage.references.create(name="Red coat", handle="red-coat", kind="outfit", image=photo["url"])

mage.run("mango", {"prompt": "@ana walking through a night market, wearing @red-coat"})

page = mage.characters.list(limit=50)
while page["next_cursor"]:
    page = mage.characters.list(cursor=page["next_cursor"])

mage.references.delete(coat["id"])
```

## Errors

| Exception | When |
| --- | --- |
| `MageAPIError` | The API answered with an error: `status`, `code` (for example `insufficient_gems`, `invalid_config`), `message`, `request_id`, `body`. |
| `MageConnectionError` | No response at all (network error or timeout), after retries. |
| `MageGenerationError` | `run()` ended with a failed or cancelled request: `code` (for example `content_blocked`), `reason` and `stage` for a [provider block](#provider-blocks), and the final `request`. |
| `MageTimeoutError` | `wait()` or `run()` passed its `timeout`. A response that finishes late is never returned; `Mage` checks the deadline between network reads, so a stalled read can overrun it by up to the time that was left when the read began (`AsyncMage` stops on time). `request` is the last state read (None if none was); the request keeps running. |

All four derive from `MageError`.

```python
from mage_space import MageAPIError, MageGenerationError

try:
    mage.run("mango", {"prompt": "..."})
except MageAPIError as error:
    if error.code == "insufficient_gems":
        print("Gems needed:", error.body["error"]["gems_required"])
    else:
        raise
except MageGenerationError as error:
    print("No output:", error.code, error.message)
```

New error codes may appear; treat an unknown `code` by its HTTP `status`.

### Provider blocks

Some models run on an external provider that can refuse a request: a real person, copyrighted material, mature content, and so on. The request fails with the code `content_blocked`, its Gems are refunded, and `MageGenerationError.reason` says why:

| `reason` | The provider refused | What to change |
| --- | --- | --- |
| `likeness` | A real person or celebrity | Remove real people from the prompt and images |
| `ip` | Copyrighted or trademarked material | Describe an original design instead of a character, brand, artist, or lyrics |
| `nsfw` | Content past the model's own limit | Try another model, or soften the request |
| `minors` | Content that may involve a minor | Make sure everyone depicted is clearly an adult |
| `audio` | The audio, usually copyrighted music or a real person's voice | Remove music or voices from the inputs, or generate without audio |
| `unspecified` | Something it did not name | Reword the prompt or change the images |

`stage` is `input` when the provider refused the prompt or input media, `output` when it refused the generated media, and None when it did not say. Retrying the same request usually gets the same block, so change the request or the model instead. New reasons may appear; treat one you do not know as `unspecified`. A `content_blocked` request without a `reason` was blocked by Mage's own content policy, which keeps the Gems.

```python
from mage_space import MageGenerationError

try:
    mage.run("mango", {"prompt": "A portrait of a famous actor"})
except MageGenerationError as error:
    if error.reason == "likeness":
        print("Describe an original person instead, or pick another model.")
    raise
```

## Retries and idempotency

Failed calls are retried up to `max_retries` times (default 2) with exponential backoff:

- Reads, deletes, cancels, and uploads retry on connection errors, 408, 429, and 5xx.
- `generate()` sends an `Idempotency-Key` header (a fresh UUID unless you pass `idempotency_key=`) and retries with the same key on connection errors and on 408 or 5xx responses that Mage did not record. A replayed submit returns the original request and charges nothing.
- Creating characters and references is never retried.

Pass your own `idempotency_key`, such as a job id, to make retries across processes safe too. Never reuse a key for a different request.

## Configuration

```python
Mage(
    api_key=None,  # default: MAGE_API_KEY
    base_url=None,  # default: MAGE_BASE_URL, else https://api.mage.space
    timeout=60.0,  # seconds per HTTP attempt
    max_retries=2,
    http_client=None,  # your own httpx.Client (an httpx.AsyncClient for AsyncMage)
)
```

Use `with Mage() as mage:` (or `mage.close()`) to close the connection pool. The SDK closes only an HTTP client it created.

## Documentation

- [API reference](https://docs.mage.space/api/overview): requests, inputs, errors, limits, and every model's fields
- [Changelog](https://github.com/mage-space/mage-python/blob/main/CHANGELOG.md)
- [Contributing](https://github.com/mage-space/mage-python/blob/main/CONTRIBUTING.md)

## License

[MIT](https://github.com/mage-space/mage-python/blob/main/LICENSE)
