Metadata-Version: 2.4
Name: animica-ai
Version: 0.1.0
Summary: Official Python SDK for the Animica API (OpenAI-compatible inference, embeddings, usage, and webhook verification).
Project-URL: Homepage, https://animica.org
Project-URL: Documentation, https://api.animica.org
Project-URL: Source, https://github.com/animicaorg/all
Author-email: Animica <support@animica.org>
License-Expression: MIT
Keywords: ai,animica,embeddings,inference,llm,openai-compatible,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# animica-ai

Official, dependency-free Python SDK for the [Animica API](https://api.animica.org).

The Animica API is **OpenAI-compatible**, so this client mirrors the familiar
`chat/completions`, `completions`, `embeddings`, and `models` surface while
adding Animica-specific helpers (usage reporting and webhook signature
verification).

- **Zero third-party dependencies** — uses only the Python standard library.
- **Python 3.9+**
- **Streaming** support via server-sent events.
- **Idempotency keys** for safe retries.
- **Webhook verification** built in.

## Install

```bash
pip install animica-ai
```

The import package is `animica_ai`:

```python
from animica_ai import Animica, AnimicaError, verify_webhook
```

## Authentication

Get an API key from your Animica dashboard. Production keys look like
`anm_live_...`; test keys look like `anm_test_...`. The key is sent as
`Authorization: Bearer <api_key>`.

```python
from animica_ai import Animica

client = Animica(api_key="anm_live_xxx")
# Optional: override the base URL (defaults to https://api.animica.org/v1).
# client = Animica(api_key="anm_live_xxx", base_url="https://console.animica.org/v1")
```

## OpenAI drop-in

Because the API is OpenAI-compatible, you can also use the official OpenAI SDK
by pointing it at the Animica base URL — no other code changes required:

```python
from openai import OpenAI

client = OpenAI(
    base_url="https://api.animica.org/v1",
    api_key="anm_live_xxx",
)

resp = client.chat.completions.create(
    model="anm-fast-8b",
    messages=[{"role": "user", "content": "Hello from OpenAI's SDK!"}],
)
print(resp.choices[0].message.content)
```

Use `animica-ai` when you want a tiny, dependency-free client (and the webhook
helper); use the OpenAI SDK if you already depend on it.

## Models

Available models include:

| Model | Purpose |
| --- | --- |
| `anm-fast-8b` | Fast general chat |
| `anm-code-7b` | Code generation |
| `anm-pro-70b` | High-quality reasoning |
| `anm-bittensor-router` | Routed inference |
| `anm-embed` | Embeddings |
| `anm-worker-small` | Worker tasks |
| `anm-worker-code` | Worker code tasks |

```python
for m in client.models()["data"]:
    print(m["id"])
```

## Quickstart

### Chat completions

```python
resp = client.chat_completions(
    model="anm-fast-8b",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Write a haiku about mining."},
    ],
    temperature=0.7,
    max_tokens=128,
)
print(resp["choices"][0]["message"]["content"])
print(resp["usage"])  # {prompt_tokens, completion_tokens, total_tokens}
```

Extra parameters such as `top_p`, `stop`, and `n` are passed straight through:

```python
resp = client.chat_completions(
    model="anm-pro-70b",
    messages=[{"role": "user", "content": "List 3 primes."}],
    top_p=0.9,
    stop=["\n\n"],
)
```

### Streaming

When `stream=True`, `chat_completions` returns a generator that yields parsed
`chat.completion.chunk` dicts and stops at `[DONE]`:

```python
stream = client.chat_completions(
    model="anm-fast-8b",
    messages=[{"role": "user", "content": "Stream a short story."}],
    stream=True,
)
for chunk in stream:
    delta = chunk["choices"][0]["delta"]
    if "content" in delta:
        print(delta["content"], end="", flush=True)
print()
```

### Text completions

```python
resp = client.completions(
    model="anm-fast-8b",
    prompt="Once upon a time",
    max_tokens=64,
)
print(resp["choices"][0]["text"])
```

### Embeddings

```python
resp = client.embeddings(
    model="anm-embed",
    input=["hello world", "goodbye world"],
)
for item in resp["data"]:
    print(item["index"], len(item["embedding"]))
print(resp["usage"])  # {prompt_tokens, total_tokens}
```

### Usage

```python
u = client.usage()
print(u["totalSpentUsd"], u["count"])
```

## Idempotency

Pass `idempotency_key` to make POST requests safe to retry — the server replays
the stored response for a repeated key:

```python
resp = client.chat_completions(
    model="anm-fast-8b",
    messages=[{"role": "user", "content": "Charge me once."}],
    idempotency_key="order-12345",
)
```

## Error handling

Non-2xx responses raise `AnimicaError`, which carries the OpenAI-shaped error
fields plus the HTTP status and the `x-request-id` response header:

```python
from animica_ai import Animica, AnimicaError

client = Animica(api_key="anm_live_xxx")
try:
    client.chat_completions(
        model="anm-fast-8b",
        messages=[{"role": "user", "content": "Hi"}],
    )
except AnimicaError as e:
    print("status:", e.status)          # e.g. 429
    print("type:", e.type)              # authentication_error | invalid_request_error |
                                        # insufficient_quota | rate_limit_error |
                                        # permission_error | api_error
    print("code:", e.code)
    print("message:", e.message)
    print("request_id:", e.request_id)  # from x-request-id, for support
```

Rate-limit responses (HTTP 429) include a `retry-after` header and the
`x-ratelimit-*` headers are present on all responses.

## Webhook verification

Animica signs webhook deliveries with the header:

```
X-Animica-Signature: t=<unixSeconds>,v1=<hex>
```

where `hex = HMAC_SHA256(endpointSecret, f"{t}.{rawRequestBody}")`. Verify the
**raw** request body (not re-serialized JSON) with `verify_webhook`, which does
a constant-time comparison and enforces a timestamp tolerance (default 300s):

```python
from animica_ai import verify_webhook

# Flask example
from flask import Flask, request, abort
import json

app = Flask(__name__)
ENDPOINT_SECRET = "whsec_xxx"

@app.post("/animica/webhook")
def webhook():
    raw = request.get_data()  # bytes — must be the raw body
    sig = request.headers.get("X-Animica-Signature", "")
    if not verify_webhook(ENDPOINT_SECRET, raw, sig):
        abort(400)
    event = json.loads(raw)
    print(event["type"], event["id"])  # evt_...
    return "", 204
```

`payload` may be `str` or `bytes`. Set `tolerance_sec=0` to disable the
timestamp window (not recommended in production).

## License

MIT
