Write Rust code against the `misanthropic` crate's non-streaming (`Client::message`) path — single messages, multi-turn chats, system prompts, structured output (`structured_output`, `add_examples`), and tool use (the `#[tool]` macro and manual `CustomMethodDef`s). Use when writing or editing Rust that calls the Anthropic Messages API through the `misanthropic` crate, or when the user mentions `misanthropic`, `Client::message`, or `Prompt`. For token-by-token streaming, see the misan-streamin...
Installs into .claude/skills of the current project.
Are you the author of Misan Messages Api?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mdegans-misan-messages-api)
---
name: misan-messages-api
description: >-
Write Rust code against the `misanthropic` crate's non-streaming
(`Client::message`) path — single messages, multi-turn chats, system
prompts, structured output (`structured_output`, `add_examples`), and
tool use (the `#[tool]` macro and manual `CustomMethodDef`s). Use
when writing or editing Rust that calls the Anthropic Messages API through
the `misanthropic` crate, or when the user mentions `misanthropic`,
`Client::message`, or `Prompt`. For token-by-token streaming, see the
misan-streaming-api skill instead.
---
# `misanthropic` — Non-Streaming (Message) API
`misanthropic` is an unofficial, ergonomic, async Rust client for the
Anthropic Messages API. This skill covers the non-streaming `Client::message`
path. For streaming, see the **misan-streaming-api** skill.
- Crate: [`misanthropic`](https://crates.io/crates/misanthropic)
- Repository: <https://github.com/mdegans/misanthropic>
- Docs: <https://docs.rs/misanthropic>
- License: MIT
## Cargo.toml
```toml
[dependencies]
misanthropic = "1.0.0-alpha.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
# For the `#[tool]` macro (below). Already pulled in by the default `derive`
# feature, but tool *argument* structs need these directly:
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
```
### Feature flags (selected)
Default features: `rustls-tls`, `langsan`, `client`, `batch`, `derive`,
`schema-order`, `schema-inline`, `schema-order-check`.
| Flag | Default | Purpose |
|------|---------|---------|
| `client` | yes | Enables `Client` (HTTP). Disable for wasm data-only. |
| `rustls-tls` | yes | Use rustls instead of system OpenSSL. |
| `langsan` | yes | Output sanitization (allow-list of benign Unicode). |
| `derive` | yes | The `#[tool]` / `#[derive(ToolArgs)]` macros. |
| `batch` | yes | Message Batches API. Does not build on wasm32. |
| `schema-order-check` | yes | Reject tool schemas with a required property after an optional one. |
| `prompt-caching` | no | Anthropic prompt-caching beta headers. |
| `markdown` | no | `ToMarkdown` trait, markdown rendering. |
| `image` / `png` / `jpeg` / `gif` / `webp` | no | Image support via the `image` crate. |
| `memsecurity` | no | Encrypt the API key in memory. |
## Quick start — single message
```no_run
use misanthropic::{Client, Prompt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// The key String is consumed, zeroized, and stored encrypted (with
// `memsecurity`) or zeroed on drop. `Client::new` takes anything that is
// `TryInto<Key>` (e.g. `String`). The `x-api-key` header is marked
// sensitive on requests.
let key = std::env::var("ANTHROPIC_API_KEY")?;
let client = Client::new(key)?;
// Build a Prompt (the request type) and send it. `Client::message`
// forces `stream = false` and returns a `response::Message` directly.
// `Prompt::user` (or `Prompt::from("…")`) is infallible — a lone turn
// of text, image or document content is always legal. The appending
// builders (`messages`, `add_message`, …) validate turn order and
// return a `Result`, so they need a `?` — and not just for correctness:
// an un-unwrapped `Result` would itself satisfy `impl Serialize` and
// reach the wire as `{"Ok": {…}}` if the error type were serializable.
// It deliberately isn't.
let message = client.message(Prompt::user("What is 2+2?")).await?;
// `response::Message` implements `Display` (prints content).
println!("{message}");
// Access fields:
// message.inner — prompt::AssistantMessage (derefs to Content)
// message.inner.content — Content (Display, iterable over Blocks)
// message.model — model::Model
// message.stop_reason — Option<StopReason>
// message.usage — Usage; counters on usage.counts (TokenCounts),
// read through via Deref (usage.input_tokens)
Ok(())
}
```
## The `Prompt` builder
`Prompt` is the request type. It defaults to model **`Haiku45`** (Claude
Haiku 4.5) with `max_tokens = 4096`. All fields are public; the builder
methods are convenience helpers that return `Self`.
```rust
use std::num::NonZeroU32;
use misanthropic::{Id, Prompt, prompt::message::Role};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let prompt = Prompt::default()
// Set model — accepts Id, model::Model, or any string.
.model(Id::Sonnet46)
// System prompt — accepts &str, String, or Content.
.system("You are a helpful assistant.")
// Append to the system prompt.
.add_system("Respond concisely.")
// Set max tokens.
.max_tokens(NonZeroU32::new(1024).unwrap())
// Set temperature (0.0–1.0).
.temperature(0.7)
// Add a user message — returns Result because turn order is validated
// (must alternate User/Assistant; first message must be User).
.add_message((Role::User, "Hello!"))?;
# let _ = prompt;
# Ok(())
# }
```
### Available models (`Id` enum)
`.model(...)` takes an `Id`, a `model::Model`, or any string. The
`Id` enum tracks current and historical models (each variant
serializes to its wire ID, e.g. `Sonnet46` → `claude-sonnet-4-6`), with both
"latest"-style and pinned/dated variants. The **default** is `Haiku45`
(`claude-haiku-4-5`).
Rather than reproduce the list here (it drifts as models ship), see the
[`Id` docs](https://docs.rs/misanthropic/latest/misanthropic/model/enum.Id.html)
for the authoritative set — the variant↔wire-ID mapping is verified against the
live `/v1/models` endpoint by `test_client_models`. For a model not in the enum
yet, pass `model::Model::Custom("your-model-id".into())` or just the string.
### Messages — generic conversions
The crate leans **heavily on generics and `From`/`Into`** so call sites stay
clean. Every message method (`add_message`, `push_message`, `messages`,
`add_messages`, …) is bounded on `M: Into<Message>`, and the tuple
conversion is `(Role, T) where T: Into<Content>` — not just `&str`. A
`Message`/`Content`/`Block` can be built from a `&str`, a `String`, a
`tool::Use`, a `tool::Result`, an `Image`, a `DocumentSource`, a slice of
`&str`, a `response::Message`, and more. So a tuple's second element is
whatever converts into `Content` — a string is just the common case.
```rust
use misanthropic::{Prompt, prompt::message::Role};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
// messages replaces all messages; add_message appends one.
let prompt = Prompt::default().messages([
(Role::User, "What is Rust?"),
(Role::Assistant, "Rust is a systems programming language."),
(Role::User, "What are its key features?"),
])?;
# let _ = prompt;
# Ok(())
# }
```
> **The crate is meant to be extended.** If something *logically* should
> convert and doesn't — e.g. `prompt.push_message(some_pushable_thing)` won't
> compile — that's usually a missing `From`/`Into` impl, not a design wall.
> Adding the conversion in the crate (it's a small, local change) is the
> idiomatic fix and a stated goal of the project; reach for that before
> contorting the call site.
### Role-pinned messages — `RoleMessage<R>`
All message types are one generic struct, `RoleMessage<R> { role, content }`:
- `Message` = `RoleMessage<Role>` — role known at runtime.
- `UserMessage` / `AssistantMessage` / `SystemMessage` =
`RoleMessage<markers::{User, Assistant, System}>` — role pinned at compile
time by a zero-sized marker that serializes as the role string, so the
pin is **wire-invisible**.
Every `RoleMessage` derefs (and `DerefMut`s) to its `Content` — and `Content`
derefs to `Vec<Block>` — so content accessors and mutators (`len`, `iter`,
`push`, `cache`, …) apply directly. Mutation through the deref can never
change the role; that invariant lives in the type parameter.
```rust
use misanthropic::prompt::message::{
AssistantMessage, Message, Role, SystemMessage, UserMessage, WrongRole,
};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
// Role-pinned construction: From<&str>/String/Content, FromIterator<Block>.
let user = UserMessage::from("Hello!");
let reply = AssistantMessage::text("Hi — how can I help?");
let system: SystemMessage = "Prefer SI units.".into();
// Deref to Content (and through it to Vec<Block>) — no `.content()` dance.
assert_eq!(user.len(), 1);
let mut cached = reply.clone();
cached.cache(); // Content::cache via DerefMut — role untouchable.
// The marker is wire-invisible: pinned and erased serialize identically.
let erased = Message::from(user.clone());
assert_eq!(serde_json::to_string(&user)?, serde_json::to_string(&erased)?);
// Downcasting checks the role; `WrongRole` reports both sides.
let err: WrongRole =
UserMessage::try_from(Message::from(reply)).unwrap_err();
assert_eq!(err.expected, Role::User);
assert_eq!(err.actual, Role::Assistant);
# let _ = (cached, system);
# Ok(())
# }
```
`Response::{message, into_message, unwrap_message}` return an
`AssistantMessage` (a response is an assistant turn by construction);
`prompt.push_message(reply)` accepts it directly. Deserializing a pinned type
rejects mismatched roles, so `serde_json::from_str::<UserMessage>(…)` doubles
as a role check. `SystemMessage` is the typed form of the mid-conversation
`Role::System` turn (Opus 4.8+; see turn-order notes below).
### Multi-turn conversation
```no_run
use misanthropic::{Id, Client, Prompt, prompt::message::Role};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("ANTHROPIC_API_KEY")?)?;
let mut chat = Prompt::default()
.model(Id::Sonnet46)
.system("You are a helpful assistant.")
.add_message((Role::User, "What is Rust?"))?;
let reply = client.message(&chat).await?;
println!("Assistant: {reply}");
// Append the assistant reply, then a user follow-up. `push_message` is the
// in-place version of `add_message` (both validate turn order).
chat.push_message(reply)?;
chat.push_message((Role::User, "What about memory safety?"))?;
let reply = client.message(&chat).await?;
println!("Assistant: {reply}");
# Ok(())
# }
```
### Prompt caching — `cache()` every turn
`cache()` marks the end of the prompt (5-minute TTL). Call it every turn: it
keeps Anthropic's 4-marker limit (the automatic slot counts) by sliding a
window — the new tail marker always stays, the oldest 5-minute message
markers go first, a 1-hour anchor only when no other 5-minute one is left,
and an evicted entry stays reachable while a kept marker is within ~20
blocks after it. `auto_cache()` lets the API place
the marker instead. Anthropic 400s a 1-hour marker after a 5-minute one
(`tools` → `system` → `messages`, automatic slot last), so the 1-hour and
automatic placements return a `CacheError` instead of building one;
`check_cache()` checks hand-placed markers the same way (the `Chat` driver
runs it before every request).
```rust
use misanthropic::{Prompt, prompt::{CacheError, message::Role}};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1-hour markers first: a long-lived prefix, then a 5-minute conversation.
let mut chat = Prompt::default()
.system("<a long manual>")
.cache_1h()?
.add_message((Role::User, "Where do I start?"))?
.cache();
assert!(chat.check_cache().is_ok());
// Each turn: push the reply and the next line, re-mark the end.
chat.push_message((Role::Assistant, "Chapter one."))?;
chat.push_message((Role::User, "And then?"))?;
chat = chat.cache();
// A 1-hour marker after those 5-minute ones would be a 400: refused.
let refused = chat.cache_1h();
assert!(matches!(refused, Err(CacheError::TtlOrder { .. })));
# Ok(())
# }
```
### Seating turns — `Prompt::seat` (drivers)
`add_message` / `push_message` are the everyday appends. `Prompt::seat` is the
wire-legality *kernel* an agent loop wants: same-role tails **concatenate**
(portable to strict-alternation backends, and what Anthropic does server-side),
and mid-conversation `Role::System` notes seat the instant the tail permits —
otherwise **buffering** (never re-attributed to the user channel) until a later
seat opens a slot. The pending-system buffer is caller-owned, so it survives
across rounds; the crate owns the legality. `seat` reports what it did as
[`Seated`].
```rust
use misanthropic::{
Prompt,
prompt::{Seated, message::{Role, SystemMessage}},
};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut prompt = Prompt::default();
// The caller owns the buffer; code that never seats a system note passes
// `&mut None` and just gets same-role merging.
let mut pending: Option<SystemMessage> = None;
// Same-role tails concatenate rather than pushing an (illegal-on-some-backends)
// second turn.
prompt.seat((Role::User, "hi"), &mut pending)?;
prompt.seat((Role::Assistant, "one."), &mut pending)?;
assert_eq!(prompt.seat((Role::Assistant, "two."), &mut pending)?, Seated::Merged);
// An operator note can't follow this assistant tail, so it buffers...
assert_eq!(
prompt.seat((Role::System, "Prefer minimal diffs."), &mut pending)?,
Seated::Buffered,
);
assert!(pending.is_some());
// ...and rides the next user turn automatically. (After a system turn, the
// caller must pass the turn to the assistant next — system→user is illegal.)
prompt.seat((Role::User, "go on"), &mut pending)?;
assert!(pending.is_none());
assert_eq!(prompt.messages.last().unwrap().role, Role::System);
# Ok(())
# }
```
[`Seated`]: https://docs.rs/misanthropic/latest/misanthropic/prompt/enum.Seated.html
### Agent loops — `response.disposition()`
`response.disposition()` classifies a turn by what the loop must do next — the
pause / clip / dispatch lore in one exhaustive `match` (a new `Disposition`
breaks the build here, not silently in a driver). `response.tool_uses()`
iterates **every** client `tool::Use` in the turn (unlike `tool_use()`, which
returns only a trailing one) — but, like `tool_use()`, only on a `tool_use`
stop; the content's own `tool_uses()` (`response.inner.content`) is the raw
view, which a paused turn's calls and a turn inferred without a stop reason
need. Client calls run **only** from a `ToolUse` (or `Paused`) turn — a
`refusal` can cut a `tool_use` (or a server tool) short. The `chat`-feature
`Chat` driver runs this same match; see *The `Chat` driver* below for how it
hands a turn it can't use back.
```no_run
use misanthropic::{
Client, Prompt,
prompt::message::{Block, Message, Role, SystemMessage},
response::Disposition,
tool::{Tool, ToolBox},
};
# type Error = Box<dyn std::error::Error + Send + Sync>;
# async fn run(client: Client, mut toolbox: ToolBox) -> Result<(), Error> {
let mut prompt = Prompt::user("Run the tests and summarize the failures.");
toolbox.prepare(&mut prompt).await?;
let mut pending: Option<SystemMessage> = None;
for _ in 0..8 { // round budget: a model that calls tools forever still stops
let response = client.message(&prompt).await?;
match response.disposition() {
// max_tokens: tool calls may be missing arguments. NEVER seat or
// dispatch; hand back — raise max_tokens (or ask for brevity), resend.
Disposition::Clipped => break,
// tool_use, or pause_turn (resending resumes the server tool): seat
// it, answer any client calls in one tool_result-led user turn. Raw
// calls: the gated `response.tool_uses()` misses a paused turn's.
Disposition::Paused | Disposition::ToolUse => {
let content = &response.inner.content;
let calls: Vec<_> = content.tool_uses().cloned().collect();
prompt.seat(response, &mut pending)?;
if calls.is_empty() {
continue;
}
let mut results = Vec::new();
for call in calls {
results.push(Block::from(toolbox.call(call).await));
}
prompt.seat((Role::User, results), &mut pending)?;
}
// end_turn / stop_sequence / refusal: seat and hand back — unless
// it's empty (a 400) or cuts a call short (a refusal or a stop
// sequence can leave a tool_use half-made, a refusal a server tool
// too); drop such a turn whole (stripping could strand a server tool).
Disposition::Done => {
let turn = &response.inner;
let usable = !turn.content.is_empty()
&& turn.content.tool_uses().next().is_none()
&& turn.unfinished_server_tool_uses().next().is_none();
if usable {
prompt.seat(response, &mut pending)?;
}
break;
}
}
}
// A paused turn left in flight admits only its continuation: pop it (one
// message — continuations merge) before a user turn follows.
let in_flight =
|turn: &Message| turn.unfinished_server_tool_uses().next().is_some();
if prompt.messages.last().is_some_and(in_flight) {
prompt.messages.pop();
}
# Ok(())
# }
```
Clip handling is policy: hand back (above), then raise `max_tokens` and
resend, or nudge the model to be briefer. Continuing a partial assistant turn
(prefill) is backend-dependent — Anthropic rejects it with thinking enabled.
### The `Chat` driver — hand-backs and resume
`Chat` (feature `chat`) runs the loop above over any `Transport`. A turn it
can't use is never seated and its calls never run: `run` returns a
`chat::Error { kind, prompt, pending, state, .. }` — `Stop::Clipped`
(`max_tokens`) or `Stop::Unusable` (a finished turn, e.g. a refusal, that
still calls tools or cuts a server tool short; dropped whole) — alongside
transport, beat, tool and turn-order failures. It hands back everything a
resume needs: the prompt (legal to resend), buffered system notes, the state,
and the torn-down toolbox and configuration (hook, budget, caching, usage
sink). `error.resume(transport)` rebuilds the same `Chat` (re-preparing the
tools), which answers the prompt before asking for a beat — so resuming is a
loop. A finished run hands back the same `chat::Parts`; `Chat::from_parts`
carries on from them. Pass the beat closure as `&mut` to keep it across
resumes. `chat::Error` converts into a `BoxError` with `?`.
```no_run
use std::num::NonZeroU32;
use misanthropic::{
Prompt, Transport,
chat::{BoxError, Chat, Stop},
prompt::message::{Message, Role},
tool::ToolBox,
};
# async fn converse<T: Transport + Clone>(
# transport: T,
# lines: Vec<&str>,
# ) -> Result<Prompt, BoxError> {
let mut lines = lines.into_iter();
let mut next_beat = async |_: &mut ()| {
let line = lines.next();
Ok::<_, BoxError>(line.map(|l| vec![Message::from((Role::User, l))]))
};
let mut chat = Chat::new(transport.clone(), Prompt::default(), ToolBox::new());
loop {
let mut error = match chat.run((), &mut next_beat).await {
Ok((parts, ())) => return Ok(parts.prompt),
Err(error) => error,
};
match &error.kind {
// Nothing seated or run: raise the limit, resume.
Stop::Clipped(_) => {
let two = NonZeroU32::new(2).unwrap();
error.prompt.max_tokens =
error.prompt.max_tokens.saturating_mul(two);
}
// A refusal (or other finished turn) that called tools: nothing ran.
Stop::Unusable(response) => {
return Err(format!("unusable: {:?}", response.stop_reason).into());
}
_ => return Err(error.into()),
}
(chat, _) = error.resume(transport.clone());
}
# }
```
## Tool use — the `#[tool]` macro (preferred)
The `#[tool]` macro (default `derive` feature) turns an `impl` block into a
typed tool: it generates the wire definitions from your argument struct's
`JsonSchema`, and gives the type a concrete `impl Tool` so dispatch is typed
and validated — no hand-parsing of `serde_json::Value`.
```no_run
use misanthropic::{
Client, Prompt,
prompt::message::{Content, Role, UserMessage},
tool::{Tool, tool},
};
use schemars::JsonSchema;
use serde::Deserialize;
// Argument struct. Field docs become the schema property descriptions the
// model sees (via `schemars`). Must be `Deserialize + JsonSchema`.
#[derive(Debug, Deserialize, JsonSchema)]
struct GetWeather {
/// City name.
city: String,
}
// A (possibly stateful) tool. Methods tagged `#[method]` must be
// `async fn(&mut self, args: ArgsTy) -> Result<Content, Content>`.
// `Ok` is the tool result; `Err` is a model-facing error.
struct Weather;
#[tool]
impl Weather {
/// Get the weather for a city.
#[method]
async fn get_weather(
&mut self,
args: GetWeather,
) -> Result<Content, Content> {
let report = format!("Sunny, 22C in {}", args.city); // your logic
Ok(report.into())
}
}
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("ANTHROPIC_API_KEY")?)?;
let mut weather = Weather;
let mut chat = Prompt::default()
.model(misanthropic::Id::Sonnet46)
.system("Use tools when appropriate.")
.add_message((Role::User, "What's the weather in Paris?"))?;
// Register the generated definition(s). Methods are namespaced by the tool
// type, e.g. `Weather__get_weather` (the `__` separator). `tools` /
// `add_tools` take anything `Into<MethodDef>`, so a tool's `definitions()`
// drops straight in (`register_tool(&weather)` is the same one-liner).
chat = chat.add_tools(weather.definitions());
let message = client.message(&chat).await?;
// `tool_uses()` yields every call in the turn (parallel calls too) and is
// empty unless stop_reason is ToolUse — never run calls from a Refusal /
// MaxTokens turn (they can be cut short).
let calls: Vec<_> = message.tool_uses().cloned().collect();
if !calls.is_empty() {
chat.push_message(message)?;
// Typed dispatch: `call.input` is deserialized into `GetWeather` and
// validated. Bad arguments become a helpful, model-facing error
// automatically. Returns a `tool::Result`.
let mut results = Vec::new();
for call in calls {
results.push(weather.call(call).await);
}
// Every result goes back in ONE user turn.
chat.push_message(results.into_iter().collect::<UserMessage>())?;
let final_reply = client.message(&chat).await?;
println!("{final_reply}");
}
# Ok(())
# }
```
Notes on the macro:
- Each `#[method]` becomes a real inherent method you can still call directly.
- Several calls in one turn? `message.tool_uses()` yields them all (empty
unless `stop_reason` is `ToolUse`); answer them in **one** user turn, as
above. `tool_use()` returns only the *last* call — complete only when
parallel tool use is disabled.
- One `#[tool]` block can hold several `#[method]`s; each is namespaced
`TypeName__method_name` (and a `ToolBox` adds its own segment:
`toolbox__TypeName__method_name`).
- `#[tool(flat)]` puts method names on the wire **bare** — no `TypeName__`
segment. Pair with `ToolBox::flat()` (no `toolbox__` segment either) for
fully bare names, e.g. matching an MCP server's `create_post`. Collisions
between two tools' bare names are debug-asserted at add time; note that
un-flattening later renames the methods (prompt-cache / transcript churn).
- `#[method(defer_loading)]` marks a method's schema as deferrable for use
with the tool-search server tool (large tool sets).
- **Declare required fields before optional ones** (`Option<…>` or
`#[serde(default)]`). It's the one layout every engine generates in the
same order (Anthropic keeps optionals in place; engines following the
structured-outputs docs hoist required first), and field order changes what
the model generates — put reasoning before answers. The default-on
`schema-order-check` feature rejects interleaving in what you author: a
compile error under `#[derive(ToolArgs)]`; under `#[tool]`, which can't
see its args' fields, a panic from `ToolArgs::definition` (so `add_tool`)
at runtime, plus a generated `#[cfg(test)]` test per method that fails
your `cargo test` first; and an `Err` from `MethodBuilder::build` (escape
hatch: `build_unchecked`). Received
schemas (a deserialized `Prompt`) are only checked structurally.
- The `Tool` trait also has `definitions()`, `call()`, plus optional
`on_init` / `on_turn` lifecycle hooks and `save_json` / `load_json` for
state persistence.
See the runnable example: `misanthropic/examples/strawberry.rs`.
## Tool use — manual `CustomMethodDef` (no macro)
When you want a hand-written schema, build a `tool::CustomMethodDef` with
`CustomMethodDef::builder(...)` and hand it to `add_tool`. (This struct was
previously named `MethodDef`, and before that `Method`.)
> Don't reach for a `CustomMethodDef { .. }` struct literal: it's
> `#[non_exhaustive]`, so the API can grow fields (it already gained `strict`,
> `defer_loading`, `allowed_callers`) without breaking you — but only if you go
> through a constructor. Use `builder()` (fluent + validated),
> `CustomMethodDef::simple(name, desc)` for a trivial empty-schema tool, or
> the `#[tool]` macro above for new code. `try_add_tool` accepts a
> `MethodBuilder` directly, validating on add.
```no_run
use misanthropic::{
Client, Prompt, json,
prompt::{UserMessage, message::Role},
tool::CustomMethodDef,
};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("ANTHROPIC_API_KEY")?)?;
let mut chat = Prompt::default()
.model(misanthropic::Id::Sonnet46)
.add_tool(
CustomMethodDef::builder("get_weather")
.description("Get the weather for a city.")
.schema(json!({
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" }
},
"required": ["city"],
}))
// Optional, forward-compatible setters — call only what you need:
// .strict(true) grammar-constrained decoding
// .defer_loading(true) defer schema (tool-search)
// .programmatic() allow code-execution callers
// .cache() prompt-caching breakpoint
.build()?,
)
.system("Use tools when appropriate.")
.add_message((Role::User, "What's the weather in Paris?"))?;
let message = client.message(&chat).await?;
// One result per call, all in ONE user turn (empty unless ToolUse).
let results: UserMessage = message
.tool_uses()
.map(|call| {
// call.name — tool name ("get_weather")
// call.id — unique ID for this call
// call.input — serde_json::Value with arguments
let city = call.input["city"].as_str().unwrap_or("?");
let weather = format!("Sunny, 22C in {city}"); // your logic
misanthropic::tool::Result::new(call.id.clone(), weather)
})
.collect();
if !results.is_empty() {
chat.push_message(message)?;
chat.push_message(results)?;
let final_reply = client.message(&chat).await?;
println!("{final_reply}");
}
# Ok(())
# }
```
Tool JSON someone else wrote (an MCP server, a saved `Prompt`) is *received*:
`CustomMethodDef::try_from(value)`, `from_serializable`, and deserializing a
`Prompt` check it structurally only, so it parses as written.
`CustomMethodDef::try_from_checked` holds it to the authoring bar instead —
with `schema-order-check`, required properties first — and returns a typed
`ToolBuildError` (`InvalidInputSchema` for a misorder, `Json` for a value
that isn't a tool definition):
```
use misanthropic::{
json,
tool::{CustomMethodDef, ToolBuildError},
};
let search = json!({
"name": "search",
"description": "Search the docs.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer" },
"lang": { "type": "string" }
},
"required": ["query", "lang"]
}
});
let received = CustomMethodDef::try_from(search.clone()).unwrap();
assert_eq!(received.name, "search");
#[cfg(feature = "schema-order-check")]
assert!(matches!(
CustomMethodDef::try_from_checked(search),
Err(ToolBuildError::InvalidInputSchema { .. })
));
```
`add_tool` accepts anything `Into<MethodDef>` — a `CustomMethodDef`, a
`ServerMethodDef` (Anthropic-executed, e.g. `web_search`), or a `Memory` (the
client-executed memory tool). `try_add_tool` accepts `TryInto<CustomMethodDef>`
(e.g. a `MethodBuilder` with validation).
## Predefined tools — server tools & client-executed tools
Predefined tools are added by versioned name with no schema of your own. Most
are **server-executed**: Anthropic runs them and the result blocks arrive in
the response (you never call anything) — e.g. `web_search` (see
`examples/web_search.rs`), `web_fetch`, `code_execution`, the tool-search
tools.
`code_execution` is the richest of these: enabling it gives the model a
sandboxed container with two sub-tools, whose outcomes come back as
`BashCodeExecutionToolResult` and `TextEditorCodeExecutionToolResult` blocks
(a *failed* bash command is still a `Result` with a non-zero `return_code`; the
`Error` variants are for the sub-tool itself failing):
```no_run
use misanthropic::{Prompt, prompt::message::{
Block, BashCodeExecutionResultContent as Bash,
TextEditorCodeExecutionResultContent as Edit, Role,
}, tool::ServerMethodDef};
# fn f(response: misanthropic::response::Message) -> Result<(), Box<dyn std::error::Error>> {
let _prompt = Prompt::default()
.add_message((Role::User, "Sum 1..5 in a file with bash."))?
.add_tool(ServerMethodDef::code_execution());
// Read the result blocks the container produced (in a response).
for block in response.inner.content.iter() {
match block {
Block::BashCodeExecutionToolResult { content, .. } => match content {
Bash::Result { stdout, return_code, .. } => {
println!("exit {return_code}: {stdout}");
}
Bash::Error { error_code, .. } => eprintln!("bash: {error_code}"),
},
Block::TextEditorCodeExecutionToolResult { content, .. } => match content {
Edit::Create { is_file_update } => println!("created (update={is_file_update})"),
Edit::View { content, .. } => println!("viewed: {content}"),
Edit::StrReplace { lines, .. } => println!("edited: {} diff lines", lines.len()),
Edit::Error { error_code, .. } => eprintln!("editor: {error_code}"),
},
_ => {}
}
}
# Ok(())
# }
```
A few are **client-executed**: Anthropic defines the schema, but the model
emits an ordinary `tool_use` that *you* run against storage you control, just
like a custom tool — so they *define* like a server tool and *execute* like a
custom one. Each has a focused guide; load the one you need:
- **memory** — persistent notes across sessions, filesystem-backed
(`FsMemoryBackend`). See [MEMORY.md](MEMORY.md).
- **text editor** (`str_replace_based_edit_tool`) — view/edit files in a
working tree (`FsEditorBackend`). See [TEXT_EDITOR.md](TEXT_EDITOR.md).
- **bash** — run shell commands in a torn-down-after sandbox, not a filesystem
jail (`BashTool` over a `DockerSandbox`). See [BASH.md](BASH.md).
All three share the same shape: `add_tool(Memory::latest())` /
`add_tool(TextEditor::latest())` (or wrap a sandbox in `BashTool`), then drive a
tool loop that executes each `tool_use` locally and feeds the `tool::Result`
back. Their backends drop into a `ToolBox` and route by their fixed bare name
with no special-casing.
## Structured output
`Prompt::structured_output::<T>()` constrains the response (grammar-based
decoding, not prompting) to a single text block of JSON matching `T`'s
schema, generated via `schemars` and sanitized to Anthropic's supported
subset (`additionalProperties: false` added, `oneOf` → `anyOf`, numeric/
string range keywords stripped — see `prompt::output::sanitize_for_anthropic`).
Parse with `response.json::<T>()`:
```no_run
use misanthropic::{Client, Id, Prompt, prompt::message::Role};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct Triage {
/// Doc comments become schema descriptions — the model reads them.
summary: String,
severity: String,
is_regression: bool,
}
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("ANTHROPIC_API_KEY")?)?;
let prompt = Prompt::default()
.model(Id::Haiku45)
.structured_output::<Triage>()
.add_message((Role::User, "Checkout shows $0.00 on mobile Safari."))?;
let triage: Triage = client.message(&prompt).await?.json()?;
# let _ = triage;
# Ok(())
# }
```
Notes:
- **Field order is generation order** — each field is context for the next,
so put anchoring fields (a summary, say) first. The constrained decoder
emits fields in schema `properties` order, and the default-on
`schema-order` feature makes that your struct's declaration order.
Measured, not assumed: the `live_generation_order` probe in
`src/prompt/output.rs` reports it per model. Two caveats — a *tool* call
without `strict` isn't grammar-constrained, so smaller models sometimes
wander; and `#[serde(flatten)]`ed fields carry no ordering guarantee.
- **Few-shot**: `Prompt::add_examples([(input, output), …])` turns each
`(impl Into<UserMessage>, T)` pair into a user/assistant exchange *and*
seeds the schema from `T` — the constraint can't drift from the exemplars.
Runnable example: `misanthropic/examples/few_shot_triage.rs`.
- **Lists**: the API requires a top-level *object* schema, so a list rides
in `prompt::Items<T>` (`{"items": [T, …]}`). When streaming, pair it with
`FilterExt::json_items::<T>()` to consume each element as it arrives —
see the misan-streaming-api skill and
`misanthropic/examples/triage_stream.rs`.
- The model can decline with `StopReason::Refusal`; changing the schema
invalidates the prompt cache for the thread, so keep it stable when
caching matters. `output_config` also carries `Effort` (token-spend
control) — the granular builders (`structured_output`, `effort`) merge
per-field rather than overwrite.
## Using `json!` instead of `Prompt`
> **For quick experiments and tests only.** In production, prefer `Prompt`:
> you get turn-order validation, the typed builder, model/feature helpers, and
> compile-time checking that raw JSON skips. Reach for `json!` to reproduce a
> payload quickly or in a throwaway test, not in real code.
`Client::message` accepts anything `Serialize`. You can use raw JSON:
```no_run
use misanthropic::{Client, json, prompt::message::Role};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("ANTHROPIC_API_KEY")?)?;
let message = client
.message(json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"system": "You are a pirate.",
"messages": [{
"role": Role::User,
"content": "Ahoy!",
}],
}))
.await?;
println!("{message}");
# Ok(())
# }
```
## Error handling
`Client` methods return `client::Result<T>` which wraps `client::Error`:
```no_run
use misanthropic::client::{Error, AnthropicError};
# async fn run(client: &misanthropic::Client, prompt: &misanthropic::Prompt) {
match client.message(prompt).await {
Ok(msg) => println!("{msg}"),
Err(Error::Anthropic(AnthropicError::RateLimit { message, .. })) => {
eprintln!("Rate limited: {message}");
}
Err(Error::Anthropic(AnthropicError::Authentication { message })) => {
eprintln!("Auth error: {message}");
}
Err(e) => eprintln!("Error: {e}"),
}
# }
```
### Error variants
| Variant | Description |
|---------|-------------|
| `Error::HTTP` | Network / reqwest error |
| `Error::Parse` | JSON deserialization failed |
| `Error::Anthropic(AnthropicError::*)` | API error (see below) |
| `Error::UnexpectedResponse` | Wrong response type (should not happen) |
**`AnthropicError` variants:** `InvalidRequest` (400), `Authentication`
(401), `Permission` (403), `NotFound` (404), `RequestTooLarge` (413),
`RateLimit` (429), `API` (500), `Overloaded` (529), `Timeout`, `Billing`,
`Unknown`. Exhaustive `match` (doc-tested drift guard for the prose above):
```rust
use misanthropic::client::AnthropicError;
# #[allow(unused_variables)]
# fn document(err: AnthropicError) {
match err {
AnthropicError::InvalidRequest { .. } => {} // 400
AnthropicError::Authentication { .. } => {} // 401
AnthropicError::Permission { .. } => {} // 403
AnthropicError::NotFound { .. } => {} // 404
AnthropicError::RequestTooLarge { .. } => {} // 413
AnthropicError::RateLimit { .. } => {} // 429 (has retry_after)
AnthropicError::API { .. } => {} // 500
AnthropicError::Overloaded { .. } => {} // 529 (has retry_after)
AnthropicError::Timeout { .. } => {}
AnthropicError::Billing { .. } => {}
AnthropicError::Unknown { .. } => {}
}
# }
```
`RateLimit` and `Overloaded` carry a `retry_after: Option<u64>` field
populated from the `retry-after` response header. Prefer the
`AnthropicError::retry_after()` accessor, which returns it as an
`Option<std::time::Duration>`:
```rust
use misanthropic::client::{Error, AnthropicError};
# fn handle(e: &Error) {
if let Error::Anthropic(api_err) = e {
if let Some(wait) = api_err.retry_after() {
eprintln!("retry after {wait:?}");
}
}
# }
```
## Response structure
```text
response::Message
├── id: Cow<str> — unique message ID
├── inner: AssistantMessage — RoleMessage<markers::Assistant>, derefs to Content
│ ├── role: markers::Assistant
│ └── content: Content — Display, iterable over Blocks
├── model: model::Model
├── stop_reason: Option<StopReason>
│ └── EndTurn | MaxTokens | StopSequence | ToolUse | PauseTurn | Refusal
├── stop_sequence: Option<Cow<str>>
└── usage: Usage — NOT Copy (carries the strings below)
├── counts: TokenCounts — Copy; flattened on the wire, Deref'd
│ ├── input_tokens: u64 — from Usage (so usage.input_tokens reads through)
│ ├── output_tokens: u64
│ ├── cache_creation_input_tokens: Option<u64>
│ ├── cache_read_input_tokens: Option<u64>
│ ├── cache_creation: Option<CacheCreation>
│ ├── output_tokens_details: Option<OutputTokensDetails>
│ └── server_tool_use: Option<ServerToolUsage>
├── service_tier: Option<Cow<str>> — capacity tier that served the request
└── inference_geo: Option<Cow<str>> — region that served the request
```
The numeric counters live on `usage.counts` (a [`TokenCounts`](crate::response::TokenCounts)), not directly
on `Usage`. Reads still work via `Usage`'s `Deref`, so `usage.input_tokens`
compiles — but for **accumulation**, hold a `TokenCounts`: it's `Copy` and
`Usage` isn't (the strings above keep it from being `Copy`). Copy `.counts` off
each response and `+=` into a running total — no `Usage` clones:
```rust
use misanthropic::response::{Usage, TokenCounts};
# #[allow(unused_variables)]
# fn document(usage: &Usage) {
let via_deref = usage.input_tokens; // Deref<Target = TokenCounts>
let explicit = usage.counts.output_tokens; // the same counters, named
// A running total is a `TokenCounts` (Copy + AddAssign), not a `Usage`:
let mut total = TokenCounts::default();
total += usage.counts; // AddAssign<TokenCounts>
# }
```
`StopReason` in full, as an exhaustive `match` (doc-tested drift guard — a new
variant breaks the build, since the tree above is prose and can't be):
```rust
use misanthropic::response::StopReason;
# #[allow(unused_variables)]
# fn document(reason: StopReason) {
match reason {
StopReason::EndTurn => {} // natural stopping point
StopReason::MaxTokens => {} // hit max_tokens — never dispatch its calls
StopReason::StopSequence => {} // a stop sequence was generated
StopReason::ToolUse => {} // wants a tool call — see `tool_use()`
StopReason::PauseTurn => {} // server tool paused; resend to continue
StopReason::Refusal => {} // model declined (safety)
}
# }
```
## Examples
Runnable examples live in `misanthropic/examples/` (run with
`cargo run --example <name> --all-features`). Read the one whose shape matches
your task — they're the most current, compiler-checked usage.
| Example | Covers |
|---------|--------|
| `strawberry.rs` | **Typed tool use** via the `#[tool]` macro (`count_letters`) — the canonical tool example. |
| `python.rs` | Tool use where the assistant calls a `python` tool to compute an answer. |
| `few_shot_triage.rs` | **Few-shot prompting** + structured output — triage a free-text bug report into a structured form. |
| `structured_commit_classifier.rs` | Structured output — classify a unified diff into a commit message. |
| `vote_intent.rs` | Structured output — analyze a social-network post into a typed result. |
| `mid_conversation_system.rs` | Mid-conversation `Role::System` message (Opus 4.8+). |
| `interleaved_thinking.rs` | Adaptive extended thinking with interleaved thinking. |
| `tool_search.rs` | The tool-search server tool over a large, `defer_loading` tool set. |
| `web_search.rs` | The `web_search` server tool. |
| `web_fetch.rs` | The `web_fetch` server tool, paired with `web_search`. |
| `code_execution.rs` | The `code_execution` server tool — bash + file editing in a sandbox container. |
| `programmatic_tool_calling.rs` | `code_execution` calling a `.programmatic()` custom tool from inside the container (PTC). |
| `memory.rs` | The client-executed `memory` tool (`FsMemoryBackend`) — see [MEMORY.md](MEMORY.md). |
| `text_editor.rs` | The client-executed `text_editor` tool (`FsEditorBackend`) — see [TEXT_EDITOR.md](TEXT_EDITOR.md). |
| `bash.rs` | The client-executed `bash` tool (`BashTool` over a `DockerSandbox`) — see [BASH.md](BASH.md). |
| `neologism.rs` | `Client::message` with a custom system prompt. |
| `website_wizard.rs` | **Streaming** (`Client::stream`) — see the misan-streaming-api skill. |
## Key design notes
- **API keys** are zeroized on drop. With `memsecurity`, they are encrypted in
memory. The `x-api-key` header is marked sensitive.
- **No `unsafe` code** — the crate uses `#[forbid(unsafe_code)]`.
- **Rate limiting** is built in (default: 50 req/min, tier 1). Adjust with
`client.set_rate_limit(quota)`.
- **`Client` is cheap to clone** — it wraps `Arc`s internally.
- **Turn order is enforced (client-side)** — messages must alternate
User/Assistant and the first must be User. Methods return `TurnOrderError`
on violation. Two exceptions: Opus 4.8+ supports an authoritative
mid-conversation `Role::System` turn (must follow a user turn and either end
the array or precede an assistant turn); and **two adjacent assistant turns
are allowed when the first contains a `ServerToolUse` block** — the API
pauses a long-running server tool with `StopReason::PauseTurn`, and you
resume by appending the paused turn back. Note Anthropic itself **no longer
enforces** strict alternation server-side, but many Anthropic-compatible
backends still do, so the crate keeps the check (in `prompt.rs`,
`check_turn_order` / `Message::may_precede`). Tool blocks are checked too:
`tool_result`s must **lead** their user turn, and every client `tool_use`
must be answered by a matching `tool_result` in the next turn — otherwise
`TurnOrderError::ToolResultNotLeading` / `UnansweredToolUse`.
- **Seating kernel for drivers** — `Prompt::seat(msg, &mut Option<SystemMessage>)`
is the shared append used by agent loops: same-role tails merge, and
`Role::System` content seats when the tail permits or buffers (never on the
user channel) until it does, concatenating onto a system tail if it lands on
one. Returns `Seated::{Appended, Merged, Buffered}`. See the seating example
above. Pair it with `response.disposition()` (`Paused` / `Clipped` /
`ToolUse` / `Done`) to decide what to seat — never a `Clipped` turn, and
never run the calls of anything but a `ToolUse` / `Paused` one.
- **Owned data, no lifetimes** — public types own their string data
(`Cow<'static, str>` under the hood, sanitized when `langsan` is on) and
carry **no lifetime parameter**. You can freely store a `Use`/`Message`/etc.
with no `.into_static()` dance — they are already `'static`. (The crate used
to thread a pervasive `'a`; it was removed because it bought no real
zero-copy — without `#[serde(borrow)]` every deserialized string allocates
anyway — at a large ergonomic cost.)