Skip to content

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.

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.

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.