# First integration

> From an empty project to a streamed chat completion: the OpenAI SDK, the Anthropic and Gemini protocol variants, error handling, and usage checks.

**What you'll build:** one Python file that sends a chat request with the OpenAI SDK, then streams the same request token by token. Along the way you'll see the two other protocols MirAPI speaks, how to read errors, and how to check what a call cost.

## Prerequisites

- Python 3.10+
- `pip install openai` — the official OpenAI SDK, used against MirAPI's OpenAI-compatible endpoint
- A `MIRAPI_API_KEY` created in the console: https://console.mirapi.ai → **API Keys**. The full key is shown only once, so copy it into your environment or a secret manager immediately.

```bash
pip install openai
export MIRAPI_API_KEY=sk-...
```

## 1. Send your first request

Create `chat.py` and point the SDK at MirAPI's OpenAI-compatible base URL, `https://api.mirapi.ai/v1`:

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.mirapi.ai/v1",
    api_key=os.environ["MIRAPI_API_KEY"],
)

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Hello! Introduce yourself in one sentence."}],
)

print(response.choices[0].message.content)
```

Run it:

```bash
python chat.py
```

You get a plain text response. Its `usage` object reports token counts only — the dollar amount is never included; you reconcile it in the console or via the usage endpoints below.

The fields you'll touch most often:

| Parameter | What it does |
|---|---|
| `model` | The exact catalogue `id` to call — here `deepseek-chat` |
| `messages` | The conversation as `role` / `content` pairs |
| `stream` | Set `true` to receive SSE deltas instead of one response |
| `max_tokens` / `max_completion_tokens` | Cap on generated tokens |
| `temperature` | Sampling randomness, `0`–`2` |
| `response_format` | Structured output (JSON mode / `json_schema`) — JSON-capable models only |
| `reasoning_effort` | Reasoning strength — reasoning-capable models only |
| `tools` / `tool_choice` | Tool calling — tool-capable models only |

Capability-gated parameters (`tools`, `response_format`, `reasoning_effort`) only work on models whose catalogue entry lists that capability under `supported_parameters`.

## 2. Stream the response

Change only the call — add `stream=True` and iterate the deltas:

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.mirapi.ai/v1",
    api_key=os.environ["MIRAPI_API_KEY"],
)

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Hello! Introduce yourself in one sentence."}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
print()
```

Text now prints as it is generated. Streaming is standard SSE — `data:` lines ending with the `data: [DONE]` sentinel — and the SDK parses it for you: no special flags or custom parsing. On reasoning models, thinking tokens arrive before the final answer.

## 3. Other protocols

MirAPI exposes three protocol front-ends to the same catalogue. Use whichever your client already speaks; the model name is the same catalogue `id`.

| Protocol | Base URL | How the key is sent |
|---|---|---|
| OpenAI-compatible | `https://api.mirapi.ai/v1` | `Authorization: Bearer sk-...` |
| Anthropic | `https://api.mirapi.ai` (no `/v1`) | `x-api-key: sk-...` (or `Authorization: Bearer`) |
| Gemini | `https://api.mirapi.ai` or `https://api.mirapi.ai/v1beta` | `x-goog-api-key: sk-...` (or `?key=sk-...`) |

The OpenAI-compatible base URL already ends in `/v1`, so the SDK appends `/chat/completions`. The Anthropic client points at the bare origin and appends `/v1/messages` itself. Gemini clients accept either the bare origin or `/v1beta`.

### OpenAI-compatible

```bash
curl https://api.mirapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $MIRAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hi"}]}'
```

### Anthropic

```bash
curl https://api.mirapi.ai/v1/messages \
  -H "x-api-key: $MIRAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-chat","max_tokens":256,"messages":[{"role":"user","content":"Hi"}]}'
```

### Gemini

```bash
curl "https://api.mirapi.ai/v1beta/models/deepseek-chat:generateContent" \
  -H "x-goog-api-key: $MIRAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contents":[{"parts":[{"text":"Hi"}]}]}'
```

Full header details and key constraints live in [Authentication](/docs/api-reference/authentication).

## 4. Handle errors

| Status | Meaning | What to do |
|---|---|---|
| 401 | Key missing, invalid, or unknown | Fix the credential |
| 403 | Key valid, request rejected — balance, whitelist, quota, or IP | Read the message, fix the cause, retry |
| 413 | Request body too large | Reduce the size |
| 429 | Rate limited | Retry with backoff — there is no `Retry-After` header |
| 500 | Gateway or upstream failure | Safe to retry idempotent requests |

An empty balance returns **403**, not 401. OpenAI-protocol errors use the envelope `{"error": {"message", "type", "param", "code"}}`; `type` is `new_api_error` (gateway-side) or `upstream_error` (sanitized upstream failure).

```json
{"error": {"message": "…", "type": "new_api_error", "param": null, "code": null}}
```

Every error message ends with a request ID — include it in any support or billing ticket. MirAPI does not retry on your behalf; for 429s use jittered exponential backoff (1s → 2s → 4s, capped around 30s).

## 5. Check usage and spend

The `usage` object in a non-streaming response counts tokens only — it carries no dollar amount. Reconcile spend in three places:

- the console billing log;
- `GET /api/usage/token` — account-level summary;
- `GET /api/log/token` — per-request detail.

```bash
curl https://api.mirapi.ai/api/usage/token -H "Authorization: Bearer $MIRAPI_API_KEY"
```

A short hello costs well under a cent — a few hundred tokens at the model's per-1M-token rate. Text is billed per 1M tokens in three tiers (input / output / cache read); images are per image and video per second of output. Billing runs on a prepaid USD balance that never expires; when it empties, requests return 403 and the same key works again immediately after a top-up. See [Billing & top-ups](/docs/billing).

## Troubleshooting

- **401 on a fresh key** — the environment variable isn't set, or the key was copied incompletely. The `Bearer` scheme is case-insensitive, and a bare key without the scheme also works.
- **403 with a valid key** — most often an empty balance, a model outside the key's whitelist, a quota cap, or an IP restriction. Top up or adjust the key.
- **A "coming soon" model errors** — catalogue entries marked *Coming soon* can't be called yet; pick one marked *Available*.
- **Model not found** — send the exact `id` the catalogue returns. The `vendor/` prefix is for browsing only and is not part of a request.
- **Streaming prints nothing** — you're iterating `delta.content`, which is `None` until text arrives (and reasoning arrives first on reasoning models).
- **429 without `Retry-After`** — that's expected; back off manually as described above.

## Related links

- [Quickstart](/docs/quickstart)
- [Models & pricing](/docs/models)
- [OpenAI compatibility](/docs/openai-compat)
- [Billing & top-ups](/docs/billing)
- [Authentication](/docs/api-reference/authentication)
- [Errors](/docs/api-reference/errors)
- [Tool-calling agent](/docs/tutorials/tool-calling-agent)