---
title: rtb-chat
description: Unified multi-provider AI chat client — genai for the mainstream, direct Anthropic Messages for the rest.
---
# `rtb-chat`
A single chat client across providers. [`genai`](https://crates.io/crates/genai)
covers the mainstream (OpenAI, Gemini, Ollama, OpenAI-compatible);
the `Claude` backend additionally drops down to direct `reqwest`
calls against the Anthropic Messages API for features `genai` does
not surface — prompt caching, extended thinking, citations.
> **Formerly `rtb-ai`.** Renamed at extraction for GTB-family name
> convergence — the Go counterpart lives at
> [chat.go.phpboyscout.uk](https://chat.go.phpboyscout.uk). Only the
> crate identity changed; the API is unchanged.
Part of the [phpboyscout Rust toolkit](https://rust.phpboyscout.uk);
extracted from — and battle-tested by —
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base).
## Public API
```rust
use rtb_chat::{AiClient, ChatRequest, Config, Message, Provider};
# async fn run() -> Result<(), rtb_chat::AiError> {
let client = AiClient::new(Config {
provider: Provider::Claude,
model: "claude-opus-4-7".into(),
..Config::default()
})?;
let reply = client.chat(ChatRequest {
messages: vec![Message::user("Summarise this changelog in one line.")],
cache_control: true,
..ChatRequest::default()
}).await?;
# Ok(())
# }
```
`Config` and `ChatRequest` are plain structs with public fields and a
`Default` impl — set what you need, spread the rest. `Provider` is
`#[non_exhaustive]`.
| `AiClient` | `new` / `chat` / `chat_stream` / `chat_structured`. |
| `Config`, `Provider` | Provider selection, model, endpoint, auth. |
| `Message`, `Role`, `ContentBlock`, `Citation`, `Usage` | Request/response message model. |
| `ChatRequest`, `ChatResponse`, `ChatStream`, `ChatStreamEvent` | Call shapes — one-shot, streaming, and structured. |
| `ThinkingMode` | Extended-thinking control on the Claude backend. |
| `AiError` | `thiserror` + `miette::Diagnostic` error enum. |
| `validate_base_url` | Endpoint allowlist guard (see below). |
Full API reference: [docs.rs/rtb-chat](https://docs.rs/rtb-chat).
## Structured output
`chat_structured::<T>()` takes any `T: serde::Deserialize +
schemars::JsonSchema`. The generated JSON Schema is sent with the
request and the response is validated with `jsonschema` *before*
deserialising — a malformed model response surfaces as an
`AiError`, never a partial `T`.
## Endpoint safety
Every `Config::base_url` passes `validate_base_url`: non-HTTPS
schemes, URLs carrying userinfo (`user:pass@host`), and placeholder
hosts (`example.com` and subdomains) are rejected. Tests against a
`wiremock`/`httpmock` server set `Config::allow_insecure_base_url`,
which is `#[serde(skip)]` so config files can't downgrade HTTPS
enforcement. Each successful `AiClient::new` logs the endpoint
hostname at INFO — never the path or query.
## Model defaults
New AI code defaults to **Claude 4.7** models with prompt caching at
every stable point (system prompt, tools, static context). Migration
mapping: Opus 4.6 → Opus 4.7, Sonnet 4.5 → Sonnet 4.6, Haiku 4.5
(current).
## Secrets
API keys cross the boundary as `secrecy::SecretString` and are
resolved through the precedence chain documented in
[configure credentials](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/how-to/configure-credentials.md).
Never log the exposed form.
## Design record
The authoritative contract is the crate's v0.1 spec (written under the
former name), retained in the rust-tool-base
[spec series](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-05-01-rtb-ai-v0.1.md).