Providers
The five translation providers, their options and API keys, and the behavior they all share.
verbatra ships five providers behind one interface: Anthropic, OpenAI, Gemini, and openai-compatible (a local or self-hosted OpenAI-compatible server) are large language models, and DeepL is a machine-translation service. You select one in the provider block of your config, by id. Every model id on this page is an example, not a recommendation: model catalogs change, so check your provider's current list.
Which one should I pick?
Any provider works with any format and command; the choice is about cost, quality, privacy, and how you get a key.
- Small or side project, want it free? Use Gemini. It has a real free API tier, so you can translate a whole app at zero cost. Grab a key from Google AI Studio and set
GEMINI_API_KEY. The free tier is rate-limited (see the note below), which only matters on a large first-time run. - Best translation quality for a production app? Use Anthropic or OpenAI. Both are paid LLMs and read
ANTHROPIC_API_KEYorOPENAI_API_KEY. - High volume, glossary-driven, or no LLM in the loop? Use DeepL. It is a dedicated machine-translation API with a free and a paid tier, and supports a native glossary by id. It does not translate placeholder-bearing strings (see the note below); for those, use an LLM provider.
- Offline, cost-free, or your strings must never leave your network? Use openai-compatible. It points verbatra at a local or self-hosted inference server (LM Studio, Ollama, vLLM) instead of a hosted API. No account, no API key, and no network egress beyond your own machine or LAN.
Not sure? Start on Gemini's free tier, then switch providers later by editing one id in your config. Nothing else changes.
Keys come from the environment
verbatra never reads an API key from the config. Each hosted provider reads exactly one environment variable:
| Provider id | Environment variable |
|---|---|
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
gemini | GEMINI_API_KEY |
deepl | DEEPL_API_KEY |
Set the variable in .env (which verbatra init gitignores) or export it in your shell or CI secret store. When the variable is unset or empty, the provider fails with a structured MISSING_API_KEY error whose message names the variable but never includes a key value. Never commit a real key.
openai-compatible is different: most local servers need no key at all, so it is not in this table. See its own section below for how its key resolution works.
Anthropic
provider: {
id: "anthropic",
options: {
model: "claude-sonnet-4-6", // example model id
maxTokens: 4096,
},
}Reads ANTHROPIC_API_KEY. model and maxTokens are both required. maxTokens caps the tokens a single response may produce; it is this provider's output-token limit option (the others call it maxOutputTokens).
OpenAI
provider: {
id: "openai",
options: {
model: "gpt-5.4-mini", // example model id
maxOutputTokens: 4096,
},
}Reads OPENAI_API_KEY. model and maxOutputTokens are both required.
Gemini
provider: {
id: "gemini",
options: {
model: "gemini-2.5-flash", // example model id
maxOutputTokens: 4096,
},
}Reads GEMINI_API_KEY. model and maxOutputTokens are both required.
The free tier and its limits
Gemini is the recommended starting point for small projects because its API has a free tier. Create a key at Google AI Studio, no billing required. The free tier caps requests per minute and per day (the exact numbers depend on the model and change over time, so check Google's current limits), and verbatra splits work into sequential sub-batches, so a first-time run over many locales can hit those caps. A brief rate-limit or server error is retried automatically with a short backoff, but that only smooths over momentary hiccups, not a sustained cap. If you do hit the cap, translate one locale at a time or lower maxBatchSize, then re-run: verbatra only picks up what is still missing, so runs are safe to repeat.
DeepL
provider: {
id: "deepl",
options: {},
}Reads DEEPL_API_KEY. DeepL is a machine-translation API, not an LLM: it has no model field and no output-token limit. The only option is an optional glossaryId naming a glossary you have already created in DeepL:
provider: {
id: "deepl",
options: {
glossaryId: "<your-glossary-id>",
},
}DeepL degrades gracefully instead of failing when it cannot honor a setting, and reports each degradation as a notice on the run:
- A configured
glossaryterm map is not applied (DeepL only uses a pre-created glossary id), reported asGLOSSARY_IGNORED. tonemaps to DeepL's formality:formalbecomes more formal,informalbecomes less formal,neutralor an absent tone is omitted. On a free-tier key, formality is not supported, so a non-default tone degrades to the default, reported asFORMALITY_DOWNGRADED.
DeepL and placeholders
DeepL cannot preserve placeholders or ICU tokens, so it does not send strings that contain them: those entries are withheld (left untranslated) and reported with a PLACEHOLDER_UNSUPPORTED notice. Placeholder-free strings translate normally. To translate placeholder-bearing strings, use an LLM provider (Anthropic, OpenAI, Gemini, or openai-compatible).
openai-compatible
Points verbatra at a server that speaks the OpenAI Chat Completions API: LM Studio, Ollama, vLLM, and similar all work. The id names the wire protocol, not where the server runs, so a hosted API that speaks the same protocol fits here too.
provider: {
id: "openai-compatible",
options: {
baseUrl: "http://192.168.178.74:1234/v1",
model: "qwen2.5-14b-instruct", // example: whatever your server exposes
maxOutputTokens: 1024,
},
}baseUrl, model, and maxOutputTokens are required. Unlike every other provider, baseUrl lives in config rather than the environment: it is a network address you already know (a LAN IP or localhost), not a secret. It must be a valid absolute URL using the http or https scheme; anything else, including a missing scheme, fails config validation immediately.
baseUrl must include your server's API path segment, typically /v1: LM Studio, Ollama, and vLLM all serve their OpenAI-compatible routes under /v1. A baseUrl without it is still a syntactically valid URL, so it passes config validation and instead fails at request time by reaching the wrong path.
A hosted API that speaks the same protocol works the same way. Mistral's chat completions API is one example:
provider: {
id: "openai-compatible",
options: {
baseUrl: "https://api.mistral.ai/v1",
model: "mistral-large-latest", // example: check Mistral's current model list
maxOutputTokens: 4096,
apiKeyEnvVar: "MISTRAL_API_KEY",
},
}apiKeyEnvVar names the environment variable holding your key for that server. This is the same openai-compatible provider pointed at a different endpoint, not a dedicated Mistral provider.
Keys are optional
Most local inference servers need no API key at all, so openai-compatible resolves its key in three tiers, in order:
apiKeyEnvVar, an optional config field naming an environment variable to read a real key from. If you set this field and its named variable is unset or empty, verbatra fails withMISSING_API_KEYinstead of silently falling back, since you explicitly said a key is required.OPENAI_COMPATIBLE_API_KEY, a convention environment variable, read whenapiKeyEnvVaris not set. If it is set and non-empty, verbatra uses it; if not, verbatra moves on without an error.- The placeholder
"local", sent when neither of the above resolves anything. This is a fixed, non-secret string, not a real key, and is exactly what makes a keyless local server work with zero configuration.
apiKeyEnvVar can never name ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or DEEPL_API_KEY: config validation rejects that outright, so a config cannot point this provider at a hosted provider's key by name. The client also never reads OPENAI_API_KEY itself and always passes an explicit key value to the underlying SDK, so a hosted key can never reach a custom baseUrl.
A real key over plaintext http travels in cleartext
verbatra allows plaintext http:// to any host, including a LAN address, with no restriction beyond the http or https scheme. This is intentional and safe in the common keyless case: nothing secret is on the wire.
It is a real risk only when you also configure a key (via apiKeyEnvVar or OPENAI_COMPATIBLE_API_KEY) and baseUrl is plaintext http:// to a non-loopback host: that key then travels over your network unencrypted. If your server requires a real key and is reachable beyond localhost, use https:// for it. verbatra does not detect or warn about this combination today; treat it as your own responsibility.
Choosing a local model
Prefer an instruction-tuned model without reasoning and with strong multilingual coverage. verbatra asks for a single strict JSON object and validates it, so a model that reliably follows format instructions is what you want. qwen2.5-14b-instruct is a reasonable default; pick the largest such model your hardware runs well.
Avoid reasoning and heavily quantized models
Reasoning ("thinking") models and heavily quantized (qat or low-bit) builds are the usual sources of failure here: they tend to emit reasoning prose around the answer or return truncated JSON, and either one fails verbatra's schema and integrity checks. If a reasoning-capable model is unavoidable, give it room to both think and answer: see Output-token limits for the maxOutputTokens and maxBatchSize tuning.
A bad local model fails loudly, not silently: verbatra validates every provider response against a canonical schema plus placeholder and ICU integrity, so a model that emits reasoning prose or truncated JSON is rejected with an INVALID_RESPONSE or OUTPUT_TRUNCATED error instead of writing a corrupt translation.
Tolerant parsing for local and smaller models
openai-compatible requests the same strict, schema-constrained response format as the hosted openai provider. The one difference is on the way back: local and smaller models are more likely to wrap an otherwise-correct answer in surrounding prose or a Markdown code fence despite being asked not to, so openai-compatible extracts the first balanced JSON object anywhere in the response before parsing, where the hosted provider does not. Its output still runs through the exact same schema validation and placeholder and ICU integrity checks as every other provider: a local model's output is untrusted input, and a malformed or placeholder-mismatched response is rejected the same way.
Not in verbatra init
verbatra init does not offer openai-compatible as a scaffold option, since it has no single required environment variable to prompt for. Add the provider block above to your config by hand; everything else on this page (glossary, tone, output-token limits) applies to it like any LLM provider.
Tone and glossary across providers
The optional tone and glossary config fields (see Config file) are applied per provider:
- Tone. The LLM providers (Anthropic, OpenAI, Gemini, and openai-compatible) receive the tone with the request and are instructed to honor it. DeepL maps it to formality, with the free-tier degradation described above.
- Glossary. The LLM providers receive the term map and are instructed to treat its terms as binding. DeepL ignores a term map (with a
GLOSSARY_IGNOREDnotice) and applies only its nativeglossaryId.
Behavior every provider shares
Batching
A locale's work is split into sequential sub-batches of at most maxBatchSize entries (default 50; see Config file), so one oversized request cannot fail the whole locale. A failed sub-batch is withheld and retried automatically on the next run while the other sub-batches still make progress. DeepL additionally splits a sub-batch across as many requests as its own per-request caps require, transparently.
Retries
Transient failures (a rate-limit response or a server error) are retried automatically with a short backoff: the OpenAI, Anthropic, and DeepL SDK clients retry by default, and verbatra adds an equivalent retry of its own for Gemini, whose SDK does not. Retries smooth over momentary hiccups; a sustained failure still surfaces as a structured error.
Structured errors
A provider failure never surfaces as a raw SDK error, which could carry request headers or a key. verbatra classifies each failure by its HTTP status or SDK error class and raises a structured error with a stable code and a fixed, secret-free message:
| Code | Meaning |
|---|---|
MISSING_API_KEY | The required environment variable is unset or empty. The message names the variable, never a value. |
RATE_LIMITED | HTTP 429 or an SDK rate-limit error. Back off and retry later. |
TIMEOUT | A network or request timeout; no response arrived in time. Retrying may help. |
AUTH_FAILED | HTTP 401 or 403: the key is invalid, revoked, or lacks permission. Retrying will not help. |
OUTPUT_TRUNCATED | The model hit its output-token limit; see below. |
INVALID_RESPONSE | The output was malformed, incomplete, or failed reconciliation against the request. |
PROVIDER_REFUSED | The model declined to answer. |
PROVIDER_BLOCKED | The request or response was safety-blocked or filtered. |
PROVIDER_ERROR | Anything unclassifiable, mapped to a static, secret-free message. |
A failure affects only its own sub-batch: the failed keys are withheld and picked up again on the next run.
Output-token limits
The four LLM providers cap how many tokens a single response may produce. When a model stops because it hit that cap, verbatra raises OUTPUT_TRUNCATED instead of a generic malformed-response failure, with a fixed message:
The provider stopped because the output-token limit was reached. Reduce the batch size or raise the configured max output tokens.
Either lever resolves it: lower maxBatchSize in your config so each request produces a smaller response, or raise the per-response cap on the provider (maxTokens for Anthropic, maxOutputTokens for OpenAI, Gemini, and openai-compatible). DeepL is not affected: it has no output-token truncation case. verbatra also recovers on its own: when a sub-batch is truncated it re-splits that sub-batch into halves and retries, down toward a single entry, so the keys that fit a smaller request still get translated and only a lone entry that still overflows is withheld for the next run.
Reasoning models spend the same budget
A reasoning (thinking) model spends its hidden reasoning tokens from the same output-token budget it uses to emit the JSON translations. It can exhaust that budget on reasoning alone and stop before writing any output, which surfaces as OUTPUT_TRUNCATED. If you use a reasoning model, give it plenty of headroom: prefer a higher maxOutputTokens (maxTokens for Anthropic) or a smaller maxBatchSize so each request has room for both the reasoning and the response.
Review flags
Every provider's accepted translations run through the same post-translation checks, and a suspicious result (for example one identical to its source, or one that missed a glossary term) is flagged into the needs-review queue rather than rejected. The reason codes and how to work the queue are covered in Translation safety and Review in Studio.
Edit on GitHub