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; set base_url |
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 | yes |
ChatRequest::max_tokens |
yes (defaults to 1024) | yes (no default) |
ChatRequest::cache_control |
yes | no (Anthropic-only feature) |
ChatRequest::thinking |
yes | no (Anthropic-only feature) |
Config::base_url |
yes | yes |
Config::timeout |
yes | yes |
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 genai column's gaps are covered in What rtb-chat does not do.
The model name picks the adapter¶
On the genai backend with base_url: None, 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.
Setting base_url takes routing away from the model name. The adapter is then
fixed by provider: OpenAI for OpenAi and OpenAiCompatible, Gemini for
Gemini, Ollama for Ollama. A model name carrying a namespace:: prefix for a
different adapter is rejected by genai on the first call rather than rerouted.
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 and 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.