Configuration¶
Config is the only input to AiClient::new. It is a plain struct with public
fields and a Default impl, so the usual construction is to set what you need and
spread the rest:
use rtb_chat::{Config, Provider};
use secrecy::SecretString;
let config = Config {
provider: Provider::Anthropic,
model: "claude-opus-4-7".into(),
api_key: SecretString::from(std::env::var("ANTHROPIC_API_KEY")?),
..Config::default()
};
Config fields at a glance¶
| Field | Type | Default | Honoured on |
|---|---|---|---|
provider |
Provider |
Provider::Anthropic |
both backends |
model |
String |
"claude-opus-4-7" |
both backends |
base_url |
Option<Url> |
None |
both backends |
api_key |
SecretString |
empty (invalid) | both backends |
timeout |
Duration |
60 s | both backends |
allow_insecure_base_url |
bool |
false |
both backends |
Config derives Debug and Clone. It derives neither Serialize nor
Deserialize, so it cannot be read from a config file directly. The tool layer
builds it in code, typically after resolving the key through
rtb_credentials.
provider: which backend and which wire protocol¶
Provider is #[non_exhaustive], so match on it with a _ arm. The six variants
and what each one selects are in Providers.
Without a base_url, provider alone does not determine where a
genai-backed request goes: the model name picks the adapter. See
The model name picks the adapter.
model: required, and never inferred¶
A provider-specific model identifier. Config::default() sets
"claude-opus-4-7".
An empty model is rejected by AiClient::new:
The value is passed through unchanged. rtb-chat does not validate it against a
list of known models, and does not map friendly names to versioned ones. A model
the provider does not recognise surfaces as a provider error on the first call,
not at construction.
base_url: overriding the endpoint¶
None means "use the vendor's default endpoint".
On the Anthropic-direct backend the override is honoured. The request URL is
built by trimming any trailing / and appending /v1/messages, so
https://gateway.internal/anthropic/ becomes
https://gateway.internal/anthropic/v1/messages. With base_url: None the
default is https://api.anthropic.com.
On the genai backend the override replaces genai's endpoint, and genai
appends its own path to it: chat/completions for the OpenAI adapter, api/chat
for Ollama. A trailing / is added if missing, so
https://api.together.xyz/v1 becomes
https://api.together.xyz/v1/chat/completions. Setting base_url also fixes the
genai adapter to the one provider names, rather than the one genai would infer
from the model name. See
The model name picks the adapter.
When base_url is Some, AiClient::new runs it through
validate_base_url.
api_key: always required, even for Ollama¶
Held as a secrecy::SecretString: Debug prints [REDACTED] and the memory is
zeroed on drop.
AiClient::new rejects an empty key before anything else:
That check runs for every provider, including Provider::Ollama, which needs no
credential at all. A local Ollama client still has to be given some non-empty
placeholder string. It is never sent anywhere: the genai Ollama adapter defines no
API-key environment variable, and rtb-chat sets none for it.
Where the key goes afterwards depends on the backend:
- Anthropic-direct: sent as the
x-api-keyrequest header. - genai: written into the process environment by
AiClient::new, asOPENAI_API_KEY(forOpenAiandOpenAiCompatible) orGEMINI_API_KEY(forGemini). See The genai backend puts your key in the process environment before you build more than one client.
timeout: per request¶
A whole-request timeout on the HTTP client each backend builds: the reqwest
client on the Anthropic-direct backend, and the one genai builds on the genai
backend. Default 60 s.
A timeout expiring surfaces as AiError::Transport on the Anthropic-direct
backend, and as AiError::Provider on genai, which reports every failure that
way.
allow_insecure_base_url: the test escape hatch¶
false in production, and there is no way to set it from a config file. Config
has no Deserialize impl, so nothing outside your own Rust code can turn it on.
Setting it to true does two things:
validate_base_urlaccepts anhttp://URL.- The Anthropic-direct
reqwestclient is built withouthttps_only, so it will actually follow through to a plaintext host.
It does not restrict you to loopback. allow_insecure_base_url: true with
base_url: Some("http://some-public-host/") is accepted, and the API key crosses
the wire in plaintext. Confine it to tests. Test without a
provider shows the intended use.
validate_base_url: what gets rejected¶
validate_base_url(&Url, allow_insecure: bool) -> Result<(), AiError> is public,
so you can run the same check at config-parse time and reject a bad endpoint
before you get as far as building a client.
AiClient::new calls it only when base_url is Some. Every rejection is
AiError::InvalidConfig.
| Input | Result |
|---|---|
https://api.anthropic.com |
accepted |
http://127.0.0.1:8080 with allow_insecure = false |
rejected: scheme not permitted |
http://127.0.0.1:8080 with allow_insecure = true |
accepted |
ftp://host/ or any non-http(s) scheme |
rejected, even with allow_insecure = true |
https://user:pw@host/ |
rejected: userinfo |
https://example.com, https://api.example.com |
rejected: placeholder host |
https://example.org, https://x.example.org |
rejected: placeholder host |
https://example.net |
accepted; only example.com and example.org are blocked |
Host matching is case-insensitive and covers subdomains of the two blocked domains. Nothing else in the URL is inspected: paths, ports and query strings pass through untouched.
Why these three rules and not others is covered in Why the endpoint is validated.
What AiClient::new logs¶
One tracing event at INFO on success, carrying the provider and the endpoint
host only:
The path, the query string and the API key are never logged. With base_url unset
the host is the vendor default for the provider; note that Provider::Ollama
reports localhost and Provider::OpenAiCompatible reports the literal string
openai-compatible, because there is no real endpoint to name yet.
Errors from AiClient::new¶
Construction fails only on configuration, never on the network. Nothing is sent
until you call chat, chat_stream or chat_structured.
| Condition | Error |
|---|---|
api_key empty |
AiError::InvalidConfig("api_key must not be empty") |
model empty |
AiError::InvalidConfig("model must not be empty") |
base_url fails validation |
AiError::InvalidConfig(...) from validate_base_url |
reqwest::Client fails to build |
AiError::InvalidConfig(...) with the reqwest message, redacted |
The checks run in that order, so an empty key masks a bad URL. Fix them one at a time.