Skip to content

rtb-chat

A single chat client across providers. genai covers the mainstream (OpenAI, Gemini, Ollama, OpenAI-compatible); the Anthropic 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. Only the crate identity changed; the API is unchanged.

Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.

Start here

If you want to… Go to
Get something working from scratch Your first chat call
Solve one task — streaming, structured output, caching, testing How-to guides
Look up a field, a default or an error Reference
Understand why it is built this way Explanation
Know what it will not do What rtb-chat does not do

Public API

use rtb_chat::{AiClient, ChatRequest, Config, Message, Provider};
use secrecy::SecretString;

let client = AiClient::new(Config {
    provider: Provider::Anthropic,
    model: "claude-opus-4-7".into(),
    api_key: SecretString::from(std::env::var("ANTHROPIC_API_KEY")?),
    ..Config::default()
})?;

let reply = client.chat(ChatRequest {
    messages: vec![Message::user("Summarise this changelog in one line.")],
    max_tokens: Some(512),
    cache_control: true,
    ..ChatRequest::default()
}).await?;

Config and ChatRequest are plain structs with public fields and a Default impl — set what you need, spread the rest. Provider is #[non_exhaustive]. api_key has no usable default: an empty key is rejected by AiClient::new.

Item Purpose
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 Anthropic backend.
AiError thiserror + miette::Diagnostic error enum.
validate_base_url Endpoint shape check — scheme, userinfo, placeholder hosts (see below).

Generated signatures: docs.rs/rtb-chat. Behaviour, defaults and failure modes: Reference.

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.

Recipe and failure modes: Get structured output.

Endpoint safety

Every Config::base_url passes validate_base_url: non-HTTPS schemes, URLs carrying userinfo (user:pass@host), and placeholder hosts (example.com, example.org and their subdomains) are rejected. Tests against a mock server set Config::allow_insecure_base_url to accept http://. Config implements neither Serialize nor Deserialize, so nothing outside your own Rust code — no config file, no environment variable — can turn that enforcement off.

Each successful AiClient::new logs the endpoint hostname at INFO — never the path or query. The reasoning is in Why the endpoint is validated.

Model defaults

Config::default() selects Provider::Anthropic with the model claude-opus-4-7 and a 60-second timeout. New AI code across the estate 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. Never log the exposed form.

On the genai-backed providers the key is written into the process environment so genai can find it. That has consequences worth reading before you build a second client — see The genai backend puts your key in the process environment.

Further reading

The blog carries a curated route through this subject: Rust, and what survived the port collects everything written about it, ordered so you can start at the beginning rather than newest-first.

Ask phpbotscout

phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.

Join the Discord