---
title: Providers
description: The six Provider variants, the endpoint each talks to, where the API key goes, and which ChatRequest fields survive the trip.
tags: [reference, providers]
---
# Providers
`Provider` selects one of two backends. Everything else about a provider — the
endpoint, the auth mechanism, which request fields survive — follows from that
choice.
```rust
pub enum Provider {
Anthropic,
AnthropicLocal,
OpenAi,
OpenAiCompatible,
Gemini,
Ollama,
}
```
The enum is `#[non_exhaustive]`: a `match` on it needs a `_` arm, and new variants
can arrive in a minor release.
## Which provider uses which backend
`Provider::is_anthropic()` returns `true` for `Anthropic` and `AnthropicLocal`
and `false` for the rest. That is the whole dispatch rule.
| `Anthropic` | Anthropic-direct (`reqwest`) | `https://api.anthropic.com/v1/messages` |
| `AnthropicLocal` | Anthropic-direct (`reqwest`) | `https://api.anthropic.com/v1/messages` — see below |
| `OpenAi` | genai | `https://api.openai.com/v1/` |
| `OpenAiCompatible` | genai | whatever the model name resolves to |
| `Gemini` | genai | `https://generativelanguage.googleapis.com/v1beta/` |
| `Ollama` | genai | `http://localhost:11434/` |
## AnthropicLocal does not default to a local endpoint
`AnthropicLocal` is for a self-hosted Anthropic-compatible endpoint — an in-house
proxy, a gateway, Claude Code Local. The wire format is identical to Cloud, which
is why it shares the backend.
What it does **not** do is change the default endpoint. With `base_url: None`,
`Provider::AnthropicLocal` sends to `https://api.anthropic.com/v1/messages`,
exactly like `Provider::Anthropic`. Always set `base_url` when you use it.
The variant still matters: it documents intent, and it is the one to keep an eye
on if a future release starts treating the two differently.
## Feature support by backend
Everything in this table was checked against the request builders in
`src/client.rs` and `src/anthropic.rs`.
| `ChatRequest::system` | yes | yes |
| `ChatRequest::messages` | yes | yes |
| `ChatRequest::temperature` | yes | **dropped** |
| `ChatRequest::max_tokens` | yes (defaults to 1024) | **dropped** |
| `ChatRequest::cache_control` | yes | no — Anthropic-only feature |
| `ChatRequest::thinking` | yes | no — Anthropic-only feature |
| `Config::base_url` | yes | **ignored** |
| `Config::timeout` | yes | **ignored** |
| `ChatResponse::citations` | yes, when the model emits them | always empty |
| `Usage` cache fields | yes | always `0` |
| `AiError::Auth` on 401/403 | yes | no — arrives as `AiError::Provider` |
| `AiError::RateLimited` on 429 | yes | no — arrives as `AiError::Provider` |
| `ChatStreamEvent::ThinkingToken` | yes, from `thinking_delta` | yes, from genai reasoning chunks |
| `chat_structured` | yes | yes |
The four entries in bold are the ones that surprise people, because the field
exists, compiles, and does nothing. They are covered in
[What rtb-chat does not do](../explanation/03-limitations.md).
## The model name picks the adapter
On the genai backend, `Provider` decides which environment variable the key is
written to — and nothing else. Routing is genai's, and genai routes on the **model
name**:
| `gpt-*`, `o1*`, `o3*`, `o4*`, `chatgpt*`, `codex*` | OpenAI — or OpenAI Responses for `gpt-5*` and the codex/pro variants |
| `gemini*` | Gemini |
| `claude*` | Anthropic |
| `grok*`, `command*`, `deepseek-*`, `moonshot-*`, … | that vendor's adapter |
| `namespace::model` | the named adapter |
| **anything else** | **Ollama, at `http://localhost:11434/`** |
The fallback is the trap. `Provider::OpenAiCompatible` with a model name like
`llama-3.1-70b-instruct` does not go to your OpenAI-compatible endpoint. It goes
to localhost, because the name matches nothing and Ollama is genai's catch-all:
```text
provider error: Web call failed for model 'llama-3.1-70b-instruct (adapter: Ollama)'.
Cause: Reqwest error: error sending request for url (http://localhost:11434/api/chat)
```
If you need a specific adapter, name it with genai's `namespace::` prefix — for
example `together::meta-llama/Llama-3-70b`. That gets the routing right, but not
the key: `Provider::OpenAiCompatible` writes `Config::api_key` to
`OPENAI_API_KEY`, and each namespaced adapter reads its own variable
(`TOGETHER_API_KEY`, `GROQ_API_KEY`), so the call fails with `ApiKeyEnvNotFound`
unless you export that variable yourself. It still cannot change the endpoint,
which is the underlying limitation.
## Where the API key goes
| `Anthropic`, `AnthropicLocal` | `x-api-key` header on each request |
| `OpenAi`, `OpenAiCompatible` | `std::env::set_var("OPENAI_API_KEY", …)` in `AiClient::new` |
| `Gemini` | `std::env::set_var("GEMINI_API_KEY", …)` in `AiClient::new` |
| `Ollama` | nothing is set; the key is still required to be non-empty |
The environment writes are process-wide and permanent for the life of the process.
Read
[The genai backend puts your key in the process environment](../explanation/03-limitations.md#the-genai-backend-puts-your-key-in-the-process-environment)
before building a second client.
## Anthropic-direct request headers
Every Anthropic-direct call sends:
| `x-api-key` | the exposed `Config::api_key` |
| `anthropic-version` | `2023-06-01` — pinned in the crate, not configurable |
| `content-type` | `application/json` |
| `user-agent` | `rtb-chat/<crate version>` |
| `accept` | `text/event-stream`, on `chat_stream` only |
`anthropic-version` is a compile-time constant. There is no config field for it,
so a Messages API feature gated behind a newer version string needs a crate
release.