---
title: "Providers"
description: "Every AI and machine translation provider verbatra supports, with its options and API key variable, plus the behavior all of them share."
type: "reference"
---
# Providers

Every AI and machine translation provider verbatra supports, with its options and API key variable, plus the behavior all of them share.

Select a provider by `id` in the `provider` block of your [config](https://verbatra.kreitz-webdev.de/docs/config-file.md). Anthropic, OpenAI, Gemini, OpenCode and openai-compatible are language models; DeepL, Google Cloud Translation and LibreTranslate are machine-translation services; `none` turns machine translation off. Language coverage is on [Languages and locales](https://verbatra.kreitz-webdev.de/docs/language-support.md), and what each provider receives on [Data handling and privacy](https://verbatra.kreitz-webdev.de/docs/data-handling.md).

## Which one should I pick? [#which-one-should-i-pick]

Every provider works with every format and command, and switching later means editing one `id`.

| If you want | Use |
| --- | --- |
| Free, for a small project with non-confidential strings | **Gemini**: a free API tier, whose requests Google may have humans review outside the EEA, Switzerland and the UK ([terms](https://verbatra.kreitz-webdev.de/docs/data-handling.md#vendor-retention-and-data-processing-terms)). |
| The best quality for a production app | **Anthropic** or **OpenAI**, both paid. |
| High volume, a native glossary, no LLM | **DeepL**, free or paid tier. |
| The widest coverage from an API key on Google Cloud | **Google Cloud Translation** (Basic, v2). |
| Offline, no cost, nothing leaving your network | **openai-compatible** with LM Studio, Ollama or vLLM. |
| No language model, but self-hosted | **LibreTranslate** on your own server. |
| No machine translation at all | **none**: memory and humans only. |

## Keys come from the environment [#keys-come-from-the-environment]

verbatra never reads a key from the config. Each hosted provider reads one variable:

| Provider id | Environment variable |
| --- | --- |
| `anthropic` | `ANTHROPIC_API_KEY` |
| `openai` | `OPENAI_API_KEY` |
| `gemini` | `GEMINI_API_KEY` |
| `deepl` | `DEEPL_API_KEY` |
| `google-translate` | `GOOGLE_TRANSLATE_API_KEY` |
| `opencode` | `OPENCODE_API_KEY` |

Set it in `.env` (which `verbatra init` gitignores), your shell, or your CI secret store, and never commit it. An unset or empty variable fails with [`MISSING_API_KEY`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#missing_api_key), naming the variable but never a value. `openai-compatible` and `libretranslate` usually need no key.

## Anthropic [#anthropic]

```ts title="verbatra.config.ts"
provider: {
  id: "anthropic",
  options: {
    model: "claude-sonnet-4-6", // example model id
    maxTokens: 4096,
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `model` | string | required | The model id. |
| `maxTokens` | positive integer | required | Caps one response's output tokens ([limits](#output-token-limits)); unrelated to the top-level budget. |
| `requestTimeoutMs` | positive integer | `120000` | Bounds each request attempt ([Request timeout](#request-timeout)). |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale ([Locale codes](#locale-codes-sent-to-the-provider)). |

Reads `ANTHROPIC_API_KEY`.

## OpenAI [#openai]

```ts title="verbatra.config.ts"
provider: {
  id: "openai",
  options: {
    model: "gpt-5.4-mini", // example model id
    maxOutputTokens: 4096,
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `model` | string | required | The model id. |
| `maxOutputTokens` | positive integer | required | Caps one response's output tokens. |
| `requestTimeoutMs` | positive integer | `120000` | Bounds each request attempt. |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

Reads `OPENAI_API_KEY`.

## Gemini [#gemini]

```ts title="verbatra.config.ts"
provider: {
  id: "gemini",
  options: {
    model: "gemini-2.5-flash", // example model id
    maxOutputTokens: 4096,
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `model` | string | required | The model id. |
| `maxOutputTokens` | positive integer | required | Caps one response's output tokens. |
| `requestTimeoutMs` | positive integer | `120000` | Bounds each request attempt. |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

Reads `GEMINI_API_KEY`.

<Callout title="The free tier and its limits">
A key from [Google AI Studio](https://aistudio.google.com/apikey) needs no billing, but its per-minute and per-day caps can stop a first run over many locales: translate one locale at a time or lower `maxBatchSize`, and re-run for the rest.
</Callout>

## DeepL [#deepl]

```ts title="verbatra.config.ts"
provider: {
  id: "deepl",
  options: {
    glossaryId: "<your-glossary-id>",
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `glossaryId` | string | none | A glossary you created in DeepL, applied natively. |
| `requestTimeoutMs` | positive integer | `120000` | The minimum wait per attempt, up to six attempts. |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

Reads `DEEPL_API_KEY`; `options` may be `{}`. DeepL has no model and no output-token limit, and degrades with a notice rather than failing:

- **Placeholders** travel as numbered markers in an ignored tag (`Hello {{name}}` as `Hello <x>{0}</x>`) and are restored. `PLACEHOLDER_UNSUPPORTED` withholds a value with a non-literal placeholder or other text holding `<`, `>`, `{` or `}`, and a result with a lost, repeated, changed or bare marker, an unknown entity or a stray tag. Markup alone goes as plain text. <AvailableFrom version="0.12.0" />
- **Tone** maps to formality (`formal` more, `informal` less, `neutral` omitted). A free-tier key sends no formality at all and reports `FORMALITY_DOWNGRADED`. A paid key requests `prefer_more` or `prefer_less`, so a language without formality, such as Swedish, falls back with the same notice instead of failing. The [glossary](#tone-and-glossary-across-providers) is not applied.

<AvailableFrom version="0.12.0" />

verbatra ships DeepL's dated [language table](https://developers.deepl.com/docs/getting-started/supported-languages) and refuses an unlisted language with `LOCALE_UNSUPPORTED_BY_PROVIDER` before anything is spent. A `glossaryId` works only when both languages are on DeepL's glossary list (such as `DE`, `EN`, `FR`, `JA`, `ZH`).

## Google Cloud Translation [#google-cloud-translation]

<AvailableFrom version="0.10.0" />

```ts title="verbatra.config.ts"
provider: {
  id: "google-translate",
  options: {},
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `requestTimeoutMs` | positive integer | `120000` | Bounds the whole call (no retries). |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

Reads `GOOGLE_TRANSLATE_API_KEY`, an API key for a Google Cloud project with the Cloud Translation API enabled. Only Basic (v2) works: Advanced (v3) needs a service account.

- **Placeholders** are masked as for DeepL, inside `<span translate="no">`; line breaks, tabs and double spaces are withheld too. <AvailableFrom version="0.12.0" />
- **Codes.** Each code sent is checked as well-formed BCP 47 before any call (`en_US` fails with `INVALID_REQUEST`). Regional codes are allowed; a pair Google rejects fails with `INVALID_REQUEST` from the API.

<AvailableFrom version="0.12.0" />

verbatra ships Google's dated [language table](https://docs.cloud.google.com/translate/docs/languages) and refuses an unlisted language before anything is spent. A regional code whose language is listed only as a whole, such as `de-AT`, is sent as written with a warning.

## OpenCode [#opencode]

<AvailableFrom version="0.12.0" />

Only [OpenCode Zen](https://opencode.ai/docs/zen/) models served over `/chat/completions` work here (the model table lists each model's endpoint). OpenCode keeps Go and the free Zen models for its own clients.

```ts title="verbatra.config.ts"
provider: {
  id: "opencode",
  options: {
    model: "glm-5.3", // example: a Zen model served over Chat Completions
    maxOutputTokens: 4096,
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `model` | string | required | The model id. |
| `maxOutputTokens` | positive integer | required | Caps one response's output tokens. |
| `requestTimeoutMs` | positive integer | `120000` | Bounds each request attempt. |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

Fixed endpoint, no `baseUrl`. Reads `OPENCODE_API_KEY`.

## openai-compatible [#openai-compatible]

Any server speaking the OpenAI Chat Completions API: LM Studio, Ollama, vLLM, or a hosted API.

The server must serve Chat Completions at `<baseUrl>/chat/completions` with strict JSON schema output. Gateways serving only their own client apps do not work.

```ts title="verbatra.config.ts"
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,
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `baseUrl` | `http` or `https` URL | required | The API root, usually ending in `/v1`. A URL ending in `/chat/completions` or `/completions` is rejected; one missing `/v1` passes validation, then fails at request time. |
| `model` | string | required | Whatever model your server exposes. |
| `maxOutputTokens` | positive integer | required | Caps one response's output tokens. |
| `apiKeyEnvVar` | variable name | none | The variable holding this server's key. |
| `requestTimeoutMs` | positive integer | `120000` | Bounds each request attempt. |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

For a hosted API such as Mistral:

```ts title="verbatra.config.ts"
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",
  },
}
```

The response uses the same strict schema as `openai`; the first balanced JSON object is taken from any surrounding prose or code fence and validated. `verbatra init --provider openai-compatible --base-url <url> --model <name>` writes this block ([init](https://verbatra.kreitz-webdev.de/docs/cli/init.md#providers)), and `--api-key-env-var <NAME>` adds the variable's name.

### Keys are optional [#keys-are-optional]

The key resolves in order:

1. **`apiKeyEnvVar`**: if set, its variable must hold a key, or the run fails with `MISSING_API_KEY`.
2. **`OPENAI_COMPATIBLE_API_KEY`**: used when set and non-empty.
3. **`"local"`**: a fixed, non-secret placeholder, which is what makes a keyless server work.

`apiKeyEnvVar` cannot name a hosted provider's variable, and the client never reads `OPENAI_API_KEY`, so a hosted key never reaches a custom `baseUrl`.

<Callout type="warn" title="A real key over plaintext http travels in cleartext">
With a key and a non-loopback `http://` URL, the key crosses your network unencrypted, without a warning, so use `https://` for a server that needs a key.
</Callout>

### Choosing a local model [#choosing-a-local-model]

Prefer an instruction-tuned, non-reasoning, multilingual model as large as your hardware runs well, such as `qwen2.5-14b-instruct`. Reasoning models and heavily quantized builds tend to wrap or truncate the answer, which fails as `INVALID_RESPONSE` or `OUTPUT_TRUNCATED`.

## LibreTranslate [#libretranslate]

<AvailableFrom version="0.12.0" />

A self-hosted [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) server (Argos Translate, no language model). verbatra only calls its HTTP API, so the server's AGPL-3.0 license does not extend to your project.

```ts title="verbatra.config.ts"
provider: {
  id: "libretranslate",
  options: {
    baseUrl: "http://localhost:5000",
  },
}
```

| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `baseUrl` | `http` or `https` URL | required | The API root, without `/translate`. |
| `requestTimeoutMs` | positive integer | `120000` | Bounds the whole call (no retries). |
| `localeMap` | map of locale to code | none | Overrides the code sent per locale. |

Its languages are the models installed on your server:

```bash
docker run -p 5000:5000 libretranslate/libretranslate --load-only en,de,fr
```

- **Key.** `LIBRETRANSLATE_API_KEY` is sent as `api_key` only when set. A server started with `--api-keys` fails the run with `MISSING_API_KEY` when it is unset, and with `AUTH_FAILED` when it rejects it.
- **Placeholders** are masked as for DeepL, as bare `{0}` markers; markup is kept, so only braces beside a placeholder count.
- **Markup.** A value with HTML-style tags is sent in `html` format: text inside tags is translated, the tags stay (phrasing across a tag may suffer), and changed tags are refused.
- **Language check.** Every target warns `LOCALE_UNVERIFIED_BY_PROVIDER` on every run; `verbatra doctor --locales --live` checks them against the server.

`verbatra init --provider libretranslate --base-url <url>` writes this block.

## none [#none]

<AvailableFrom version="0.12.0" />

Disables machine translation by policy, for a rule that forbids sending strings to any service.

```ts title="verbatra.config.ts"
provider: { id: "none" }
```

`options` may be omitted or `{}`. No provider is constructed, no key is read and no request is made; what each command does instead is on [Run without machine translation](https://verbatra.kreitz-webdev.de/docs/human-only-workflow.md).

## Tone and glossary across providers [#tone-and-glossary-across-providers]

A request carries only its target locale's glossary terms, and every output is checked against them.

| Provider | `tone` | `glossary` |
| --- | --- | --- |
| Anthropic, OpenAI, Gemini, OpenCode, openai-compatible | sent and honored | translations, forbidden renderings, do-not-translate terms and notes sent as binding data |
| DeepL | mapped to formality | `GLOSSARY_IGNORED`; only `glossaryId` applies |
| Google Cloud Translation, LibreTranslate | `FORMALITY_DOWNGRADED` for `formal` or `informal` | `GLOSSARY_IGNORED` |

## Behavior every provider shares [#behavior-every-provider-shares]

### Batching [#batching]

A locale's work goes out in sequential sub-batches of at most [`maxBatchSize`](https://verbatra.kreitz-webdev.de/docs/config-file.md#maxbatchsize) entries. A failed sub-batch is withheld and retried next run while the rest proceed. DeepL and Google Cloud Translation also split a sub-batch to fit their own request caps.

### Retries [#retries]

Rate limits and server errors are retried with backoff, by the client for OpenAI, OpenCode, openai-compatible, Anthropic and DeepL and by verbatra for Gemini. Google Cloud Translation and LibreTranslate do not retry. Gemini honours a stated wait up to 5 seconds and fails a longer one (an exhausted daily quota) at once with `RATE_LIMITED` naming the wait.

### Request timeout [#request-timeout]

`requestTimeoutMs` (default `120000` ms) keeps a server that never answers from stalling a run and holding a locale's write lock. Raise it for a slow local model or a large `maxBatchSize`.

```ts title="verbatra.config.ts"
provider: {
  id: "openai-compatible",
  options: {
    baseUrl: "http://192.168.178.74:1234/v1",
    model: "qwen2.5-14b-instruct",
    maxOutputTokens: 1024,
    requestTimeoutMs: 300000, // 5 minutes
  },
}
```

- **Per attempt.** Each [retry](#retries) gets the full timeout. The LLM providers also retry a timed-out attempt, and all but Gemini a connection error, so a hung server takes up to three timeouts plus backoff.
- **The error names the cause.** `TIMEOUT` means one attempt got no answer in time; repeated refused connections fail as `PROVIDER_ERROR`.
- **DeepL** cannot abort a request in flight, so an interrupted run leaves it to settle at DeepL.

### Locale codes sent to the provider [#locale-codes-sent-to-the-provider]

<AvailableFrom version="0.12.0" />

Files, the lock and the memory use your config's codes. Only the request is translated to what the provider expects:

| Provider | Source locale | Target locale |
| --- | --- | --- |
| DeepL | The language alone: `en-US` as `EN`, `zh-Hant-TW` as `ZH`. | `zh-Hant`, `zh-TW`, `zh-HK`, `zh-MO` as `ZH-HANT`; `zh-Hans`, `zh-CN`, `zh-SG` as `ZH-HANS`. DeepL variants stay (`EN-GB`, `EN-US`, `PT-BR`, `PT-PT`, `ES-419`, `FR-CA`, `FR-FR`, `DE-CH`, `DE-DE`). Spanish with a Latin American or Caribbean region (`MX`, `AR`, `CO`, `CL`, `PE`, `VE`, `EC`, `GT`, `CU`, `BO`, `DO`, `HN`, `PY`, `SV`, `NI`, `CR`, `PA`, `UY`, `PR`, `US`, `419`) as `ES-419`. Other regions and scripts are dropped: `de-AT` as `DE`. |
| Google Cloud Translation | `zh-Hant` (`zh-HK`, `zh-MO`) as `zh-TW`, `zh-Hans` (`zh-SG`) as `zh-CN`, `nb` as `no`; everything else as written. | Same as the source. |
| LibreTranslate | The language alone (`de-AT` as `de`), except `pt-BR` stays and Chinese goes as `zh-Hans` or `zh-Hant` (`zh-TW` as `zh-Hant`). | Same as the source. |
| Anthropic, OpenAI, Gemini, OpenCode, openai-compatible | As written. | Same as the source. |

An LLM also gets the [language, script and region names](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#language-script-and-region-names). DeepL has no generic English or Portuguese target, so a target ending up as bare `en` or `pt` (such as `en-AU`) is refused before the run: map it to `EN-GB`, `EN-US`, `PT-BR` or `PT-PT`.

`localeMap` overrides the code per locale:

```ts title="verbatra.config.ts"
provider: {
  id: "deepl",
  options: {
    localeMap: {
      "en-AU": "EN-GB",
      "fr-BE": "FR-FR",
    },
  },
}
```

- **Keys** must be `sourceLocale` or a target locale as configured, or the config fails with `CONFIG_INVALID`. The map goes under `options`, for every provider except `none`.
- **Values** are non-empty strings of at most 64 characters, sent unchanged: as the code to a machine-translation provider, in place of the code to an LLM (`"pt-BR (Brazilian Portuguese)"` works), whose language names still come from the key. A bare `EN` or `PT` for DeepL fails with `INVALID_REQUEST`; a value the provider rejects fails its sub-batch.
- **Effect on memory.** Only new and changed strings go out with a new code. A non-empty map is part of the [memory](https://verbatra.kreitz-webdev.de/docs/the-cache.md) fingerprint, so changing it starts a separate bucket.

### Checking a provider's language support [#checking-a-providers-language-support]

<AvailableFrom version="0.12.0" />

Every configured locale is judged before anything is spent:

| Provider | Judged against | Refused before the run | Warned about |
| --- | --- | --- | --- |
| DeepL | Its shipped language table, with glossary and formality support per language | An unlisted source or target language | A glossary for a pair outside the glossary languages, a tone without formality, a partly listed code |
| Google Cloud Translation | Its shipped language table | An unlisted language | Any glossary, a `formal` or `informal` tone, a regional code listed only as a language |
| LibreTranslate | Nothing before a run; the server's `/languages` with `doctor --live` | Never | Every target locale on every run, any glossary, a `formal` or `informal` tone |
| Anthropic, OpenAI, Gemini, OpenCode, openai-compatible | Nothing: an LLM accepts any language | Never | A language outside the [well-tested list](https://verbatra.kreitz-webdev.de/docs/language-support.md#checking-support-before-a-run) |
| none | Not applicable | Never | Never |

An unsupported locale is refused with [`LOCALE_UNSUPPORTED_BY_PROVIDER`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_unsupported_by_provider) (exit `2`) before any provider is built; a warning never refuses and appears as a [notice](https://verbatra.kreitz-webdev.de/docs/error-codes.md#sdk-notice-codes). [`verbatra doctor`](https://verbatra.kreitz-webdev.de/docs/cli/doctor.md#locale-support) reports each verdict, and its `--live` flag checks the provider's current list, but `translate` always uses the shipped table. For a language added since, map the locale in `localeMap`: an explicit code is sent with a warning, not refused.

### Structured errors [#structured-errors]

A provider failure surfaces as a [structured, secret-free error](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#errors-never-leak) affecting only its own sub-batch. A `PROVIDER_ERROR` names only the host and port of an openai-compatible or LibreTranslate `baseUrl`, and only `translation.googleapis.com` for Google, never the query carrying its key.

### Output-token limits [#output-token-limits]

The LLM providers cap each response's tokens; a model that stops at the cap raises [`OUTPUT_TRUNCATED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#output_truncated). verbatra re-splits a truncated sub-batch into halves, down toward one entry, and withholds only an entry that still overflows. To fix it for good, lower `maxBatchSize` or raise `maxTokens` (Anthropic) or `maxOutputTokens`. A reasoning model spends hidden reasoning tokens from the same budget, so give it generous headroom.

### Review flags [#review-flags]

A suspicious translation from any provider is flagged for review, not rejected. See [review reasons](https://verbatra.kreitz-webdev.de/docs/error-codes.md#review-reason-codes) and [Review in Studio](https://verbatra.kreitz-webdev.de/docs/review-in-studio.md).
