The Anthropic adapter calls Claude for chat. It supports thinking, automatic prompt caching, and tools that Anthropic runs.
npm i @tanstack/ai-anthropicpnpm add @tanstack/ai-anthropicyarn add @tanstack/ai-anthropicbun add @tanstack/ai-anthropicPut the key in the environment of your server:
ANTHROPIC_API_KEY=sk-ant-...anthropicText, anthropicSummarize, and anthropicFiles use the first of these variables that is set:
If none of them is set, these factories throw. To pass the credential in code, use createAnthropicChat. The create* factories never read a credential from the environment:
import { createAnthropicChat } from "@tanstack/ai-anthropic";
const adapter = createAnthropicChat("claude-sonnet-5-5", process.env.ANTHROPIC_API_KEY!);Got a Bearer or OAuth token? See Bearer and OAuth tokens.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: anthropicText("claude-sonnet-5-5"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}The client is the same for every provider. Point useChat at your route:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
sendMessage(input);
setInput("");
}}
>
{messages.map((message) => (
<p key={message.id}>
{message.parts
.map((part) => (part.type === "text" ? part.content : ""))
.join("")}
</p>
))}
<input value={input} onChange={(event) => setInput(event.target.value)} />
</form>
);
}For Vue, Solid, Svelte, Angular, and the other frameworks, see Quick Start.
You now have a streaming Claude chat. Tools and Structured Outputs work the same as with every adapter.
import { summarize } from "@tanstack/ai";
import { anthropicSummarize } from "@tanstack/ai-anthropic";
const result = await summarize({
adapter: anthropicSummarize("claude-sonnet-4-6"),
text: "Your long text to summarize...",
threadId: "thread-1",
maxLength: 100,
style: "concise", // "concise" | "bullet-points" | "paragraph"
});
console.log(result.summary);anthropicFiles uploads media to the Anthropic Files API, so a message can point to the file. See Files API.
Anthropic does not make images. For images, use OpenAI or Gemini.
Anthropic runs some tools itself, such as web search and a code sandbox. Import them from @tanstack/ai-anthropic/tools and add them to tools:
import { chat } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
import { webSearchTool } from "@tanstack/ai-anthropic/tools";
const stream = chat({
adapter: anthropicText("claude-sonnet-5-5"),
messages: [{ role: "user", content: "What's new in AI this week?" }],
threadId: "thread-1",
tools: [
webSearchTool({
name: "web_search",
type: "web_search_20250305",
max_uses: 2,
}),
],
});Tools that Anthropic runs:
Tools that your app runs when Claude asks:
Each tool, with a full sample, is on Anthropic: Advanced. For which models take which tool, see Provider Tools.
Provider options go in modelOptions, with the Anthropic names:
import { chat } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
const stream = chat({
adapter: anthropicText("claude-sonnet-5-5"),
messages: [{ role: "user", content: "Hello!" }],
threadId: "thread-1",
modelOptions: {
max_tokens: 4096,
stop_sequences: ["END"],
},
});Want Claude to think harder, or not at all? Set reasoning on chat(). The adapter turns the level into the right thinking fields for the model.
import { chat } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
const stream = chat({
adapter: anthropicText("claude-opus-5-5"),
messages: [{ role: "user", content: "Plan a database migration." }],
threadId: "thread-1",
reasoning: "xhigh",
});What goes to Anthropic:
The levels of each model:
| Model | Levels | budgetTokens |
|---|---|---|
| claude-opus-5-5, claude-sonnet-5-5, claude-fable-5-1, claude-fable-5, claude-opus-5, claude-opus-4-8, claude-opus-4-7 | low to max | no |
| claude-haiku-5-5, claude-sonnet-5 | off, low to max | no |
| claude-opus-4-6, claude-sonnet-4-6 | low, medium, high, max | yes |
| claude-opus-4-5 | low, medium, high | yes |
| claude-haiku-4-5, claude-sonnet-4-5 | off to high | yes |
A budget model with budgetTokens:
import { chat } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
const stream = chat({
adapter: anthropicText("claude-haiku-4-5"),
messages: [{ role: "user", content: "Check this proof." }],
threadId: "thread-1",
reasoning: { level: "high", budgetTokens: 8000 },
});With an outputSchema, every model in ANTHROPIC_MODELS uses native structured output, so thinking stays on. A direct structuredOutput() call, or a model id that is not in that list, forces a tool. Claude cannot think while a tool is forced, so that call sends no thinking. See Reasoning for the levels and the nearest-level rule.
Newest first. The package exports the full list as ANTHROPIC_MODELS.
| Family | Models |
|---|---|
| Claude 5.5 | claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5 |
| Claude 5 | claude-fable-5-1, claude-fable-5, claude-opus-5, claude-sonnet-5 |
| Claude 4 | claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-6, claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 |
adapter.inputModalities lists the input kinds of the selected model. See Check what a model accepts.
Did Claude return an empty answer to a life-science or security question? A safety classifier on the newer models can decline a request. Set fallbacks, and the Claude API sends a declined request to another Claude model in the same call:
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: anthropicText("claude-opus-5-5"),
messages,
threadId: "thread-1",
modelOptions: { fallbacks: "default" },
});
return toServerSentEventsResponse(stream);
}What this does:
Some refusals still stand. For example, a category can have no recommended model, or the fallback model can be busy. Then the run ends with finishReason: "content_filter", not with an error. The rawEvent of that RUN_FINISHED holds the Anthropic stop_details, with category and explanation.
Show the reason in the UI with onChunk:
import { useState } from "react";
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react";
export function Chat() {
const [refusal, setRefusal] = useState<string | null>(null);
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
onChunk: (chunk) => {
if (chunk.type === "RUN_FINISHED" && chunk.finishReason === "content_filter") {
const details = chunk.rawEvent;
const category =
details &&
typeof details === "object" &&
"category" in details &&
typeof details.category === "string"
? details.category
: "unknown";
setRefusal(`Claude declined this request (category: ${category}).`);
}
},
});
return (
<div>
{messages.map((message) => (
<p key={message.id}>
{message.parts.map((part) => (part.type === "text" ? part.content : null))}
</p>
))}
{refusal && <p role="alert">{refusal}</p>}
<button onClick={() => sendMessage("Describe how CRISPR edits a gene.")}>Send</button>
</div>
);
}Limits:
Now a declined request gets an answer from a fallback model. When no model can answer, your UI tells the user why.
These topics are on Anthropic: Advanced:
To call Claude through a GitHub Copilot plan, see GitHub Copilot.