crimson-crab v0.2.1 MIT OR Apache-2.0 MSRV 1.75
The Rust SDK for Claude that can't panic on you.
Built on one hard guarantee: unwrap, expect, panic! and todo! are denied at compile time across the whole library — so a malformed or surprising API response is always an Error you handle, never a panic in your service. And it's the complete Claude surface — streaming, tool use, thinking, prompt caching, batches — with wire-faithful, forward-compatible types that keep working the day Anthropic ships a new model.
cargo add crimson-crab
Spec fidelity, not surface area
The complete Claude API. Nothing lost in translation.
Streaming, tool use, adaptive thinking, prompt caching, batches — every Claude capability is a first-class Rust type that mirrors the wire exactly. And when Anthropic ships something new, you’re covered from day one: new models, new features, and new content types flow through without breaking a single build.
Streaming SSE
A hand-rolled SSE parser yields fine-grained StreamEvents and accumulates a final Message for you — no eventsource dependency.
Tool use
Define tools with raw JSON schemas, drive the agentic loop yourself, and pass any server-tool through ToolUnion::Raw.
Adaptive thinking
ThinkingConfig covers adaptive, budgeted and disabled modes, with effort and display controls surfaced directly.
Prompt caching
Attach CacheControl to any block with an optional 5m/1h TTL, and read cache hits back off Usage.
Message Batches
Create, poll and cancel batches, then stream results back with a JSONL-decoding Stream<BatchResult>.
Token counting
Turn any request into a count request and get exact input_tokens before you spend on generation.
Typed errors & retries
Every non-2xx maps to a precise Error variant with request_id. Backoff with jitter honors retry-after automatically.
Forward compatible
Unknown block types, stream events and stop reasons deserialize into Unknown catch-alls — new API surface never breaks a build.
Runtime-light & WASM-ready
The public API exposes futures_core::Stream, not tokio types. reqwest underneath means it also checks on wasm32-unknown-unknown.
From zero to streaming in one screen
Ergonomic, but nothing hidden.
A builder for every request, helpers for the common paths, and full command of stop_reason. Here it is end to end.
use crimson_crab::{Client, MessagesRequest, MessageParam, model_ids};
// Reads ANTHROPIC_API_KEY from the environment.
let client = Client::from_env()?;
let req = MessagesRequest::builder()
.model(model_ids::CLAUDE_OPUS_5)
.max_tokens(1024)
.system("You are terse.")
.messages(vec![MessageParam::user("Hello, Claude")])
.build()?; // validates required fields
let msg = client.messages().create(&req).await?;
if msg.stop_reason == Some(StopReason::Refusal) {
// handle a declined response
}
println!("{}", msg.text()); // concat of all text blocks
use futures_util::StreamExt;
use crimson_crab::{StreamEvent, ContentDelta};
let mut stream = client.messages().stream(&req).await?;
while let Some(event) = stream.next().await {
if let StreamEvent::ContentBlockDelta { delta, .. } = event? {
if let ContentDelta::TextDelta { text } = delta {
print!("{text}"); // tokens as they arrive
}
}
}
let final_msg = stream.final_message(); // fully accumulated Message
// ...or collapse the whole stream into a single line:
let msg = client.messages().stream(&req).await?.collect_final().await?;
// The agentic loop, driven for you (new in 0.3). The manual
// loop stays fully supported if you'd rather own the control flow.
/// The arguments of the weather tool.
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct GetWeather {
/// The city to look up, e.g. "Paris".
city: String,
}
let result = client
.messages()
.runner(req)
// A tool and its handler, registered together — they can't drift.
.tool(
Tool::from_type::<GetWeather>("get_weather", "Get the weather"),
|args: GetWeather| async move {
Ok::<_, String>(format!("22C in {}", args.city))
},
)
.max_turns(8)
.run() // tool errors go back to the model, not to you;
.await?; // parallel tool calls answered in wire order
println!("{} (after {} turns)", result.message.text(), result.turns);
// cargo add crimson-crab --features schemars (new in 0.2)
/// A contact extracted from free-form text.
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct Contact {
/// The contact's full name.
name: String,
/// The employer, or `null` if none was mentioned.
company: Option<String>,
}
// The schema comes from the struct — inlined, additionalProperties: false,
// everything required (Option fields become nullable). No JSON by hand.
let parsed = client.messages().parse::<Contact>(&req).await?;
println!("{} ({:?})", parsed.data.name, parsed.data.company);
println!("{} output tokens", parsed.message.usage.output_tokens);
// Tools work the same way — the input schema is derived, doc
// comments and all, from the argument type:
let weather = Tool::from_type::<GetWeather>(
"get_weather",
"Look up the current weather for a city",
);
Depth beats breadth
Why a dedicated client?
Multi-provider frameworks like rig and genai are genuinely great — if you need one interface across many model vendors, use them. crimson-crab makes the winning bet for Claude builders: go deep on one API and track it relentlessly — so every new Claude capability lands here first, typed and tested.
Straight talk: if your app talks to several model vendors, a framework will serve you better. crimson-crab is for teams who have chosen Claude and want the whole surface, exactly as Anthropic ships it.