Translating text through the API: a pipeline for large volumes
Translating one paragraph is easy. Translating ten thousand lines without double charges and with a predictable price is a different task.
4 min · · Updated · автоматизация · перевод
The right model for the job, not the default one
Translation isn't the most creative task, so running the entire volume through the most expensive model makes no sense. For straightforward translation of technical and marketing text, start with the cheap glm-5.3-flash ($0.04/$0.04 per 1M tokens) and gpt-5.6-luna ($0.10/$0.10). For literary text or anything with wordplay where tone matters, try claude-sonnet-5 ($0.30/$0.30) — check on your own texts whether it holds style better across long passages. You don't have to pick one model for the whole pipeline: run the first pass with a cheap model, then re-run the paragraphs an editor flagged through claude-opus-5-5.
Estimating cost before running
Before pushing ten thousand lines through, it helps to count tokens for free:
curl https://api.zukko.pro/v1/messages/count_tokens \
-H "Authorization: Bearer fetch_your_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"text to translate..."}]}'The call costs nothing and never reaches the model, so you can run the whole corpus through it beforehand and estimate a budget using the model's price from GET /v1/models/info, which also lists the limits.
Batch processing without double charges
Network drops on a large volume are routine: the client times out, you don't know whether the line's translation was billed, and you retry. To avoid paying twice for the same line, attach an Idempotency-Key header with a unique value per line (for example a hash of the text plus the line number):
import hashlib, requests
def translate_line(text, key):
idem = hashlib.sha256(f"{key}:{text}".encode()).hexdigest()
resp = requests.post(
"https://api.zukko.pro/v1/chat/completions",
headers={
"Authorization": "Bearer fetch_your_key",
"Idempotency-Key": idem,
},
json={
"model": "glm-5.3-flash",
"messages": [
{"role": "system", "content": "Translate to English, keep the formatting."},
{"role": "user", "content": text},
],
},
timeout=30,
)
return resp.json()Repeating the request with the same Idempotency-Key and the same body within 24 hours returns the same response without a new charge. If the body changes while the key stays the same, you get 422 idempotency_conflict — an immediate signal that your retry logic has drifted somewhere.
Caching repeated fragments
A large corpus usually has repeated pieces — boilerplate phrases, recurring interface strings, legal wording. For those, turn on response caching with the X-Zukko-Cache: true header (temperature: 0 also enables the cache, but only on models that accept that parameter; Claude models — claude-sonnet-5, claude-opus-5-5 — may reject a temperature other than 1 with a 400, so don't pass it to them): then an identical request from the same user gets cached for up to 7 days for chat, and a repeat comes back instantly and free. This only works on a fully identical request body — the same fragment with the same system prompt and parameters; similar but not identical pieces don't hit the cache. You can see it in the response via the X-Zukko-Cache: hit header — worth logging the hit rate on your side, since such hits aren't written to /v1/usage.
Batching several lines into one request
Sending one short sentence per request is expensive in overhead: every call pays the round-trip cost regardless of text length. It's more practical to group a batch of lines — 20-50 depending on length — into one request as a numbered list, asking for the translation back in the same order with the same numbers. That means fewer calls for the same volume of text, and in this case the Idempotency-Key gets attached to the whole batch rather than to each line separately.
Limits and what to do on 429
The gateway has no rate limit of its own. A 429 from Zukko means a specific key's spend limit: key_daily_limit_reached (resets at 00:00 UTC) or key_monthly_limit_reached, if the key has a daily or monthly limit set in the dashboard. Other 429s during a parallel run come from the model provider — and on those the right move is not to keep firing requests but to back off exponentially and reduce worker concurrency. If the pipeline runs unattended overnight, a key limit also acts as insurance against a retry-loop bug burning through the whole balance in an hour.
A fallback model when the primary one fails
If the primary translation model is unavailable (503 provider_unavailable or 503 model_disabled), the script should be able to switch to a backup without stopping the whole pipeline. Usually that's a neighboring model of similar tier: gpt-5.6-luna as a fallback for glm-5.3-flash, claude-opus-5-5 as a fallback for claude-sonnet-5. It makes sense to keep the fallback list in config rather than in code, so the order can change without a deploy.
FALLBACK = {
"glm-5.3-flash": "gpt-5.6-luna",
"claude-sonnet-5": "claude-opus-5-5",
}
def translate_with_fallback(text, model="glm-5.3-flash"):
try:
return translate_line(text, model)
except requests.HTTPError as e:
if e.response.status_code == 503 and model in FALLBACK:
return translate_line(text, FALLBACK[model])
raiseQuality control with spot-check back-translation
On a large volume there's no point proofreading every line by hand. It's more practical to take a random sample — say every hundredth line — and translate it back into the source language with the same model. If the back-translation diverges significantly in meaning from the original, that line goes into a manual review queue. This won't catch stylistic issues, but it reliably catches lost meaning and cases where the model invented something that wasn't in the source.
Verifying results and tracking spend
While the pipeline runs, it's worth reading the X-Zukko-Cost-Micros header on every response (regular, non-streaming responses carry it) and logging it — that gives you an average price per translated line and lets you catch an anomaly if some model suddenly starts generating three times more tokens than usual. Once done, the full history (shared across the whole account, all keys together) can be reconciled through GET /v1/usage with cursor pagination, comparing actual spend against what count_tokens estimated up front.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- An AI agent for job hunting: a résumé tailored to the job and cover lettersWe build a small tool: you give a vacancy text and a file with your real experience, and get a tailored résumé, a letter and a list of gaps. No invented skills.
- Fiction with AI: how to write together, not instead of youHow to work with a model on prose: build a project bible, hold the style, edit in passes and estimate the cost of a long text.
- AI for testers: test cases, checklists and bug reportsHow a QA engineer can use a model for test cases, checklists and bug reports, check coverage, and avoid requirements the model made up.
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.