# OpenAI compatibility

> How MirAPI implements the OpenAI protocol — base URLs, authentication, supported endpoints, streaming, tool calling, and the differences from the OpenAI API.

MirAPI implements the OpenAI protocol — any code written for the OpenAI SDK keeps working once you point it at MirAPI's base URL. The Anthropic and Gemini protocols are also supported natively, each on its own base URL, so a single key covers all three.

## Base URLs

| Protocol | Base URL | Authentication |
|---|---|---|
| OpenAI | `https://api.mirapi.ai/v1` | `Authorization: Bearer` |
| Anthropic | `https://api.mirapi.ai` | `x-api-key` or `Authorization: Bearer` |
| Gemini | `https://api.mirapi.ai` or `https://api.mirapi.ai/v1beta` | `x-goog-api-key` or `?key=` |

Rule of thumb: when a tool appends its own path (for example `/chat/completions`), set its base URL to `https://api.mirapi.ai/v1`. Anthropic's `/v1/messages` endpoint is served from the bare root, so point the Anthropic SDK at `https://api.mirapi.ai`.

## Authentication

OpenAI-protocol requests authenticate with `Authorization: Bearer sk-...`. The scheme is case-insensitive, and a bare key without the scheme is also accepted. Anthropic endpoints also accept `x-api-key`; Gemini endpoints accept `x-goog-api-key` or a `?key=` query parameter.

Keys are constrained by four rules — model whitelist, validity period, quota cap, and IP allowlist — and a key that violates one of them is rejected with 403. The `OpenAI-Organization` header is accepted but ignored: MirAPI has no organization scoping. See [Authentication](/docs/api-reference/authentication).

```bash
curl -X POST "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": "Hello!"}]
  }'
```

## Endpoint support

| Endpoint | Status |
|---|---|
| `GET /v1/models` · `GET /v1beta/models` | Supported |
| `POST /v1/chat/completions` | Supported |
| `POST /v1/responses` · `POST /v1/responses/compact` | Supported |
| `POST /v1/messages` (Anthropic) | Supported |
| `POST /v1beta/models/{model}:generateContent` (Gemini) | Supported |
| `POST /v1/embeddings` · `POST /v1/engines/{model}/embeddings` | Supported |
| `POST /v1/images/generations` · `POST /v1/images/edits` | Supported |
| `POST /v1/audio/transcriptions` · `POST /v1/audio/translations` · `POST /v1/audio/speech` | Supported |
| `POST /v1/videos` · `GET /v1/videos/{task_id}` · `GET /v1/videos/{task_id}/content` | Supported (async) |
| `POST /v1/video/generations` · `GET /v1/video/generations/{task_id}` | Supported (async) |
| Kling / Jimeng video compatibility routes | Supported |
| `POST /v1/rerank` · `POST /v1/moderations` | Supported |
| `GET /v1/realtime` (WebSocket) | Supported |
| `POST /v1/messages/count_tokens` | Not implemented (404) |
| `/v1/files*` · `/v1/fine-tunes*` | Not implemented (404) |

## Requests

```python
import os
from openai import OpenAI

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

resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Explain prompt caching in one sentence."}],
)

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

The core parameters are:

| Parameter | Purpose |
|---|---|
| `model` | Model id, exactly as returned by `GET /v1/models` |
| `messages` | Conversation history; `image_url` content parts enable vision input |
| `stream` | Set `true` for SSE streaming |
| `tools` / `tool_choice` | Function calling |
| `response_format` | Structured JSON output (`json_schema` or JSON mode) |
| `reasoning_effort` | Reasoning intensity, for models that support it |

`POST /v1/responses` (and `/v1/responses/compact`) serve OpenAI's newer Responses format as an alternative to chat completions.

## Streaming

Streaming is standard SSE, identical to OpenAI: pass `"stream": true`, read `data:` lines, and stop at `data: [DONE]`. It works for chat completions and responses; reasoning content arrives in the stream before the answer. Non-streaming `usage` reports token counts only — never a dollar amount.

```python
stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Count from 1 to 5."}],
    stream=True,
)

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

## Tool calling

Function calling follows the OpenAI format — `tools` and `tool_choice`:

```python
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "What is the weather in Paris?"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather in a city",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    }],
)

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

Tool calling works only for models that support it — check the capability flags in the [catalogue](/docs/models). A full loop (define → call → execute → feed back → final answer) is in the [tool-calling tutorial](/docs/tutorials/tool-calling-agent).

## Structured outputs

Ask for structured JSON with `response_format`. Both JSON mode and strict `json_schema` are supported; the model must have JSON output capability.

```python
resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Name two planets."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "planets",
            "schema": {
                "type": "object",
                "properties": {"planets": {"type": "array", "items": {"type": "string"}}},
                "required": ["planets"],
            },
        },
    },
)
```

See [Structured outputs](/docs/guides/structured-outputs).

## Errors

| Status | Meaning | What to do |
|---|---|---|
| `401` | Key missing, invalid, or unknown | Fix your credentials |
| `403` | Key valid but rejected (balance, whitelist, quota, IP) | Resolve the cause, then retry |
| `413` | Payload too large | Reduce the request size |
| `429` | Rate limited | Back off; no `Retry-After` header is sent |
| `500` | Server error | Safe to retry idempotent requests |

OpenAI-style errors use the envelope `{"error":{"message","type","param","code"}}`; `type` is `new_api_error` (gateway side) or `upstream_error` (upstream failure, message sanitized). Every message ends with a request ID — include it in support tickets. The gateway does not retry for you: on 429, use jittered exponential backoff (1s → 2s → 4s, capped around 30s). See [Errors](/docs/api-reference/errors).

## Billing

Requests are billed per token (per-1M USD tiers for input, output, and cache reads), per image, or per second of video output. Reasoning tokens are billed as output, and cache reads cost less than normal input. The response `usage` object contains no amount: reconcile dollar cost in the console billing log, `GET /api/usage/token`, or `GET /api/log/token`. When the balance runs out, requests return 403 and recover immediately after you top up. See [Billing](/docs/billing).

## Known differences from OpenAI

- **Model names** — use the exact id from `GET /v1/models` (for example `deepseek-chat`). `vendor/`-prefixed ids are for browsing and filtering in the console, not for API requests.
- **`usage` has no amount** — it reports token counts only; the dollar cost lives in the console and `/api/log/token`.
- **403 means "rejected", not "unauthenticated"** — a valid key rejected for balance, whitelist, quota, or IP returns 403.
- **No organization scoping** — `OpenAI-Organization` is accepted but ignored.
- **Prompt caching is automatic** — OpenAI-compatible models cache prompts automatically; confirm hits via `usage.prompt_tokens_details.cached_tokens`. See [Prompt caching](/docs/guides/prompt-caching).
- **Errors are sanitized** — upstream error messages have vendor-internal details stripped and replaced with a stable message.
- **429 has no `Retry-After`** — the gateway never retries; back off client-side.
- **Realtime is WebSocket-only** — `GET /v1/realtime` upgrades to a `wss://` connection; there is no HTTP response. See [Realtime speech](/docs/guides/realtime).
- **No files, fine-tuning, or `count_tokens`** — these endpoints return 404.

## Troubleshooting

- **401?** The key is missing, invalid, or unknown — verify it and retry.
- **403 on a request that used to work?** Check the balance and key constraints (whitelist, quota, IP). Topping up restores the key immediately.
- **429?** Back off with jitter (1s → 2s → 4s, capped ~30s).
- **Model not found?** Use the exact id from `GET /v1/models` and drop any `vendor/` prefix.
- **Double `/v1` in the URL?** If your SDK appends its own path, point it at `https://api.mirapi.ai/v1`, not a deeper path.

## Related links

- [Quickstart](/docs/quickstart)
- [Configuration](/docs/configuration)
- [Models](/docs/models)
- [Billing](/docs/billing)
- [Authentication](/docs/api-reference/authentication)
- [Errors](/docs/api-reference/errors)
- [Prompt caching](/docs/guides/prompt-caching)
- [Structured outputs](/docs/guides/structured-outputs)