Metadata-Version: 2.4
Name: holdapi-client
Version: 0.1.0
Summary: Dependency-free Python client for the HoldAPI credit gateway
Project-URL: Homepage, https://holdapi.lol
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# HoldAPI Python client

Python 3.10 or newer. No third-party runtime dependencies.

Install the public wheel:

```sh
python -m pip install https://holdapi.lol/downloads/holdapi_client-0.1.0-py3-none-any.whl
```

Set `HOLDAPI_API_KEY` privately in your environment. Do not paste it into scripts or logs.

```python
import os
from holdapi import HoldApiClient

with HoldApiClient() as client:
    catalog = client.models().json()["data"]
    available = {row["id"] for row in catalog if row["type"] == "chat"}
    # Choose an ID from the catalog and set HOLDAPI_CHAT_MODEL before running.
    model = os.environ["HOLDAPI_CHAT_MODEL"]
    if model not in available:
        raise ValueError("Choose an available chat model from client.models()")
    response = client.chat(
        model,
        [{"role": "user", "content": "Explain credit metering."}],
        max_tokens=200,
        max_credit_micro_usd=10_000,
    )
    print(response.json()["choices"][0]["message"]["content"])
    print(response.billing.charged_usd)
```

`max_credit_micro_usd` is an integer reservation ceiling enforced by the gateway before a hold or paid request. It does not guarantee an absolute final-cost ceiling: verified usage above the hold can still be charged.

Every method returns `Response(status, body, headers, billing)`. `body` is bytes, including audio. `json()` uses `Decimal` for fractional JSON numbers. Billing exposes exact integer `charged_micro_usd`, `reserved_micro_usd`, `remaining_micro_usd`, `pending`, `usage_id` and `request_id`. When `pending` is true, `charged_micro_usd` and `charged_usd` are `None`; a reservation is not a final charge. This release rejects `stream=True` before dispatch and does not expose incremental SSE iteration.

Named methods: `models`, `balance`, `tools`, `chat`, `generate_image`, `edit_image`, `speech`, `sound_effects`, `music`, `video`, `messages`, `responses`, `tool_call`, `poll_owned`, and `wait_for_job`. Model IDs use `provider/model`. Native Messages and Responses have the gateway's stateless restrictions. Discover currently available tools with `tools()`; model and rail availability are validated by the gateway.

`video` preserves the gateway's `image` and `duration_seconds` parameters. Signed public video job identifiers are accepted by `poll_owned` without decoding or replacing their signatures. Credit ceilings must contain at most 15 decimal digits.

```python
with HoldApiClient() as client:
    job = client.generate_image(
        "openai/gpt-image-2", "A blue sky", async_=True,
        max_credit_micro_usd=100_000,
    )
    result = client.wait_for_job("image", job.json()["id"])
```

The Python `async_` option becomes the gateway's `async` field. A request dictionary also works through `tool_call`:

```python
with HoldApiClient() as client:
    job = client.tool_call("/images/generations", {
        "model": "openai/gpt-image-2", "prompt": "A blue sky", "async": True
    }, max_credit_micro_usd=100_000)
    result = client.wait_for_job("image", job.json()["id"])
```

`poll_owned` accepts only a public gateway identifier and `image`, `music` or `video`. It never follows a provider `poll_url`. `wait_for_job` repeats only successful pending GETs; HTTP errors and transport failures are returned immediately without retry. Paid calls have zero automatic retries, including 429, 5xx and timeouts. `APIError` provides status, code, redacted error body and billing.

Image methods default to a 300-second timeout. Other calls default to 60 seconds. Explicit custom gateway URLs require HTTPS; loopback HTTP is allowed for local tests. Provider origins such as OpenAI and BlockRun are rejected to keep HoldAPI keys off those hosts. Redirects and environment HTTP proxies are disabled.

```python
with HoldApiClient() as client:
    client.edit_image("Make the background blue", image_bytes, max_credit_micro_usd=100_000)
    client.download_media(result.json()["data"][0]["url"], destination="image.png")
```

`edit_image` accepts image bytes, an explicit `pathlib.Path`, or inline image data URIs. `download_media` uses a separate connection with no Bearer key or cookies. It accepts public HTTPS only, pins a vetted public DNS address, rejects redirects, caps bytes and checks MIME against magic bytes. It cannot download private/loopback media or arbitrary text. Saving uses the caller's explicit local path, atomically and without overwriting unless `overwrite=True`. It does not infer filenames from remote headers. Media operations are local SDK actions, not remote MCP file operations.

Run the offline tests:

```sh
PYTHONPATH=src python -m unittest discover -s tests -v
uv build --python /path/to/python
```

Tests use a threaded loopback fixture and mocked DNS/TLS transport for media. They make no provider calls, use no database, and pay no fees.
