Errors¶
Every fallible call returns Result<_, AiError>. AiError is
#[non_exhaustive], derives Clone, and implements both std::error::Error
(via thiserror) and miette::Diagnostic.
AiError variants¶
| Variant | Raised when | Typical fix |
|---|---|---|
InvalidConfig(String) |
AiClient::new rejects the config, or a schema fails to serialise |
fix the config; see Configuration |
Provider(String) |
the provider returned a non-2xx that is not 401/403/429, or a malformed body | read the message — it carries the provider's own explanation |
Transport(String) |
DNS, TCP, TLS, timeout, or an interrupted body read | retry; check the endpoint and the network |
Auth(String) |
the provider returned 401 or 403 | check the API key and its permissions |
RateLimited { host, retry_after } |
the provider returned 429 | back off for retry_after, or longer |
SchemaValidation(String) |
chat_structured got JSON that does not match T's schema |
loosen the type, or make the prompt more specific |
Deserialize(String) |
chat_structured got something that is not JSON, or JSON serde rejects |
usually a fenced or truncated reply — raise max_tokens |
Diagnostic codes still say rtb::ai¶
The miette diagnostic codes were assigned before the crate was renamed from
rtb-ai, and they were not renamed with it:
| Variant | Code |
|---|---|
InvalidConfig |
rtb::ai::config |
Provider |
rtb::ai::provider |
Transport |
rtb::ai::transport |
SchemaValidation |
rtb::ai::schema |
Deserialize |
rtb::ai::deserialize |
Auth |
rtb::ai::auth |
RateLimited |
rtb::ai::rate_limited |
If you match on codes, or grep logs for them, use rtb::ai::*. They are a public
contract, so renaming them would be a breaking change.
How HTTP status codes map¶
Applies to the Anthropic-direct backend, on both chat and chat_stream.
| Status | Variant |
|---|---|
| 2xx | success |
| 401, 403 | Auth |
| 429 | RateLimited |
| any other non-2xx | Provider |
Provider messages are built as status {status} from {host}: {detail}, where
detail is pulled out of the body in this order:
error.message— the Messages API's own error shape;- a top-level
messagefield; - the raw body, truncated to 500 characters on a character boundary and
suffixed with
….
An empty body becomes (empty response body). A body that cannot be read at all
does not mask the status — the detail is simply empty.
So a real credit-balance failure reads:
provider error: status 400 Bad Request from api.anthropic.com: Your credit balance is too low to access the Anthropic API.
RateLimited is the exception: it is returned before the body is read, so it
carries no detail. retry_after is parsed only from a Retry-After header
holding an integer number of seconds. The HTTP-date form of the header parses as
None, which means "no hint", not "retry now".
genai providers never return Auth or RateLimited¶
The status mapping above is implemented in rtb-chat's own Anthropic path. The
genai backend has no equivalent: every failure genai reports — bad key, rate
limit, model not found, connection refused — is wrapped as AiError::Provider.
Matching on AiError::Auth or AiError::RateLimited to drive a retry or a
re-authentication prompt therefore works on Provider::Anthropic and
Provider::AnthropicLocal and silently never fires on the other four. Match on
AiError::Provider too, and inspect the message, if you need that behaviour
everywhere.
What redaction does to the message¶
Every String payload built from provider output goes through
rtb_redact::string before it is stored in the error. That scrubber removes:
- userinfo in a URL (
https://user:token@host→https://[redacted]@host); Authorization-style values —Bearer,BasicandTokenfollowed by a credential;- sensitive query-string parameters;
- PEM private-key blocks;
- well-known credential prefixes, JWT-shaped strings, and long opaque runs of token characters.
So an error message can arrive with [redacted] in the middle of it. That is the
redactor working, not a truncated response. The marker is lower-case — the
upper-case [REDACTED] on Config::api_key comes from secrecy's own Debug,
a different mechanism.
Messages that rtb-chat writes itself — api_key must not be empty, model must
not be empty, the validate_base_url rejections — are literals and are not run
through the redactor. RateLimited::host is not redacted either; it is a
hostname, which is what makes the error useful.
Displaying an error¶
AiError implements Display, so {e} gives the human-readable form. Under
miette with the fancy feature — which this crate enables — the diagnostic code
is shown alongside it:
match client.chat(req).await {
Ok(resp) => println!("{}", resp.message.content[0].as_text().unwrap_or_default()),
Err(e) => eprintln!("{e}"),
}
Errors are Clone, so one can be stashed for a retry decision and reported later
without borrowing games.