crimson-crab
Health Uyari
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Gecti
- Code scan — Scanned 2 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
A Rust SDK for Anthropic's Claude API that won't panic on you — no unwrap/expect/panic, denied at compile time. Full Claude surface: streaming, tools, thinking, prompt caching, batches.
🦀 crimson-crab
A Rust SDK for Anthropic's Claude API that can't panic on you.
The full Claude API in idiomatic Rust — streaming, tool use, extended thinking, prompt caching, and batches — built on one hard guarantee: no unwrap, expect, panic!, or todo! anywhere in the library, denied at compile time. A malformed or unexpected response is always an Error you handle, never a panic in your service. Wire-faithful types and forward-compatible enums keep working the day Anthropic ships a new model or block type. Claude-only, and deep on purpose.
191 tests · zero clippy warnings · a panic-free library (unwrap/expect/panic denied at compile time) · MSRV 1.75 · MIT OR Apache-2.0
- Docs: docs.rs/crimson-crab
- Site: singhpratech.github.io/crimson-crab
- Source: github.com/singhpratech/crimson-crab
Install
cargo add crimson-crab
Streaming and batch results are plain futures_core::Streams. To drive them with .next(), add the StreamExt extension trait:
cargo add futures-util
30-second quickstart
use crimson_crab::model_ids::CLAUDE_OPUS_5;
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_5)
.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(())
}
Client is Clone + Send + Sync and shares one connection pool, so build it once and store it in your axum state, your MCP server struct, or a plain field — no Arc, no Mutex, no manual bounds.
Why crimson-crab
- Panic-free by construction — the one guarantee for code in your request path.
unwrap,expect,panic!, andtodo!are denied at compile time across the whole library and enforced in CI — not a style guideline, a lint gate. However surprising the JSON on the wire, you get anErrorto handle, never a panic in an async task. Few Claude clients make this promise; this one does, and you can verify it insrc/lib.rs. - Wire-faithful types — your code never breaks on new models. Types mirror the API field-for-field; there are no renamed concepts or leaky abstractions to relearn, and no adapter layer to lag behind a release.
- Forward-compatible enums — future models work day one. Every wire enum (content blocks, stream events, deltas, stop reasons, tool definitions, cache TTLs, thinking configs) carries an
Unknowncatch-all that preserves the raw JSON and re-serializes it unchanged instead of erroring. A response from a model the SDK has never heard of — including future Fable- and Mythos-class models — deserializes cleanly and round-trips verbatim. - A tokio-free public API — runs anywhere. The public surface exposes
futures_core::Stream, not runtime-specific types;tokiois a dev-dependency only. The same builder code compiles for native andwasm32-unknown-unknownon default features. - Official-SDK-parity retries — production-grade out of the box. Connection errors, timeouts,
408/409/429, and5xxare retried with full-jitter exponential backoff (0.5s base, 8s cap) and honorretry-after— capped at 60s so a hostile or broken server can't park your retry loop for hours. Streaming requests retry only before the first byte. - Streaming that never truncates mid-generation. The client uses an idle read timeout rather than a total-request deadline, so a long-but-actively-flowing SSE response is never cut off just because total elapsed time crossed a limit.
- Tested against real API fixtures. Every content block and stream event from the wire reference has a serde round-trip test, and every endpoint has
wiremockcoverage — 191 tests, zero clippy warnings.
Feature coverage
| Capability | crimson-crab | Generic multi-provider clients |
|---|---|---|
| Messages (create / count tokens) | ✅ | ✅ |
Fine-grained SSE streaming + accumulated final Message |
✅ | usually text-only |
| Tool use (custom tools + server-tool passthrough) | ✅ | partial |
| Extended thinking (adaptive / budgeted / display) | ✅ | rare |
Prompt caching (cache_control, 5m/1h TTLs) |
✅ | rare |
Structured output (output_config JSON Schema) |
✅ | rare |
| Message Batches (create / poll / cancel / stream results) | ✅ | rare |
| Models endpoint (get / list with pagination) | ✅ | varies |
| Forward-compatible unknown-variant handling | ✅ | varies |
New beta flags without an SDK release (betas + extra_body) |
✅ | rare |
Claude-specific features are first-class here because Claude is the only API this crate targets. Building against several model vendors? A multi-provider framework will serve you better — rig and genai are genuinely good. crimson-crab is for teams who have chosen Claude and want the whole surface, exactly as Anthropic ships it.
Streaming
Iterate typed events as they arrive; the stream accumulates a complete Message for you in the background.
use crimson_crab::prelude::*;
use futures_util::StreamExt;
# async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result<()> {
let mut stream = client.messages().stream(request).await?;
while let Some(event) = stream.next().await {
if let StreamEvent::ContentBlockDelta {
delta: ContentDelta::TextDelta { text },
..
} = event?
{
print!("{text}");
}
}
// After draining, the accumulated `Message` is identical in shape to a
// non-streaming response.
if let Some(message) = stream.final_message() {
println!("\n[stop_reason: {:?}]", message.stop_reason);
}
# Ok(())
# }
Relaying text deltas (SSE bodies, channels, web handlers)
MessageStream is Send + Unpin and crimson_crab::Error is Send + Sync + std::error::Error, so a streaming response drops straight into an axum Sse body (or any channel) with no Box::pin and no wrapper error type. Map the event stream down to plain String deltas with filter_map:
use crimson_crab::prelude::*;
use futures_util::StreamExt;
# async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result<()> {
// A `Stream<Item = crimson_crab::Result<String>>` of plain text deltas, ready to
// hand to an `axum` `Sse` body, a channel, or any consumer.
let text_deltas = client
.messages()
.stream(request)
.await?
.filter_map(|event| async move {
match event {
Ok(StreamEvent::ContentBlockDelta {
delta: ContentDelta::TextDelta { text },
..
}) => Some(Ok(text)),
// A late/in-stream error surfaces as an `Err` item — forward it as a
// final `event: error` frame instead of dropping the connection.
Err(e) => Some(Err(e)),
Ok(_) => None,
}
});
forward(text_deltas).await;
# Ok(())
# }
# async fn forward<S>(_deltas: S)
# where
# S: futures_util::Stream<Item = crimson_crab::Result<String>>,
# {
# }
Tool use (manual agentic loop)
message.into_param() converts a response Message straight into a request MessageParam — the two tool_use blocks echoed verbatim — so the "append the assistant turn, then a user message of tool results" contract is two lines with no lossy serde round-trip. Parallel tool calls need no special handling: just iterate message.content.
use crimson_crab::prelude::*;
use crimson_crab::types::ToolResultBlockParam;
# async fn run(client: &Client, mut messages: Vec<MessageParam>, tool: Tool) -> crimson_crab::Result<()> {
loop {
let request = MessagesRequest::builder()
.model("claude-opus-4-8")
.max_tokens(1024)
.messages(messages.clone())
.tool(tool.clone()) // `.tool(_)` appends any `Into<ToolUnion>`; `.tools(vec)` replaces
.build()?;
let message = client.messages().create(&request).await?;
if message.stop_reason != Some(StopReason::ToolUse) {
println!("{}", message.text());
break;
}
// Answer every tool call. `ContentBlock` is in the prelude, so matching
// `ToolUse` needs no extra import.
let mut results = Vec::new();
for block in &message.content {
if let ContentBlock::ToolUse(call) = block {
match run_tool(&call.name, &call.input) {
// Success: the discoverable `ContentBlockParam::tool_result` helper.
Ok(output) => results.push(ContentBlockParam::tool_result(&call.id, output)),
// Failure: surface it to the model with `is_error: true`.
Err(why) => results.push(ContentBlockParam::ToolResult(
ToolResultBlockParam::error(&call.id, why),
)),
}
}
}
// Echo the assistant turn back verbatim, then one user message of results.
messages.push(message.into_param());
messages.push(MessageParam::user(results));
}
# Ok(())
# }
// Your tool dispatch. Note `std::result::Result<_, _>`: see "Imports & the prelude".
# fn run_tool(_name: &str, _input: &serde_json::Value) -> std::result::Result<String, String> {
# Ok("tool output".to_string())
# }
Prompt caching & token budgeting
The simplest caching path: a plain-string system prompt plus a top-level cache_control, which auto-places one breakpoint on the last cacheable block — no per-block wiring.
use crimson_crab::prelude::*;
# async fn run(client: &Client) -> crimson_crab::Result<()> {
let request = MessagesRequest::builder()
.model("claude-opus-4-8")
.max_tokens(256)
.system("A long, reusable system prompt worth caching…")
.cache_control(CacheControl::ephemeral()) // or `CacheControl::ephemeral_with_ttl(CacheTtl::OneHour)`
.messages(vec![MessageParam::user("Restate rule one.")])
.build()?;
let message = client.messages().create(&request).await?;
let usage = &message.usage;
println!(
"fresh input: {} written to cache: {:?} read from cache: {:?}",
usage.input_tokens,
usage.cache_creation_input_tokens,
usage.cache_read_input_tokens,
);
# Ok(())
# }
Token accounting for cost reports. The three input buckets are disjoint: input_tokens counts only the uncached input, while cache_creation_input_tokens and cache_read_input_tokens are separate. Total input tokens = input_tokens + cache_creation_input_tokens + cache_read_input_tokens. Never add the cache buckets into input_tokens — that double-bills the cached prefix.
Need the number before you spend on generation? Derive a count request from the same messages request — no rebuilding the prompt twice:
# use crimson_crab::prelude::*;
# async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result<()> {
let count = client.messages().count_tokens(&request.as_count_request()).await?;
println!("this request will cost {} input tokens", count.input_tokens);
# Ok(())
# }
For fine-grained control you can attach a breakpoint to an individual block instead: build a TextBlockParam (in crimson_crab::types), set cache_control, and pass it as a system block or message content — see examples/prompt_caching.rs.
Message Batches
The whole submit → poll → stream → tally pipeline. BatchRequestItem::from_request turns a MessagesRequest into a batch entry with no hand-rolled JSON; BatchStatus and BatchRequestCounts are typed for progress display; and results() decodes the JSONL stream line-by-line, tolerating blank lines and a missing trailing newline.
use crimson_crab::api::{BatchRequestItem, BatchResultOutcome, BatchStatus};
use crimson_crab::prelude::*;
use futures_util::StreamExt;
# async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result<()> {
// Submit, each entry tagged with your own custom id.
let items = vec![BatchRequestItem::from_request("row-1", request)?];
let batch = client.batches().create(&items).await?;
// Poll until the batch reaches a terminal state. (A built-in `poll_until_ended`
// helper is on the v0.2 roadmap; until then, loop with your runtime's timer and
// your own deadline guard.)
let batch = loop {
let current = client.batches().get(&batch.id).await?;
if current.processing_status == BatchStatus::Ended {
break current;
}
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
};
// Results arrive in any order — key them by `custom_id`, never by position.
let mut results = client.batches().results(&batch.id).await?;
while let Some(result) = results.next().await {
let result = result?;
match result.result {
BatchResultOutcome::Succeeded(ok) => {
println!("{}: {}", result.custom_id, ok.message.text());
}
BatchResultOutcome::Errored(err) => {
// `err.error` is the raw error envelope (a `serde_json::Value`).
println!("{}: errored: {}", result.custom_id, err.error);
}
BatchResultOutcome::Canceled(_) | BatchResultOutcome::Expired(_) => {}
// `BatchResultOutcome` is `#[non_exhaustive]`; the wildcard keeps you
// forward-compatible with outcome types added in a future release.
_ => {}
}
}
# Ok(())
# }
Imports & the prelude
use crimson_crab::prelude::*; is the fastest way to get the common types — Client, MessagesRequest, MessageParam, StreamEvent, ContentDelta, ContentBlock, ContentBlockParam, Tool, ToolChoice, StopReason, CacheControl, and more.
One thing worth knowing: the prelude also re-exports the crate's Result<T> and Error type aliases, which shadow std::result::Result / std::error::Error inside a glob import. If you write a two-type-argument Result<T, E> in the same scope — common with axum handlers, thiserror, or macro-heavy crates like rmcp — it resolves to the one-argument alias and fails with a confusing E0107 ("type alias takes 1 generic argument but 2 were supplied"). Two easy fixes:
- Fully-qualify it:
std::result::Result<String, String>(as in the tool-loop example above); or - Skip the glob and import exactly what you need. Curated paths:
crimson_crab::Client, request/response types undercrimson_crab::api::*(e.g.MessagesRequest), and the wire types undercrimson_crab::types::*(e.g.MessageParam,TextBlockParam,ToolResultBlockParam).
Platform support
The crate compiles for native targets and wasm32-unknown-unknown on default features — no feature juggling. On wasm, reqwest resolves to the browser fetch backend and TLS features are ignored, so you do not need to disable rustls-tls; cargo tree -i tokio --target wasm32-unknown-unknown prints nothing.
Two honest caveats for edge/browser deployments: the retry loop cannot sleep on wasm (there are no threads), so on that target retries fire without backoff and do not observe retry-after — the browser applies its own backpressure. Streaming type-checks on wasm but is best-effort and not exercised in a headless browser in CI. For a 429-sensitive edge worker, cap max_retries accordingly.
More examples
Runnable programs live in examples/: basic, streaming, tool_use, thinking, prompt_caching, and structured_output. There's also live_smoke — a manual end-to-end check that makes real API calls (token count + non-streaming + streaming). Run one with:
ANTHROPIC_API_KEY=sk-ant-... cargo run --example streaming
Build a Claude-powered MCP server
Want a ready-to-run Model Context Protocol server backed by Claude? Start from the template — a Rust MCP server (rmcp + crimson-crab) you can spin up in one click:
→ crimson-crab-mcp-template — click Use this template, or:
cargo generate --git https://github.com/singhpratech/crimson-crab-mcp-template
Roadmap
Shipping fast and iterating; spec fidelity and release cadence are the whole point. On deck for v0.2+: the Files API (beta), schemars-derived tool schemas with a #[tool] macro, typed tool-input deserialization, a tool-runner loop helper, a batches().poll_until_ended() convenience, a parse::<T>() structured-output helper, the Admin/Usage API, and Vertex/Bedrock transports. The model field is an open string everywhere, so any model Anthropic ships works today with zero changes.
Minimum supported Rust version
MSRV is 1.75. Raising it is a minor-version change.
Semver policy
The wire enums are #[non_exhaustive] and carry Unknown catch-all variants, so a minor release may add a new known variant (for a feature Anthropic ships) without breaking your build. Match with a wildcard _ arm on SDK enums to stay forward-compatible across minor versions.
License
Licensed under either of MIT or Apache-2.0 at your option.
🦀 Crimson Crab · crimson-crab is an independent open-source project and is not affiliated with Anthropic.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi