ConfigurationProviders

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_KEY or OPENAI_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 idEnvironment variable
anthropicANTHROPIC_API_KEY
openaiOPENAI_API_KEY
geminiGEMINI_API_KEY
deeplDEEPL_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 glossary term map is not applied (DeepL only uses a pre-created glossary id), reported as GLOSSARY_IGNORED.
  • tone maps to DeepL's formality: formal becomes more formal, informal becomes less formal, neutral or an absent tone is omitted. On a free-tier key, formality is not supported, so a non-default tone degrades to the default, reported as FORMALITY_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:

  1. 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 with MISSING_API_KEY instead of silently falling back, since you explicitly said a key is required.
  2. OPENAI_COMPATIBLE_API_KEY, a convention environment variable, read when apiKeyEnvVar is not set. If it is set and non-empty, verbatra uses it; if not, verbatra moves on without an error.
  3. 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_IGNORED notice) and applies only its native glossaryId.

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:

CodeMeaning
MISSING_API_KEYThe required environment variable is unset or empty. The message names the variable, never a value.
RATE_LIMITEDHTTP 429 or an SDK rate-limit error. Back off and retry later.
TIMEOUTA network or request timeout; no response arrived in time. Retrying may help.
AUTH_FAILEDHTTP 401 or 403: the key is invalid, revoked, or lacks permission. Retrying will not help.
OUTPUT_TRUNCATEDThe model hit its output-token limit; see below.
INVALID_RESPONSEThe output was malformed, incomplete, or failed reconciliation against the request.
PROVIDER_REFUSEDThe model declined to answer.
PROVIDER_BLOCKEDThe request or response was safety-blocked or filtered.
PROVIDER_ERRORAnything 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