Skip to content
Adapters

Anthropic

The Anthropic adapter calls Claude for chat. It supports thinking, automatic prompt caching, and tools that Anthropic runs.

1. Install

shell
npm i @tanstack/ai-anthropic

2. Set the API key

Put the key in the environment of your server:

shell
ANTHROPIC_API_KEY=sk-ant-...

anthropicText, anthropicSummarize, and anthropicFiles use the first of these variables that is set:

  1. ANTHROPIC_AUTH_TOKEN: sent as a Bearer token. An sk-ant-oat token here turns on OAuth mode.
  2. ANTHROPIC_OAUTH_TOKEN: sent as a Bearer token, in OAuth mode.
  3. ANTHROPIC_API_KEY: sent as x-api-key.

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:

ts
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.

3. Stream a chat from the server

ts
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);
}

4. Render the chat

The client is the same for every provider. Point useChat at your route:

tsx
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.

Summarization

ts
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);

Files

anthropicFiles uploads media to the Anthropic Files API, so a message can point to the file. See Files API.

No image generation

Anthropic does not make images. For images, use OpenAI or Gemini.

Provider tools

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:

ts
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:

  • webSearchTool: search the web, with citations.
  • webFetchTool: read one URL.
  • codeExecutionTool: run code in a sandbox, also with hosted skills.
  • memoryTool: keep facts across turns.

Tools that your app runs when Claude asks:

  • bashTool: run shell commands.
  • textEditorTool: view and change files.
  • computerUseTool: control a virtual desktop.
  • customTool: a tool with an inline schema.

Each tool, with a full sample, is on Anthropic: Advanced. For which models take which tool, see Provider Tools.

Models and options

Model options

Provider options go in modelOptions, with the Anthropic names:

ts
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"],
  },
});
  • The sampling options temperature, top_p, and top_k also go in modelOptions. Claude Sonnet 5, Claude Opus 4.7 and later, and the 5-generation models reject them. The types take them only on the models that accept them, for example claude-sonnet-4-6.
  • When you do not set max_tokens, the adapter sends the full output limit of the model. See The max_tokens default.
  • Sampling options do not go at the root of chat(). See Moving Sampling Options into modelOptions.

Reasoning

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.

ts
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:

  • off sends thinking: { type: "disabled" } on the models with off in the table below. On a model without off, such as claude-fable-5, the types reject it. At run time, it moves to the lowest level that the model has.
  • Claude 4.6 and later get adaptive thinking and output_config.effort. With an outputSchema, the adapter adds output_config.format next to the effort.
  • Budget models get thinking: { type: "enabled", budget_tokens }. The level picks the budget, or you set it with budgetTokens. If max_tokens is below the budget, the adapter raises it.
  • summary: false sends display: "omitted", so the thinking text does not stream.

The levels of each model:

ModelLevelsbudgetTokens
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-7low to maxno
claude-haiku-5-5, claude-sonnet-5off, low to maxno
claude-opus-5-fastoff, low to maxyes
claude-opus-4-6, claude-sonnet-4-6low, medium, high, maxyes
claude-opus-4-5low, medium, highyes
claude-haiku-4-5, claude-sonnet-4-5, claude-opus-4-1off to highyes

A budget model with budgetTokens:

ts
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 },
});

Structured output forces a tool, and Claude cannot think while a tool is forced. So the structured output call sends no thinking. See Reasoning for the levels and the nearest-level rule.

Models

Newest first. The package exports the full list as ANTHROPIC_MODELS.

FamilyModels
Claude 5.5claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5
Claude 5claude-fable-5-1, claude-fable-5, claude-opus-5, claude-opus-5-fast, claude-sonnet-5
Claude 4claude-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, claude-opus-4-1

adapter.inputModalities lists the input kinds of the selected model. See Check what a model accepts.

Refusal fallback

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:

ts
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:

  • "default" uses the model that Anthropic recommends for the refusal category. A list, such as [{ model: "claude-opus-4-8" }], names up to three models to try in order.
  • The adapter adds the server-side-fallback-2026-07-01 beta header for you.
  • The fallback answer streams like any other answer. When text has already started, events before the fallback block name the model that declined. That includes RUN_STARTED and that first text segment. Events after the block name to.model.
  • When the classifier declines before any output, message_start already names the fallback model. RUN_STARTED uses that name too.
  • The types accept fallbacks on claude-fable-5, claude-fable-5-1, claude-opus-5, claude-opus-5-5, and claude-sonnet-5-5.

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:

tsx
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:

  • Fallback works only on the Claude API. Claude on Vertex does not have it.
  • claude-haiku-5-5 has no fallback model, so its refusals always stand.
  • The adapter does not send the fallback content block back on the next turn. A refusal before any output is not affected.

Now a declined request gets an answer from a fallback model. When no model can answer, your UI tells the user why.

Advanced

These topics are on Anthropic: Advanced:

  • Bearer and OAuth tokens, gateways, and your own Anthropic client.
  • Claude on Vertex AI.
  • The max_tokens default and prompt caching.
  • Every provider tool, with a full sample.
  • The API reference.

To call Claude through a GitHub Copilot plan, see GitHub Copilot.