Build

Anthropic

anthropic() — Claude provider via @anthropic-ai/sdk. Lazy-loaded peer-dep; install when you use it.

The Anthropic SDK is the production path for Claude. agentfootprint's anthropic() factory wraps it as an LLMProvider — same interface as mock(), so the rest of your agent code is identical between dev and prod.

Install

npm install @anthropic-ai/sdk

The SDK is a peer dependency declared in peerDependenciesMeta with optional: true — npm doesn't auto-install it. Lazy-required at first call; friendly install hint if missing.

anthropic() is imported from the agentfootprint/providers subpath, NOT the main agentfootprint barrel. Every provider lives there — mock, the browser variants and createProvider included — so bundlers walking the main entry never touch the optional peer-dep requires. (The one provider helper on the main barrel is providerFromEnv.)

Use

import { Agent } from 'agentfootprint';
import { anthropic } from 'agentfootprint/providers';

const provider = anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  // defaultModel: 'claude-sonnet-4-5-20250929',  // optional, used when Agent doesn't override
});

const agent = Agent.create({
  provider,
  model: 'claude-sonnet-4-5-20250929',
}).build();

Model ids are Anthropic's own: dateless from the 4.6 generation on (claude-opus-5-5), dated before it (claude-sonnet-4-5-20250929, or its alias claude-sonnet-4-5).

Tools (native function calling)

Anthropic's Messages API has native tool_use blocks. The provider translates the agent's Tool[] into the API's tool format and round-trips assistant tool_calls via LLMMessage.toolCalls. ReAct correctness preserved across multi-iteration runs.

One tool per reply — parallelToolCalls: false

By default Claude may ask for several tools at once: one assistant message carries many tool_use blocks, and the agent runs them all inside a single loop iteration. That is usually what you want — it is faster and cheaper.

It is not what you want when the shape of the loop is part of what you are measuring. Per-iteration analysis reads one tool source per iteration: localizeContextBug seeds one 'tool' suspect from that iteration's lastToolResult, and removableSources de-duplicates by tool name. So a reply that batched three tools is attributed to the last tool of the batch — the other two never appear as their own influence rows, and you cannot ablate them individually.

Set parallelToolCalls: false to cap the model at one tool per reply:

const provider = anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  parallelToolCalls: false, // one tool per reply → one tool per iteration
});

On the wire this sends tool_choice: { type: 'auto', disable_parallel_tool_use: true }. auto is deliberate: the model still decides which tool to call, and whether to call one at all — only the count is capped. Nothing is sent on requests that carry no tools (Anthropic rejects tool_choice there), and parallelToolCalls: true sends nothing either, because allowing batches is already Anthropic's default.

Asking for this in the system prompt ("call one tool at a time") is not equivalent — that is a request the model may ignore, this is a request parameter the API enforces. Cost: one extra round trip per tool.

The same option exists on browserAnthropic(), and both adapters put the identical field on the wire.

Thinking

.thinking({ budget }) asks the model to think before it answers. Claude models disagree about how that is asked — a budget sent to Claude 4.7 or later is an HTTP 400 — so the provider sends the shape the model takes:

ModelOn the wire
Opus 4.7 and later, Sonnet 5 and 5.5, Haiku 5.5, Fable, Mythos 5 and 5.1, Mythos Preview — and any id not listed herethinking: { type: 'adaptive', display: 'summarized' }, no budget
Opus 4.6 and Sonnet 4.6 (they take both; budget deprecated, still accepted); Opus 4.5, Sonnet 4.5, Haiku 4.5, Opus 4.1, Opus 4, Sonnet 4, Sonnet 3.7thinking: { type: 'enabled', budget_tokens: budget }
Claude 3.5 Sonnet, 3.5 Haiku, 3 Opus, 3 Sonnet, 3 Haikurefused at build() — they cannot think
const provider = anthropic({  _client: recordingClient(bodies), // ← drop this line to talk to the real API});provider.thinkingMode?.('claude-opus-5-5'); // 'adaptive'provider.thinkingMode?.('claude-haiku-4-5'); // 'budget'for (const model of ['claude-opus-5-5', 'claude-haiku-4-5']) {  const agent = Agent.create({ provider, model })    .system('Think it through, then answer in one line.')    .thinking({ budget: 4000 })    .build();  await agent.run({ message: input });}// Opus 5.5:  thinking: { type: 'adaptive', display: 'summarized' } — no budget sent// Haiku 4.5: thinking: { type: 'enabled', budget_tokens: 4000 }

Adaptive thinking takes no budget. The model decides whether and how much to think, so the budget is not sent (dev mode says so once per model) and no effort level is guessed from it. It still keeps max_tokens above itself — budget + 1024 when the request's is lower — because thinking counts toward max_tokens. display: 'summarized' is what makes the thinking readable; these models otherwise return it empty.

Refused before anything is sent, as an UnsupportedThinkingError (from agentfootprint/providers; provider, model, reason, retryable: false):

  • 'no-thinking' — at build() for the agent's model and its skill and escalation brains; on the first call for a model .configure() picks at run time.
  • 'budget' — a budget model takes a whole number of at least 1024.
  • 'temperature' — thinking takes only the default temperature, 1.
  • 'forced-tool-choice' — .outputSchema(…, { strategy: 'tool-forced' }) cannot ride with a budget; use 'instruct' (or, on Opus 4.6 or Sonnet 4.6, declare them adaptive — below).

withRetry does not retry the refusal, withFallback does not move it to the other side, and withCircuitBreaker does not count it: the request is wrong, not the vendor.

let refusal: UnsupportedThinkingError | undefined;try {  Agent.create({ provider, model: 'claude-3-haiku-20240307' })    .system('Think it through.')    .thinking({ budget: 4000 })    .build();} catch (err) {  if (!(err instanceof UnsupportedThinkingError)) throw err;  refusal = err; // at build, by name: err.model, err.reason === 'no-thinking'}

browserAnthropic() and invokeModelGateway() read the same table, and platform ids (us.anthropic.claude-sonnet-4-5-20250929-v1:0, claude-opus-4-5@20251101) count from claude- on. Ask what will be sent with provider.thinkingMode(model), a ThinkingMode: 'adaptive', 'budget' or 'none'. Wrappers forward it; withFallback sends each side its own model's shape and answers the least of the two, so a fallback that cannot think is refused at build. Source: Anthropic's thinking support by model.

An id the table cannot read — a gateway alias, an application-inference-profile ARN, a deployment name — is read as adaptive. If it serves a model that needs a budget (Claude 4.5 and earlier), say so with the adapter's thinkingMode option; return undefined for the table's answer. The same option makes Opus 4.6 adaptive, which in budget mode does not think between tool calls. The provider declares the result and sends it, so the build check and the wire agree:

invokeModelGateway({ /* … */ model: 'haiku-fast', thinkingMode: (id) => (id === 'haiku-fast' ? 'budget' : undefined) });

Forced tool choice is model-specific

carriesForcedToolChoice still says whether the adapter writes a forced choice on its wire. forcedToolChoiceFor(provider, model) also reads the optional supportsForcedToolChoice(model) restriction.

Claude Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 reject forced tool choice, with or without a thinking request. Haiku 5.5 accepts it. The SDK, browser and InvokeModel gateway adapters use one model table and refuse an unsupported choice before sending anything, with UnsupportedToolChoiceError from agentfootprint/providers (provider, model, retryable: false). This follows Anthropic's forced-tool compatibility rules.

assert.equal(forcedToolChoiceFor(provider, 'claude-opus-5-5'), false);assert.equal(forcedToolChoiceFor(provider, 'claude-haiku-5-5'), true);const forcedAgent = Agent.create({ provider, model: 'claude-opus-5-5' })  .outputSchema(    { parse: (value: unknown) => value },    { strategy: 'tool-forced', jsonSchema: { type: 'object', properties: {} } },  )  .build();await assert.rejects(forcedAgent.run({ message: input }), UnsupportedToolChoiceError);// Use strategy: 'instruct' explicitly if prompt-based output is acceptable.

An explicit .outputSchema(…, { strategy: 'tool-forced' }) refuses at run start when the provider/model does not support it. Choose 'instruct' explicitly if prompt-based output fits your use case. The internal constrained classifier already has a strict text-parsing path and uses that path for these models; it does not weaken its allowed-value check.

Wrappers preserve the declaration; a fallback pair requires support on both sides. The default resilience policies do not retry this refusal, switch providers or count it as a circuit failure. Custom policies remain yours. Unknown deployment aliases keep their previous behavior: the table does not claim to know the model hidden behind them, and the service may still refuse. The thinking-mode option controls only thinking, not forced-tool support.

Thinking replay: stop only when unsafe

During a tool turn, Anthropic requires the preceding assistant content, including signed and redacted thinking, in its original order. A tool result does not finish that turn. Earlier completed turns can omit thinking; see the tool-workflow contract.

The SDK, browser and InvokeModel gateway share one replay check at the adapter that actually sends the call. If saved evidence proves that required active-turn thinking was changed, removed or reintroduced, the call raises UnsafeThinkingReplayError from agentfootprint/providers before sending. Keep system instructions, tools and earlier messages stable until the turn finishes; retain the original assistant blocks. Default resilience wrappers do not retry or hide this refusal. Unknown local provenance is not proof that an opaque signature is invalid; server authentication remains the server's job.

For a witnessed completed turn, eligible invalid thinking is removed explicitly. agentfootprint.agent.thinking_omitted records every removed block's indices, reason, provider and model, with phase: 'prepared': it proves preparation, not delivery or acceptance. No reasoning or signature appears in this event. The run persists retirement into its history so saving and resuming cannot silently bring those blocks back. Standalone provider callers can use onReplayPrepared; successful responses also carry replayOmissions.

LLMMessage.replay holds optional JSON layout and consistency evidence, not a copy of the signed content. Preserve it alongside thinkingBlocks when managing history yourself. Complete and streaming replies retain the same block order; a no-thinking end_turn reply also records completion. The receipt stays at the provider-port boundary; servedAt reports a thinking-replay gap when a later adapter omission means it cannot prove the downstream message body.

For a standalone integration, import the replay types from agentfootprint/providers. MessageReplay describes the saved evidence; each ReplayPart identifies a text span, thinking block or tool call in its ordered layout. A layout marked lossy is negative evidence, not a promise of recoverable content. ReplayPreparation is the pre-transport hook payload: the prepared request plus its omissions. Each ThinkingReplayOmission names one removed block without its contents, and ThinkingReplayReason describes the locally observed inconsistency. These types do not authenticate signatures or prove that a prepared request was sent.

Only documented bound model families get the local prefix check. Older models, unknown aliases and cross-model signature compatibility are not guessed. No preserved-thinking beta or server drop policy is enabled. Prefix and contiguous sequence rules follow Anthropic's preserved-thinking documentation.

Streaming

provider.stream(req) uses the SDK's native iterator; chunks land as they arrive. Final chunk carries the full LLMResponse (toolCalls + usage + stopReason) — single round-trip serves UI tokens AND ReAct decisioning.

Production patterns

Wrap with resilience decorators for production:

import { withRetry, withFallback } from 'agentfootprint/resilience';
import { anthropic, openai } from 'agentfootprint/providers';

const provider = withRetry(
  withFallback(
    anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }),
    openai({ apiKey: process.env.OPENAI_API_KEY! }),
  ),
  { maxAttempts: 5 },
);

See Resilience guide.

Browser variant

For browser environments where the Anthropic Node SDK doesn't bundle cleanly, use BrowserAnthropicProvider — fetch-based, zero peer deps. Requires the anthropic-dangerous-direct-browser-access: true header (which the provider sets automatically). Production browser apps should proxy through a backend.

Limitations

  • Multi-modal content (images, video) not supported — LLMMessage.content is string.
  • responseFormat (JSON-Schema-coerced output) not exposed yet — pass schema instructions via systemPrompt.

Next steps

On this page