crimson-crab: a Production-Grade Rust SDK for Claude — and Why tokio Leaves the Dependency Tree on wasm32

crimson-crab is a Rust SDK for Anthropic's Claude API: v0.1.0, 191 passing tests, zero clippy warnings, and a library that denies unwrap, expect and panic at compile time. This is the launch post: why tokio sits in the native dependency tree and is absent from the wasm32 one, why 113 of the 191 tests are the documentation, and what happens when a response arrives from a model the SDK has never heard of.
Run cargo tree --edges normal --invert tokio against crimson-crab on a native target and tokio v1.52.4 is sitting right there, arriving through reqwest v0.12.28 and hyper v1.10.1. Run the same query with --target wasm32-unknown-unknown and the count is zero.
Nothing about the crate changed between those two commands. On wasm32, reqwest resolves to the browser's fetch backend; hyper falls out of the graph and tokio falls out with it. One source tree, default features both times, two dependency graphs.
What it is
crimson-crab 🦀 is a Rust SDK for Anthropic's Claude API. v0.1.0 went up on crates.io on 16 July 2026. It covers Messages and token counting, fine-grained SSE streaming with an accumulated final Message, tool use with custom tools and server-tool passthrough, extended and adaptive thinking, prompt caching with 5-minute and 1-hour TTLs, structured output via JSON Schema, Message Batches, and the Models endpoint.
The scope is deliberately narrow: Claude, and nothing else. If you are building against several model vendors, a multi-provider framework will serve you better, and rig and genai are genuinely good at that job. crimson-crab is for teams who have already chosen Claude and want the whole surface, exactly as Anthropic ships it.
use crimson_crab::model_ids::CLAUDE_OPUS_4_8;
use crimson_crab::prelude::*;
#[tokio::main]
async fn main() -> crimson_crab::Result<()> {
// Reads ANTHROPIC_API_KEY from the environment.
let client = Client::from_env()?;
let request = MessagesRequest::builder()
.model(CLAUDE_OPUS_4_8)
.max_tokens(1024)
.messages(vec![MessageParam::user("Explain Rust's borrow checker in one line.")])
.build()?;
let message = client.messages().create(&request).await?;
println!("{}", message.text());
Ok(())
}
Note the #[tokio::main] in that snippet. On a native target you still bring a runtime; the crate simply declines to choose one for you. Client is Clone + Send + Sync and shares one connection pool, so you build it once and drop it in your axum state or a plain struct field. No Arc, no Mutex.
The dependency that depends on your target
tokio is not a direct dependency of crimson-crab. cargo tree --edges normal --depth 1 lists nine: bytes, fastrand, futures-core, futures-util, pin-project-lite, reqwest, serde, serde_json, thiserror. In the manifest, tokio appears only under [dev-dependencies], where the test suite and the examples use it.
That command proves less than it looks like it proves. --depth 1 excludes dev-dependencies by definition, so it could not have printed tokio whatever the manifest said. The transitive tree is the evidence that counts, and on native it is unambiguous. tokio is in your build, and it has to be, because reqwest's default backend is hyper and hyper runs on tokio. Any reqwest-based crate advertising itself as tokio-free on native is wrong, and cargo tree -i tokio settles the argument in about a second.
What survives that scrutiny is narrower and more useful. Nothing in the public API names a runtime type: streaming hands back a futures_core::Stream, MessageStream is Send + Unpin, and crimson_crab::Error is Send + Sync + std::error::Error. The crate has no opinion about your executor and never asks for one. The claim worth making is about the public API surface and the direct dependency list, not about your final binary.
The wasm32 tree is where that design turns into a difference you can measure. On wasm32-unknown-unknown, reqwest resolves to the browser's fetch backend. hyper leaves the graph and tokio leaves with it, so the count is zero. cargo check --target wasm32-unknown-unknown passes on default features, with no feature juggling and no default-features = false incantation to memorize.
Two limits on what that establishes. cargo check type-checks and borrow-checks, then stops: no codegen, no linking. A clean check means the type system is satisfied. It does not hand you a wasm artifact, and it certainly does not demonstrate one running in a browser. The second limit is that a dependency graph without tokio is a statement about the graph, while what your bundler finally emits is a separate question. So the defensible version is a small one: the crate type-checks clean for wasm32 on default features, and tokio is absent from that target's tree. Anything past that is a promise this post cannot cash.
The docs cannot rot
cargo test --all-features gives 191 passed, 0 failed. The composition is the interesting part: 43 unit tests in src/, 35 integration tests across 7 files, and 113 doc-tests.
113 of 191. The majority of this test suite is the documentation. Worth being exact about what that buys, because "the examples all work" is a claim people make loosely. All 113 are compiled by cargo test. 92 of them also execute. The remaining 21 carry no_run, so they compile and type-check and then stop, because their bodies call the live API and the suite has no key. None carry ignore, so nothing in the docs is hidden from the compiler.
The compile step is where most of the value sits, and it covers all 113. A doc example that drifts out of sync with the API stops compiling, and the build goes red on the commit that broke it. It never gets the chance to become the stale snippet somebody files an issue about eighteen months later.
The integration tests run against wiremock, so the whole suite works offline: no API key, no network, no rate limits, no flakes. You can clone the repo on a plane and get a green run.
Panic-freedom you can check
Plenty of libraries describe themselves as panic-free in a README paragraph. This one is checkable in about ten seconds. src/lib.rs opens with:
#![forbid(unsafe_code)]
#![cfg_attr(
not(test),
deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::todo)
)]
#![deny(missing_docs)]
and cargo clippy --all-features --all-targets passes with 0 warnings. The not(test) scope means those denies bind the library build, so the property holds for a mechanical reason: the compiler refuses to produce a version of the library where it does not. deny(missing_docs) is quietly what keeps the doc-test count honest too, since an undocumented public item fails the build outright.
A response from a model that does not exist yet
Every wire enum in the crate carries an Unknown catch-all: content blocks, stream events, deltas, stop reasons, tool definitions, cache TTLs, thinking configs. An unrecognised variant is preserved as raw JSON and re-serialized unchanged. The enums are #[non_exhaustive], so you match with a wildcard arm and a minor release can add a known variant without breaking your build.
That is the whole mechanism, and it is one of the 92 doc-tests that execute:
use crimson_crab::types::{ContentBlock, TextBlock};
let json = serde_json::json!({"type": "text", "text": "Hello"});
let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
assert_eq!(block, ContentBlock::Text(TextBlock::new("Hello")));
assert_eq!(serde_json::to_value(&block).unwrap(), json);
// An unrecognised block type is preserved rather than rejected.
let novel = serde_json::json!({"type": "brand_new", "foo": 1});
let block: ContentBlock = serde_json::from_value(novel.clone()).unwrap();
assert!(matches!(block, ContentBlock::Unknown(_)));
assert_eq!(serde_json::to_value(&block).unwrap(), novel);
The last two assertions are the ones that matter. A block type nobody had invented when the crate was published deserializes cleanly, and serializes back byte-equivalent to what arrived. Your process keeps running and your logs keep the payload.
The model field is an open string everywhere, which is the same principle applied one level up. The crate exports constants (CLAUDE_OPUS_4_8, CLAUDE_FABLE_5, CLAUDE_SONNET_5, CLAUDE_HAIKU_4_5) as conveniences, and a model missing from that list still works: pass its id as a string. Beta flags get the same escape hatch: .beta("some-flag") appends an anthropic-beta flag and .extra_field(key, value) sets a top-level body field, so a beta that shipped this morning is reachable today without waiting on an SDK release.
Retries, and the streaming timeout
Connection errors, timeouts, 408, 409, 429 and 5xx retry with full-jitter exponential backoff: 0.5s base, 8s cap. retry-after is honored and capped at 60s, so a hostile or broken server cannot park your retry loop for an hour. Streaming requests retry only before the first byte, which is the only safe answer once tokens are already on the wire.
The streaming detail I like most is the timeout. The client applies an idle read timeout rather than a total-request deadline, so a long but actively flowing SSE response is never cut off merely for crossing an elapsed-time limit. If you have ever had a long generation guillotined at exactly thirty seconds by an HTTP client counting wall-clock instead of silence, you already know why that distinction earns its place in the design.
Status, honestly
v0.1.0, published 16 July 2026. A single crate: roughly 6,076 lines in src/ and 1,445 in tests/, with 7 runnable examples (basic, batches, prompt_caching, streaming, structured_output, thinking, tool_use). MSRV 1.75, edition 2021, dual-licensed MIT OR Apache-2.0. Raising the MSRV would be a minor-version change.
Things this post does not contain: benchmarks, latency figures, throughput numbers. None were measured, so none are quoted. There are no adoption numbers either, because it launched hours before this went up and there aren't any yet. What I can hand you instead is a test suite, a clippy run and a dependency tree, every one of which you can reproduce yourself in a few minutes on your own machine. For a v0.1.0, that seems like the honest trade.
Find crimson-crab
- crimson-crab project site — the one-page tour
- crimson-crab on crates.io ·
cargo add crimson-crab - docs.rs/crimson-crab — the API reference, and the source of those 113 doc-tests
- crimson-crab on GitHub — README, ARCHITECTURE.md, and the examples
crimson-crab is an independent open-source project and is not affiliated with Anthropic. Every number in this post comes from a command you can run against a fresh clone: cargo test --all-features, cargo clippy --all-features --all-targets, and cargo tree.
Subscribe to new posts from theaivibe.org
Related Posts

The Hidden Cost of Embedding Model Drift in Production RAG
Your vector index and your query encoder drifted apart months ago. Retrieval quality is quietly collapsing, and nothing in your observability stack noticed.

The Future of Local AI: Every AI Lab Should Redesign Its LLM Architecture to Run on Your Laptop
Kimi K3 proved open-weight LLMs can reach the frontier — and proved they're far too big to run where users actually are. The next race isn't a bigger model; it's the architecture review that puts frontier AI on an ordinary laptop.

I Gave Quantized LLM Checkpoints a Type, and the Type Immediately Caught Real Bugs
A four-bit model file tells you how many elements it has — and almost nothing else that matters. Not the scale-derivation rule, not the zero-point convention, not the packing order. In 2026 alone, six documented incidents across vLLM and SGLang turned those silent agreements into silently wrong model output. GRIT is my answer: a 64-byte descriptor and an O(1) boundary check for block-scaled reduced-precision arrays, with five zero-dependency implementations that agree bit-for-bit on 96/96 cross-language fingerprints — and a read-only scanner that found real convention ambiguity in checkpoints you can download today.