Skip to content

Batch processing

Batch processing turns a list of inputs into a list of outputs without you babysitting every call. This page builds one Python script that translates each row of input.csv (id,text) into output.csv (id,text,translation) concurrently, survives a mid-run restart, and stays under your key’s rate limit.

  • Python 3.10+
  • pip install aiohttp — the async HTTP client the script uses
  • A MIRAPI_API_KEY created in the console: https://console.mirapi.ai → API Keys
  • A chat model enabled for that key — the model whitelist applies, and a model outside it returns 403
Terminal window
pip install aiohttp
export MIRAPI_API_KEY=sk-...

Save this as batch.py:

import asyncio
import csv
import os
import random
import aiohttp
BASE = "https://api.mirapi.ai/v1"
API_KEY = os.environ["MIRAPI_API_KEY"]
MODEL = "deepseek-chat"
CONCURRENCY = 5 # at most 5 requests in flight (client-side rate limit)
MAX_RETRIES = 5
INPUT_CSV = "input.csv"
OUTPUT_CSV = "output.csv"
def load_done() -> set[str]:
"""Resume: ids already written to the output file."""
done: set[str] = set()
try:
with open(OUTPUT_CSV, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
done.add(row["id"])
except FileNotFoundError:
pass
return done
async def translate(session: aiohttp.ClientSession, text: str) -> str:
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": "Translate to English. Reply with the translation only."},
{"role": "user", "content": text},
],
}
for attempt in range(MAX_RETRIES):
try:
async with session.post(
f"{BASE}/chat/completions",
json=payload,
headers={"Authorization": f"Bearer {API_KEY}"},
) as resp:
if resp.status == 429:
# MirAPI sends no Retry-After header; back off manually.
wait = min(2**attempt + random.uniform(0, 1), 30)
await asyncio.sleep(wait)
continue
resp.raise_for_status()
data = await resp.json()
return data["choices"][0]["message"]["content"].strip()
except (aiohttp.ClientError, KeyError):
# ClientResponseError (4xx/5xx) subclasses ClientError, so
# balance, whitelist, and quota failures retry like the rest.
await asyncio.sleep(1)
raise RuntimeError(f"failed after {MAX_RETRIES} attempts")
async def main() -> None:
done = load_done()
sem = asyncio.Semaphore(CONCURRENCY)
with open(INPUT_CSV, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
write_mode = "a" if done else "w"
with open(OUTPUT_CSV, write_mode, newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["id", "text", "translation"])
if not done:
writer.writeheader()
async with aiohttp.ClientSession() as session:
for row in rows:
if row["id"] in done:
continue # checkpoint: skip already-translated rows
async with sem:
translation = await translate(session, row["text"])
writer.writerow({"id": row["id"], "text": row["text"], "translation": translation})
f.flush() # durable checkpoint after every row
asyncio.run(main())

The knobs you’ll adjust:

Constant Default What it controls
CONCURRENCY 5 Max requests in flight — your client-side rate limiter
MAX_RETRIES 5 Retries per row before the script fails that row
MODEL deepseek-chat The exact catalogue id to call
BASE https://api.mirapi.ai/v1 OpenAI-compatible base URL

The script speaks the OpenAI protocol. MirAPI exposes the same catalogue over three front-ends — the Anthropic (/v1/messages) and Gemini (...:generateContent) variants are shown in First integration.

  • Rate limiting: the semaphore caps in-flight requests at CONCURRENCY. Raise it carefully — once you exceed your key’s limit, the gateway returns 429.
  • 429 backoff: an exponential wait with jitter (2**attempt), capped at 30 seconds. MirAPI sends no Retry-After header and never retries on your behalf, so the backoff has to live in your code.
  • Resume: ids already in output.csv are skipped on restart, and each row is flushed immediately — a crash loses at most the row in flight.
Status Meaning What to do
401 Key missing, invalid, or unknown Fix the credential
403 Key valid but rejected — balance, whitelist, quota, or IP Read the message, fix the cause, retry
413 Request body too large Reduce the size
429 Rate limited Back off with jitter — there is no Retry-After header
500 Gateway or upstream failure Retry idempotent requests

An empty balance returns 403, not 401. OpenAI-protocol errors use the envelope {"error": {"message", "type", "param", "code"}}, where type is new_api_error (gateway-side) or upstream_error (a sanitized upstream failure). Every error message ends with a request ID — include it in a support or billing ticket.

The script retries aiohttp.ClientError (which covers 4xx/5xx responses) for MAX_RETRIES attempts, then gives up on that row. A persistent 403 from an empty balance will not clear itself by retrying — top up instead of spinning.

Text is billed per 1M tokens in three tiers — input, output, and cache read — with cache reads cheaper than ordinary input. A short sentence runs a few hundred tokens, so the three-row test below costs well under a cent. Reasoning tokens, when a reasoning model is used, are billed as output.

The non-streaming usage object reports token counts only — no dollar amount. Reconcile spend in three places: the console billing log, GET /api/usage/token (account-level summary), and GET /api/log/token (per-request detail). 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.

  • 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 — usually an empty balance, a model outside the key’s whitelist, a quota cap, or an IP restriction. Top up or adjust the key.
  • Model not found — send the exact id the catalogue returns. The vendor/ prefix is for browsing only and is not part of a request.
  • A “coming soon” model errors — catalogue entries marked Coming soon can’t be called yet; pick one marked Available.
  • Rows stall on 429 — that’s the backoff doing its job. Lower CONCURRENCY if it happens constantly, rather than raising MAX_RETRIES.
  • A row fails after MAX_RETRIES — the error message ends with a request ID; grab it before the script moves on.
Terminal window
printf 'id,text\n1,Hello world\n2,Bonjour le monde\n3,Hola mundo\n' > input.csv
python batch.py
cat output.csv

Kill the script mid-run with Ctrl-C and rerun it — completed rows are not re-translated.