Anthropic
- In one line
- Dispatches unified requests to the Anthropic Messages API with an
x-api-keyheader or an OAuth bearer token. - You need
- The
provider-anthropicfeature and either an API key or a bearer token. - Read this if
- You route requests through
ProviderEndpoint::Anthropic, use extended thinking or prompt caching, or want the Messages translation that DeepSeek reuses for its bridge.
This adapter speaks the Anthropic Messages protocol. Against the OpenAI page it differs in four ways:
- The key goes in
x-api-key, and every request carriesanthropic-version: 2023-06-01. max_tokensis always sent and defaults to 1024.- The stream has typed events, and a thinking block carries a signature.
- Token counts are not read; only prompt cache counters are.
Endpoint
| Fact | Value |
|---|---|
| Feature flag | provider-anthropic |
ProviderEndpoint variant | Anthropic |
| Default base URL | https://api.anthropic.com/v1, used when the client's base URL is empty |
| Route | POST {base_url}/messages |
The default base URL carries /v1. The adapter appends only /messages and trims a trailing / first.
Authentication
Exactly one credential is sent.
| Source | Header |
|---|---|
CucaClientBuilder::with_bearer_token | Authorization: Bearer <token> |
CucaClientBuilder::with_api_key | x-api-key: <key> |
- When both are set, the bearer token is used.
- Every request also carries
anthropic-version: 2023-06-01. - Dispatch sends at most one
anthropic-betaheader,prompt-caching-2024-07-31; see Prompt caching. - With neither credential, dispatch fails with
CucaError::Config("anthropic requires an api key or bearer token").
Request translation
The request body is built from a UnifiedRequest:
modelis the request's model;streamis alwaystrue.max_tokensis required by the API and defaults to 1024 when the request leaves it unset.temperatureis included only when set.systemis everyTextblock of everySystemmessage joined with a newline, as one string. With a prompt cache breakpoint on a system block it is an array of{type: "text"}blocks instead. Non-text system blocks are dropped, and the key is omitted with no system messages.messagescarries user and assistant turns only. Tool results ride inside a user message astool_resultblocks, and standaloneTool-role messages are skipped.toolscarries everyUnifiedRequest::toolsentry as{name, description, input_schema}. The key is omitted when the request declares no tool, and notool_choiceis sent.- Content blocks map as follows:
Textbecomes{type: "text"}.ImageBase64becomes animageblock with abase64source.Thinkingbecomes athinkingblock, withsignatureonly when the block has one.ToolCallbecomestool_usewithinputholding the arguments.ToolResultbecomestool_resultwithtool_use_idandcontent.
Response stream
The SSE parser yields typed events, and the translator keeps at most one content block in flight.
- A
text_deltabecomesMessageContentBlock::Textimmediately. thinking_deltaandsignature_deltaaccumulate and are emitted as oneThinkingblock, with its signature, atcontent_block_stop.input_json_deltafragments accumulate and are emitted as oneToolCallblock atcontent_block_stop. Fragments that do not parse as JSON givenullarguments.message_start,message_deltaandpingproduce no block. Unknown event types are ignored.message_stopends the stream. The stream also ends when the byte stream ends.
Token usage
- The adapter reads no
input_tokensoroutput_tokens.UnifiedResponse::usageisNone,prompt_tokensis0, andcompletion_tokenscounts one perText,ThinkingandToolCallblock. - Prompt cache counters are read from
message_start:usage.cache_read_input_tokensbecomesPromptCacheUsage::read_tokensandusage.cache_creation_input_tokensbecomeswrite_tokens. - A counter present alone pairs with zero. With neither counter,
UnifiedResponse::prompt_cache_usageisNone.
Thinking
A top-level thinking key is sent when req.thinking is set and enabled. A disabled or unset config sends no key.
- Budget mode sends
{"type": "enabled", "budget_tokens": N}.NisThinkingParams::Anthropic { budget_tokens: Some(n), .. }when set, otherwise the map below. - Adaptive mode applies when
ThinkingParams::Anthropic { adaptive: true, .. }is set. It sends{"type": "adaptive"}plus"effort"when the unified effort is set.
Unified ThinkingEffort | Budget mode budget_tokens | Adaptive mode effort |
|---|---|---|
Minimal | 1024 | "low" |
Low | 2048 | "low" |
Medium | 8192 | "medium" |
High | 16384 | "high" |
XHigh | 16384 (same as High) | "xhigh" |
| unset | 10000 | key omitted |
Errors
| Condition | Error |
|---|---|
| Feature not compiled | CucaError::ProviderNotEnabled("provider-anthropic") |
| No API key and no bearer token | CucaError::Config |
| Key, token or beta value is not a valid header value | CucaError::Config |
| Invalid prompt cache breakpoint | CucaError::Config; see Prompt caching |
| Non-2xx response | CucaError::Http { status, body } with the captured body |
| Connection or body-read failure | CucaError::Transport { message } |
content_block_* or error event is not valid JSON | CucaError::Json |
error event | CucaError::Provider with provider: Anthropic and the event's message |
| Frame bytes are not valid UTF-8 | CucaError::SseParse |
A malformed frame or an error event ends the stream after the error.
Limits
- No cap on request or response size, and the client sets no request timeout.
- The SSE parser keeps one reusable buffer that starts at 8192 bytes. Its high-water mark is the largest single frame plus one partial line.
- One thinking text, one signature and one tool call input are buffered at a time, each until its
content_block_stop. max_tokensdefaults to 1024. Nothing caps a value the request sets.
Prompt caching
PromptCacheDirective::Ephemeral breakpoints add "cache_control": {"type": "ephemeral"} to the marked content blocks.
- The
anthropic-beta: prompt-caching-2024-07-31header is sent exactly when the request's selected provider is Anthropic and the directive has at least one breakpoint. - A breakpoint on a system block makes
systeman array of text blocks instead of one string. - Each breakpoint is a
(message_index, block_index)pair. A duplicate pair, an index out of range, a non-text system block, or a standaloneTool-role message is aCucaError::Config. - The DeepSeek bridge forces the directive to
Disabled; see DeepSeek.
OAuth 2.0 PKCE helpers
The provider-anthropic feature adds the pieces of the RFC 7636 authorization-code flow:
OAuthPkceConfigholdsclient_id,authorize_url,token_urlandscopes.generate_pkce_pair()returns a code verifier of 64 random bytes, base64url encoded without padding (86 characters), and the challengebase64url(SHA-256(verifier)). It fails withCucaError::Iowhen the system has no randomness source.authorization_url(config, challenge, state)builds the URL withresponse_type=code,client_id,code_challenge,code_challenge_method=S256,state, andscope(scopes joined by+) when scopes are set.exchange_code(config, code, verifier, redirect_uri)postsgrant_type=authorization_codeto the token endpoint and returns theaccess_token. It fails withCucaError::Httpon a non-2xx response andCucaError::Providerwhen the response has noaccess_token.
These items live in the crate-private provider module, and the crate root does not re-export them. CucaClientBuilder::with_anthropic_oauth stores an OAuthPkceConfig that CucaClient::oauth_config returns. Dispatch does not read it and nothing in the crate calls exchange_code. A token obtained by any means is passed to CucaClientBuilder::with_bearer_token.
Example
The smallest streaming turn: the Anthropic variant and an API key. A bearer token through with_bearer_token is the other auth mode.
cargo add cuca --path /path/to/cuca --features provider-anthropic
cargo add tokio --features rt,macros
cargo add tokio-streamexport ANTHROPIC_API_KEY="sk-ant-..."$env:ANTHROPIC_API_KEY = "sk-ant-..."use std::io::{Write, stdout};
use cuca::types::{MessageContentBlock, ProviderEndpoint};
use cuca::{CucaClient, UnifiedRequest};
use tokio_stream::StreamExt;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = CucaClient::builder()
.with_provider(ProviderEndpoint::Anthropic)
.with_api_key(std::env::var("ANTHROPIC_API_KEY")?)
.build()?;
let request = UnifiedRequest::new("claude-sonnet-4-0")
.add_system_message("You are concise.")
.add_user_message("Say hello.")
.set_max_tokens(128);
let mut stream = client.generate_stream(request).await?;
let mut text_blocks = 0usize;
let mut thinking_blocks = 0usize;
while let Some(block) = stream.next().await {
match block? {
MessageContentBlock::Text(text) => {
print!("{text}");
stdout().flush()?;
text_blocks += 1;
}
MessageContentBlock::Thinking { .. } => thinking_blocks += 1,
_ => {}
}
}
println!("\nblocks: {text_blocks} text, {thinking_blocks} thinking");
Ok(())
}cargo run
The provider-anthropic feature comes from the cargo add line above.