Anthropic provider
Standalone — usable in any Node project, no
@warlock.js/corerequired.
@warlock.js/ai-anthropic is the Anthropic provider adapter for
@warlock.js/ai. It wraps the official
@anthropic-ai/sdk Messages API and hands you a vendor-neutral
ModelContract — the same shape every agent, workflow, and supervisor
already speaks. Swap Claude in for any other provider without touching
your orchestration code.
Install
Section titled “Install”npm install @warlock.js/ai @warlock.js/ai-anthropicyarn add @warlock.js/ai @warlock.js/ai-anthropicpnpm add @warlock.js/ai @warlock.js/ai-anthropicConstruct
Section titled “Construct”One AnthropicSDK holds one live Anthropic client. Build it once at
boot and reuse it everywhere — every model it produces shares the same
connection pool, auth, and rate-limit state.
import { AnthropicSDK } from "@warlock.js/ai-anthropic";
const anthropic = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY!,});The config extends the upstream ClientOptions, so anything the SDK
accepts works here too — most usefully baseURL to point at a gateway
or proxy, and provider to relabel the upstream (it defaults to
"anthropic" and flows through to AgentReport.model, logs, and any
provider-aware middleware).
const anthropic = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY!, baseURL: "https://my-gateway.internal/anthropic", provider: "anthropic-gateway",});First call
Section titled “First call”Build a model, hand it to an agent, run it. execute() never throws —
failures land in error as a typed AIError.
import { ai } from "@warlock.js/ai";import { AnthropicSDK } from "@warlock.js/ai-anthropic";
const anthropic = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY! });
const assistant = ai.agent({ model: anthropic.model({ name: "claude-sonnet-4-6" }), systemPrompt: "You are a concise senior TypeScript engineer.",});
const { text, usage, error } = await assistant.execute("Why use generics?");
if (error) { console.warn(error.code, error.category);} else { console.log(text, usage.total);}Use as a model
Section titled “Use as a model”const model = anthropic.model({ name: "claude-sonnet-4-6", temperature: 0.7 });// Pass to any @warlock.js/ai agent / workflow / supervisor.Anthropic requires a token cap (max_tokens) on every request. When you
don’t set maxTokens (per-model, per-call, or through an agent’s
modelOptions), the adapter picks a default sized for agent work:
| Request | Default max_tokens |
|---|---|
Streaming (stream()) | The model’s documented max output — 128K for the Claude 5 family and Opus 4.6–4.8 / Sonnet 4.6, 64K for Opus / Sonnet / Haiku 4.5 |
Non-streaming (complete()) | 16384, capped by the model’s max output |
| Unknown model id | 16384 (set maxOutputTokens on the model to use its real limit) |
The non-streaming default stays at 16384 because the Anthropic SDK refuses
a non-streaming request whose max_tokens implies more than ten minutes
of generation unless the client sets a timeout; stream long outputs (an
agent’s stream()), or set maxTokens and a client timeout yourself.
With extended thinking on, the default also keeps room for the thinking
budget plus 4096 answer tokens. Limits are taken from Anthropic’s model
pages (checked 2026-10-17).
anthropic.model({ name: "claude-haiku-4-5" }); // stream: 64000, complete: 16384anthropic.model({ name: "my-gateway/opus", maxOutputTokens: 128_000 }); // a gateway idanthropic.model({ name: "claude-opus-5-5", maxTokens: 8192 }); // explicit cap always winsA response that still hits the cap ends the agent run as incomplete
(AgentIncompleteError, report.status: "incomplete") — or continues,
with continueOnOutputLimit. See
Cut-off responses.
Capabilities
Section titled “Capabilities”- Tool calling — vendor-neutral
ToolConfigs map to Claudetool_useblocks; tool results round-trip back asuserturns. - Streaming —
stream()yields textdeltas, atool-callonce atool_useblock’s arguments have fully accumulated, and a terminaldonecarrying the final finish reason and usage. - Structured output — on by default. A root-object JSON Schema is
forwarded to Claude’s native
output_config.format(JSON-schema structured outputs). Non-object schemas or astructuredOutput: falseoverride fall back to the agent’s soft system-prompt hint plus client-side validation. - Vision — auto-detected from the model name. Every Claude 3,
3.5/3.7, and 4 family model is treated as multimodal (prefixes
claude-3,claude-4,claude-opus-4,claude-sonnet-4,claude-haiku-4); pre-3 families (claude-2,claude-instant) are text-only. Override either way with thevisionflag. - No embedder. Anthropic ships no first-party embeddings API, so
this adapter has no
embedder(). Pair Claude with an embedding provider — OpenAI, Google, Bedrock, or Ollama — when you need vectors.
Image attachments accept both a remote url and inlined base64
bytes (image/jpeg, image/png, image/gif, image/webp).
Prompt caching
Section titled “Prompt caching”Claude bills the most-repeated tokens — tool schemas, the system prompt — at a fraction of the input rate when reused across trips. The adapter exposes two independent breakpoints:
| Where | How you opt in | What it caches |
|---|---|---|
| Tool schemas | model({ promptCaching: true }) (per-model) | Marks the tools block cache_control: { type: "ephemeral" } so multi-trip agents reuse tool definitions at the cache-read rate. |
| System prompt | modelOptions: { cacheControl: { breakpoints: 1 } } (per-call) | Emits the system prompt as a cached text block. |
const model = anthropic.model({ name: "claude-sonnet-4-6", promptCaching: true, // cache the static tool schemas across trips});promptCaching is off by default: a cache write costs ~1.25×, so a breakpoint only pays off when the cached prefix is read at least twice (multi-trip / multi-turn agents). The split is deliberate — tool schemas are static (a per-model decision), while the system prompt often carries per-turn placeholders (a per-call decision). Reads and writes flow back as Usage.cachedTokens / Usage.cacheWriteTokens (1-hour writes also as Usage.cacheWrite1hTokens) and are billed at their own rates — see Pricing and usage.
Per call, cacheControl: { ttl: "1h" } overrides the model’s promptCacheTtl for the entries that call writes.
Thinking on Claude 5 models
Section titled “Thinking on Claude 5 models”Claude 5 models run adaptive thinking by default and reject budget_tokens with a 400, so a reasoning option becomes thinking: { type: "adaptive" } plus output_config.effort. effort: "none" depends on the model:
| Model | effort: "none" sends | Why |
|---|---|---|
| Sonnet 5.5 | thinking: { type: "between_tools" } alone | Its only thinking-off form. No output_config.effort and no display — the API rejects either beside it; the model default effort, high, is within the range it allows. |
| Sonnet 5, Haiku 5.5 | thinking: { type: "disabled" }, no effort, no display | Both accept disabled. Haiku 5.5 accepts it only at effort high or below, so no effort is sent and its default, medium, applies. |
| Opus 5 | thinking: { type: "adaptive" } + output_config.effort: "low" | Opus 5 accepts disabled, but with thinking off it can write a tool call into its visible text instead of a tool_use block (the call never runs, no error is raised) and leak thinking tags. Anthropic recommends lowering effort instead, so the adapter keeps thinking on at the lowest effort. |
| Opus 5.5, Fable 5 / 5.1 | no thinking field | No thinking-off form exists (disabled is a 400 at every effort), so the model still thinks at its default effort. Pass effort: "low" to spend less. |
Thinking text is empty by default on these models. Ask for readable summaries with thinkingDisplay:
await agent.execute("Plan the migration.", { modelOptions: { reasoning: { effort: "high" }, providerOptions: { anthropic: { thinkingDisplay: "summarized" } }, },});Provider blocks and compaction
Section titled “Provider blocks and compaction”Thinking, redacted-thinking, compaction and server-tool blocks have no place in the neutral content string, and several features only work when the assistant turn goes back exactly as Claude sent it. When a response has such a block, the adapter returns the whole ordered turn as blocks. The agent stores it on the assistant message and the adapter replays it verbatim on the next call.
Claude checks that every turn before a replayed thinking block is unchanged, including the tool-call turns and tool results in between. Storing only the final reply with its blocks drops those turns, and the next request fails (or the block is dropped). To carry blocks across runs, keep the run’s turns from report.messages (set captureMessages: true) and append the final reply:
// The agent runs with `captureMessages: true`.const result = await chat.execute(question, { history });const finalTrip = result.report.trips.at(-1);
history = [ // Your history, the question, and every tool-call turn and tool result of the run. // The agent re-adds the system prompt on each run, so leave it out here. ...(result.report.messages ?? []).filter((message) => message.role !== "system"), // The final reply, as the model sent it. { role: "assistant", content: finalTrip?.output ?? "", blocks: finalTrip?.blocks },];result.blocks is set only for a single-trip run, where the reply is the whole turn.
The adapter replays a stored block only when it is a well-formed assistant-turn block from this adapter (its data.type matches its type); otherwise it rebuilds the turn from content and toolCalls. Treat a transcript that comes from a client like any other client input.
Beta features go through providerOptions.anthropic: betas routes the call to the beta endpoint, and body adds request fields as-is, after the adapter’s own. body may not set model, messages, system, tools, mcp_servers or stream, which the adapter builds from the call; the call fails with InvalidRequestError if it tries. The same body carries fallbacks, inference_geo or any other field the Messages API accepts. Other adapters ignore the anthropic key, so the options are safe in a fallback chain.
Context editing and server-side compaction
Section titled “Context editing and server-side compaction”Two Anthropic betas manage a long conversation on the server, and both have typed options. The adapter adds the context_management.edits and the beta headers for you:
const agent = ai.agent({ model: anthropic.model({ name: "claude-opus-5-5" }), tools, modelOptions: { providerOptions: { anthropic: { // Clear old tool results (and optionally old thinking) once the prompt grows. contextManagement: { clearToolUses: { trigger: { inputTokens: 100_000 }, keep: 5, excludeTools: ["memory"] }, clearThinking: { keep: 2 }, }, // Summarize earlier context on the server when the prompt reaches the trigger. serverCompaction: { triggerTokens: 200_000, instructions: "Summarize the transcript inside <summary></summary> tags. Do not call any tools while writing this summary; respond with text only.", }, }, }, },});| Option | Wire field (edit) | Beta |
|---|---|---|
contextManagement.clearToolUses | clear_tool_uses_20250919: trigger, keep, clear_at_least, exclude_tools, clear_tool_inputs | context-management-2025-06-27 |
contextManagement.clearThinking | clear_thinking_20251015: keep ({ type: "thinking_turns", value } or "all"), always listed first | context-management-2025-06-27 |
serverCompaction | compact_20260112: trigger (input tokens, at least 50,000; API default 150,000), instructions | compact-2026-01-12 |
true uses the API defaults (tool-result clearing at 100,000 input tokens keeping 3 tool uses; thinking kept per the model’s default). The options fail with InvalidRequestError on an invalid count or when body.context_management is also set. Field names, defaults and headers follow Anthropic’s “Context editing” and “Compaction at a token threshold” pages (read 2026-10-17).
With server compaction, the response starts with a compaction block. The adapter keeps it in the turn’s blocks, the agent replays it, and the API ignores everything before it on the next request, so keep the turn’s blocks when you store the conversation (ai.session() does). The top-level input_tokens / output_tokens leave the compaction step out; the adapter adds the compaction entries of usage.iterations to usage, so cost and budgets see what was billed. pause_after_compaction is reachable through body, but the agent treats a paused response as a final stop, so leave it off inside an agent.
Preserved thinking and client-side compaction
Section titled “Preserved thinking and client-side compaction”On Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 a replayed thinking block is valid only while the system prompt, the tools and every message before it are unchanged (“preserved thinking”). Server-side compaction and context editing don’t count as edits. A summary the agent writes on the client does: the turns it keeps after the summary still carry thinking produced against the replaced history.
The agent’s automatic compaction marks those kept turns compaction: "retained". On the models above the adapter replays them without their thinking / redacted_thinking blocks (text and tool calls stay), which is the no-beta path Anthropic documents for compaction that keeps recent turns. Other models (Opus 5, Sonnet 5, Fable 5, Mythos 5.1, the 4.x family) don’t run the check, so the kept blocks are replayed as they are. Override the inference per model with preservedThinking:
anthropic.model({ name: "my-gateway/opus", preservedThinking: true });To let the API decide instead, set thinkingBlockBinding. The adapter sends thinking.block_binding.prefix_mismatch_behavior with the thinking-binding-controls-2026-08-01 beta and replays every block; "drop_block" drops the invalid ones (listed in the response’s input_transformations), "error" fails with a 400. It is also how you recover a stored conversation that was edited by hand. On a preserved-thinking model that runs adaptive thinking by default, the adapter sends thinking: { type: "adaptive" } to carry it; with between_tools the option is skipped (debug log), since that shape takes no other field.
const recovering = ai.agent({ model: anthropic.model({ name: "claude-opus-5-5" }), modelOptions: { providerOptions: { anthropic: { thinkingBlockBinding: "drop_block" } } },});
await recovering.execute(question, { history });Finish reasons
Section titled “Finish reasons”Claude stop_reason | finishReason |
|---|---|
end_turn, stop_sequence | stop |
tool_use | tool_calls |
max_tokens | length |
model_context_window_exceeded | context_exceeded |
pause_turn | pause_turn (the agent resumes it automatically) |
refusal | refusal |
Vision
Section titled “Vision”Pass image parts on a user message — the agent resolves attachments
before they reach the adapter, which forwards them as Claude image
blocks.
const model = anthropic.model({ name: "claude-sonnet-4-6" });
const response = await model.complete([ { role: "user", content: [ { type: "text", text: "What's in this photo?" }, { type: "image", source: { url: "https://example.com/cat.jpg" } }, ], },]);Pricing and usage
Section titled “Pricing and usage”Every response reports token usage, on model.complete() and the
streaming done chunk as well as on agent reports. Anthropic’s
input_tokens is the uncached remainder only: cache reads and writes
are counted on top of it. The adapter therefore marks the usage
inputIncludesCache: false, and cost bills every channel exactly once.
| Field | Anthropic source | Billed at |
|---|---|---|
input | input_tokens | input |
cachedTokens | cache_read_input_tokens | cachedInput |
cacheWriteTokens | cache_creation_input_tokens | cacheWrite (5-minute write, 1.25× input) |
cacheWrite1hTokens | cache_creation.ephemeral_1h_input_tokens | cacheWrite1h (1-hour write, 2× input) |
reasoningTokens | output_tokens_details.thinking_tokens | inside output |
cost | computed when the model is priced | sum it with totalCost(usage.cost) |
When Anthropic omits the per-lifetime breakdown, all writes count as
1-hour writes only if every cache marker in the request asked for
ttl: "1h".
Built-in list prices. Every known Claude id is priced out of the box
from ANTHROPIC_MODEL_PRICING — the “Model pricing” table of
Anthropic’s pricing page,
fetched 2026-10-17 (standard global routing). Dated snapshot ids such as
claude-haiku-4-5-20251001 resolve to their alias. Resolution:
per-model pricing > SDK pricing registry > built-in table >
unpriced (cost stays undefined).
import { totalCost } from "@warlock.js/ai";import { AnthropicSDK, anthropicPricingFor } from "@warlock.js/ai-anthropic";
const anthropic = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY ?? "", // Contract rates win over the built-in table. pricing: { "claude-sonnet-4-6": { input: 2.4, output: 12, cachedInput: 0.24 } },});
const model = anthropic.model({ name: "claude-sonnet-4-6" });const { usage } = await model.complete([{ role: "user", content: "hi" }]);
console.log(totalCost(usage.cost)); // USD for this one callconsole.log(anthropicPricingFor("claude-opus-5-5")); // { input: 4, output: 20, cachedInput: 0.2, cacheWrite: 5, cacheWrite1h: 8 }Any entry that leaves out the write rates — your own { input, output }
included — gets them from Anthropic’s multipliers (1.25× input for a
5-minute write, 2× for a 1-hour write; withAnthropicCacheWriteRates).
The cache-read rate differs per model, so set cachedInput yourself or
reads bill at the full input rate.
Not built in, so pass pricing for them: Claude Haiku 5.5 (priced by
prompt length, with higher rates above 100,000 prompt tokens), US-only
inference (inference_geo), fast mode, the Batch API, and Claude on
Bedrock or Vertex AI.
An overloaded API (HTTP 529 or a mid-stream overloaded_error) surfaces
as ProviderOverloadedError (code: "PROVIDER_OVERLOADED",
retryable: true). Every provider error carries status, retryable
and billed (false for 401 / 402 / 403, otherwise "unknown": Anthropic
publishes no rule for failed requests).
Need an offline token estimate before a call? anthropic.count(text)
returns a fast character-heuristic approximation — good for budgeting
and quota guards, not for billing. For an exact count, call
model.countTokens(messages, options): it asks Anthropic’s
messages.countTokens endpoint, with the same system prompt, tools and
thinking settings the real call would send.
For budget preflight, countInputTokens(model, messages, { tools }) from
@warlock.js/ai returns { tokens, source }. It estimates only when native
counting is absent or explicitly throws InvalidRequestError with
reason: "count-unsupported"; this adapter preserves that signal. System and
tools are supported natively. Ordinary validation, auth, network and rate-limit
errors propagate. See the cost/budget guide for reserve mode.
For the agent/workflow surface these models plug into, see
@warlock.js/ai. For provider-specific notes and the
latest model ids, see the warlock-js-ai-anthropic skill.
Build and send an audited request
Section titled “Build and send an audited request”model.buildRequest(messages, options?) is synchronous and makes no network call.
It returns a detached { endpoint, body, betas } snapshot, including thinking,
cache markers, provider body options and merged tool results. body.stream is
false by default; pass { ...options, stream: true } to build a streaming request.
betas identifies the beta header flags; the SDK receives these separately from
its serialized Messages body.
const request = model.buildRequest(messages, options);const bodyJSON = JSON.stringify(request.body);// Hash or reserve spend against bodyJSON before sending.const response = await model.send(request, { signal });
const streamingRequest = model.buildRequest(messages, { ...options, stream: true });for await (const chunk of model.send(streamingRequest, { signal })) { // Consume the same neutral chunks as model.stream().}send() uses the built endpoint, body and beta flags without rebuilding the
conversation or applying new defaults. It passes betas only when endpoint is
"beta.messages": a request whose endpoint is "messages" is sent without
them, even if betas was edited by hand. Transport cancellation stays separate
from the audited body. complete(), stream() and countTokens() use this same
build path and always select their respective request modes. buildRequest()
throws build failures raw (an InvalidRequestError for a reserved body key, a
DataCloneError for a body value structuredClone cannot copy, such as a
function); complete(), stream() and countTokens() log them like send
failures and reject with an AIError, wrapping anything else in an
InvalidRequestError.
Treat the snapshot as read-only after hashing it; SDK JSON serialization and beta
header handling still apply.
Hosts with their own transport can import toAnthropicMessages,
toAnthropicTools, toProviderBlocks, fromProviderBlocks,
ANTHROPIC_BLOCK_FORMAT and the AnthropicMessages type from the package root.
fromProviderBlocks(blocks, toolCalls) retains the adapter’s assistant-block
allowlist and tool-id validation.
Built-in tools and streamed thinking
Section titled “Built-in tools and streamed thinking”Anthropic’s own tools — web_search, web_fetch, code_execution (server-run) and bash, text_editor, memory (run by your code) — go in tools through ai.providerTool({ provider: "anthropic", definition }). The definition is sent verbatim, before your function tools; the prompt-cache breakpoint stays on the last function tool. Server tool results come back as provider blocks and are replayed verbatim on the next call. See Provider-defined tools.
const webSearch = ai.providerTool({ provider: "anthropic", definition: { type: "web_search_20250305", name: "web_search", max_uses: 5 },});With streamChunks (on by default under agent.stream()), stream() also yields reasoning for each thinking_delta, tool-args for each input_json_delta of a client tool_use block, and usage after message_start and every message_delta. On Claude 5-family models thinking text streams only with thinkingDisplay: "summarized". See Stream reasoning and tool arguments.
Hosted-search usage and fees
Section titled “Hosted-search usage and fees”complete() and streaming done map the provider’s
usage.server_tool_use.web_search_requests / web_fetch_requests to
usage.serverToolUse.webSearchRequests / webFetchRequests. Streaming
message_delta counts are cumulative snapshots, never added twice. Raw
server_tool_use and search-result blocks remain in response.blocks and are
replayed in history; they never invoke local function handlers.
To add request fees to token costs, explicitly compose the verified
ANTHROPIC_SERVER_TOOL_PRICING export into model pricing.serverTools:
import { ANTHROPIC_SERVER_TOOL_PRICING } from "@warlock.js/ai-anthropic";const model = anthropic.model({ name: "claude-sonnet-4-6", pricing: { input: 3, output: 15, serverTools: ANTHROPIC_SERVER_TOOL_PRICING },});The search fee is $0.01 per search; fetch has no additional request fee (verified 2026-10-18). Tokens remain additional. No code-execution request count is inferred from blocks. See hosted-tool costs and fee reservations.