llm-content-blocks 0.1.0

Typed fluent builder for Anthropic Messages-API content blocks (text, image, tool_use, tool_result, document). Emits the exact JSON shape the API expects. No SDK dependency.
Documentation

llm-content-blocks

Crates.io Documentation License

Typed fluent builder for Anthropic Messages-API content blocks. No SDK requirement, narrow deps (serde, serde_json, base64).

use llm_content_blocks::{Blocks, CacheControl};

let content = Blocks::new()
    .text("Look at this image:")
    .image_b64(&png_bytes, "image/png").unwrap()
    .text_with_cache("What's in it?", CacheControl::Ephemeral)
    .build();

// wrap as a full message
let user_msg = Blocks::new().text("Hi").build_message("user");
// Message { role: "user", content: vec![Text { text: "Hi", ... }] }

// tool result (one-shot, not chained)
use serde_json::json;
let tr = Blocks::tool_result_block("toolu_1", json!("the answer"), false);

Why

The Anthropic Messages content-block shapes are simple but fiddly when you assemble them programmatically: tool responses, multi-image inputs, cached system prompts, retry-with-correction loops. Blocks is a fluent builder that emits the exact JSON shape the API expects.

No SDK dependency. The crate doesn't talk to the API. It just makes the right shape, so you can use it from any HTTP client (reqwest, ureq, hyper) or pass the output through serde_json to the official SDK.

Supported blocks: text, image (base64 or URL), tool_use, tool_result, document (PDF, plain text).

Install

[dependencies]
llm-content-blocks = "0.1"

API

use llm_content_blocks::{Blocks, CacheControl};
use serde_json::json;

// chained appends, return &mut self
let mut b = Blocks::new();
b.text("hello");
b.text_with_cache("cached body", CacheControl::Ephemeral);
b.image_b64(&bytes, "image/png").unwrap();
b.image_url("https://example.com/x.png");
b.tool_use("toolu_1", "search", json!({"q": "anthropic"}));
b.tool_result("toolu_1", json!("the answer"), false);
b.document_b64(&pdf_bytes, "application/pdf").unwrap();
b.document_pdf_b64(&pdf_bytes);

// terminal
let blocks = b.build();                              // Vec<ContentBlock>
let msg = Blocks::new().text("hi").build_message("user"); // Message { role, content }

// one-shot helpers
let tr = Blocks::tool_result_block("toolu_1", json!("answer"), false);

CacheControl::Ephemeral is the only valid value today (Anthropic's prompt cache). The cache_control field is omitted from JSON when unset. The crate rejects unknown image and document media types up front with a typed [BlockError], so you get a build-site error instead of a 400 from the API.

Serialization

ContentBlock derives serde::Serialize with #[serde(tag = "type", rename_all = "snake_case")]. The produced JSON matches the Anthropic dict shape exactly. Pass &content (or the Message envelope) to serde_json::to_value / serde_json::to_string and ship it.

Companion libraries

  • claude-cost - cache-aware cost calculator for Anthropic + Bedrock model IDs.
  • agentprompt-rs - Jinja-style prompt template render; pair with this builder for final message construction.
  • claude-stream-rs - incremental SSE parser for Anthropic streamed responses.

License

MIT OR Apache-2.0