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.
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.
| Variant | Backend | Default endpoint |
|---|---|---|
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.
| Capability | Anthropic-direct | genai |
|---|---|---|
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.
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:
| Model name | genai adapter |
|---|---|
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:
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¶
| Provider | Mechanism |
|---|---|
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 before building a second client.
Anthropic-direct request headers¶
Every Anthropic-direct call sends:
| Header | Value |
|---|---|
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.