claude-api
PublicRepository: anthropics/skills
Higher Risk
5 findings
Skill description is 1068 characters; Agent skills limit the `description` field to 1024 characters.
Remediation Shorten the description to 1024 characters or fewer while keeping it specific
Skill name or description contains 'Anthropic', suggesting official affiliation
Remediation Do not impersonate official skills or use unauthorized branding
Skill manifest does not include a 'license' field. Specifying a license helps users understand usage terms.
Remediation Add 'license' field to SKILL.md frontmatter (e.g., MIT, Apache-2.0)
Detects potential exposure of sensitive information like API keys, passwords, tokens, and certificates: curl` / HTTP:** get a short-lived token with `ant auth print-credentials --access-token` and send it
Remediation Review and remove credential harvesting pattern
Detects coercive prompt injections in tool description fields: include internal or system XML tags in your response
Remediation Review and remove prompt injection pattern
Description
Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration. TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens). SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
Details
Skill Files
# Building LLM-Powered Applications with Claude
This skill helps you build LLM-powered applications with Claude. Choose the right surface based on your needs, detect the project language, then read the relevant language-specific documentation.
## Before You Start
Scan the target file (or, if no target file, the prompt and project) for non-Anthropic provider markers - `import openai`, `from openai`, `langchain_openai`, `OpenAI(`, `gpt-4`, `gpt-5`, file names like `agent-openai.py` or `*-generic.py`, or any explicit instruction to keep the code provider-neutral. If you find any, stop and tell the user that this skill produces Claude/Anthropic SDK code; ask whether they want to switch the file to Claude or want a non-Claude implementation. Do not edit a non-Anthropic file with Anthropic SDK calls. (Exception: the `prompt-audit` subcommand is non-interactive and does not stop here - it records non-Anthropic provider markers in its report's stated assumptions and never proposes switching a non-Anthropic file to the Anthropic SDK.)
## Output Requirement
When the user asks you to add, modify, or implement a Claude feature, your code must call Claude through one of:
1. **The official Anthropic SDK** for the project's language (`anthropic`, `@anthropic-ai/sdk`, `com.anthropic.*`, etc.). This is the default whenever a supported SDK exists for the project.
2. **Raw HTTP** (`curl`, `requests`, `fetch`, `httpx`, etc.) - only when the user explicitly asks for cURL/REST/raw HTTP, the project is a shell/cURL project, or the language has no official SDK.
Never mix the two - don't reach for `requests`/`fetch` in a Python or TypeScript project just because it feels lighter. Never fall back to OpenAI-compatible shims.
**Never guess SDK usage.** Function names, class names, namespaces, method signatures, and import paths must come from explicit documentation - either the `{lang}/` files in this skill or the official SDK repositories or documentation links listed in `shared/live-sources.md`. If the binding you need is not explicitly documented in the skill files, WebFetch the relevant SDK repo from `shared/live-sources.md` before writing code. Do not infer Ruby/Java/Go/PHP/C# APIs from cURL shapes or from another language's SDK.
**If WebFetch or repository access fails** (network restricted, timeouts, clone blocked): do not keep retrying - write code from the patterns and namespace/package tables in the `{lang}/` file, run the compiler or interpreter on it, and iterate on the error output. For statically-typed SDKs (C#, Java, Go) a compile-fix loop against local errors reaches working code faster than blocked network research.
## Defaults
Unless the user requests otherwise:
For the Claude model version, please use Claude Opus 5, which you can access via the exact model string `claude-opus-5`. Please default to using adaptive thinking (`thinking: {type: "adaptive"}`) for anything remotely complicated. And finally, please default to streaming for any request that may involve long input, long output, or high `max_tokens` - it prevents hitting request timeouts. Use the SDK's `.get_final_message()` / `.finalMessage()` helper to get the complete response if you don't need to handle individual stream events
## Warning: API Drift - Your Training Prior May Be Stale
Several common Claude API shapes changed in 2025-2026. If you recall a pattern from training, verify it against the `{lang}/` files in this skill before writing - the rows below are the most frequent drift points:
| Area | Stale prior | Current API |
|---|---|---|
| Extended thinking | `thinking: {type: "enabled", budget_tokens: N}` | On Claude 4.6+ models: `thinking: {type: "adaptive"}`. `budget_tokens` is deprecated on Opus 4.6 / Sonnet 4.6 and **rejected with a 400** on Fable 5/5.1 / Sonnet 5 / Opus 5 / 4.8 / 4.7. Pre-4.6 models still use `budget_tokens`. |
| Web search / web fetch tool type | `web_search_20250305`, `web_fetch_20250910` | `web_search_20260209`, `web_fetch_20260209` (dynamic filtering) on Opus 5/4.8/4.7/4.6, Sonnet 5, and Sonnet 4.6. Older models keep the basic variants; on Vertex AI only basic `web_search_20250305` is available (web fetch is not on Vertex) - see the Server Tools QR below. |
| PHP parameter names | snake_case wire names as named args (`max_tokens`) | Top-level named args are camelCase (`maxTokens`). Nested array keys vary by feature (e.g. `'taskBudget'`, `'skillID'`, `'mcp_server_name'`) - copy the exact key from the documented example; do not bulk-convert. |
| Managed Agents credentials | Keep secrets host-side via custom tools (the only option before vaults shipped) | Vault `environment_variable` credentials - stored by Anthropic, substituted at egress, never visible in the sandbox (`shared/managed-agents-tools.md` -> Vaults). Host-side custom tools remain the fallback for self-hosted sandboxes. |
| Files API / Skills | `client.beta.files.*` / `client.beta.skills.*` with beta `files-api-2025-04-14` / `skills-2025-10-02` | Out of beta: `client.files.*` / `client.skills.*`, no beta header. In current SDKs `client.beta.files` / `client.beta.skills` have breaking shape changes from previous versions, matching the stable namespaces - migrate per `shared/live-sources.md` -> Files API / Skills Guide. |
The `{lang}/` files in this skill are authoritative over recalled patterns.
---
## Subcommands
If the User Request at the bottom of this prompt is a bare subcommand string (no prose), search every **Subcommands** table in this document - including any in sections appended below - and follow the matching Action column directly. This lets users invoke specific flows via `/claude-api <subcommand>`. If no table in the document matches, treat the request as normal prose.
| Subcommand | Action |
|---|---|
| `migrate` | Migrate existing Claude API code to a newer model. **Read `shared/model-migration.md` immediately** and follow it in order: Step 0 (confirm scope - ask which files/directories before any edit), Step 1 (classify each file), then the per-target breaking-changes section. Do not summarize the guide - execute it. If the user did not name a target model, ask which model to migrate to in the same turn as the scope question. After the per-target changes are applied, audit the in-scope prompt text, tool descriptions, and request code against `shared/prompt-audit.md` - prompting written for the source model is part of every migration, and it does not announce itself. |
| `prompt-audit` | Audit existing prompts, skills, and tool descriptions for dated patterns ("cruft") written for older models. **Read `shared/prompt-audit.md` immediately** and follow it in order: Step 0 (establish scope and target model from the request and the repository - state the assumptions in the report, do not stop to ask), inventory, provenance, then the pattern scan. Produce both deliverables in full - the audit report (findings with `file:line`, pattern, why it's obsolete for the target model, confidence) and a proposed diff - without pausing for confirmation; apply edits only if the request explicitly asked for them. Do not summarize the guide - execute it. |
| `upgrade` | Upgrade the project's Anthropic SDK dependency across a major version - currently the Python SDK, `anthropic` 0.x -> 1.x. Trailing words may name the language and/or a scope (`upgrade python`, `upgrade python sdk src/`). **Read `python/claude-api/sdk-upgrade.md` immediately** and follow it in order: Step 0 (confirm scope, then establish the current and target versions - a published 1.x must exist before you write a pin), the Step 1 inventory, each numbered section, then verification and the report. Do not summarize the guide - execute it. If the detected or named language has no `sdk-upgrade.md` in this skill, say that no major-version upgrade guide is bundled for that SDK yet and point the user at that SDK's CHANGELOG (repositories in `shared/live-sources.md`); do not improvise one from the Python guide. This is not model migration - to move code to a newer Claude model, use `migrate`. |
| `cost-optimize` | Reduce what existing Claude API code costs to run, without sacrificing output quality. **Read `shared/cost-optimization.md` immediately** and follow it in order: Step 0 (establish scope, quality bar, and baseline), the token profile - measured through the Usage and Cost Admin API when the user has an Admin API key, from the app's own `response.usage` logs when it has those (ask), or estimated from the code otherwise - then a savings-ranked shortlist of levers (quoted in dollars, % of bill, or relative buckets depending on which of those data sources you have), free wins (caching, input-token hygiene, loop hygiene, output-token hygiene, batch) before tradeoffs (budgets, effort, model choice, multi-model); any lever that earns a place becomes its own diff - proposed by default, applied and measured against the eval covering the traffic it touches when the user asks and approves - and "no changes recommended" is a valid outcome. Two standing rules: every run that exercises the model spends real money, so get the user's approval first; and when context for a lever is missing, work through it interactively with the user - this workflow is not expected to one-shot the audit. Do not summarize the guide - execute it; presenting the profile and the ranked plan to the user is part of executing it. |
---
## Language Detection
Before reading code examples, determine which language the user is working in (exception: for the `prompt-audit` subcommand, skip this section's ask steps - the audit is non-interactive and its inventory is language-agnostic; when no language is inferable, proceed without asking and state the assumption in the report):
1. **Look at project files** to infer the language:
- `*.py`, `requirements.txt`, `pyproject.toml`, `setup.py`, `Pipfile` -> **Python** - read from `python/`
- `*.ts`, `*.tsx`, `package.json`, `tsconfig.json` -> **TypeScript** - read from `typescript/`
- `*.js`, `*.jsx` (no `.ts` files present) -> **TypeScript** - JS uses the same SDK, read from `typescript/`
- `*.java`, `pom.xml`, `build.gradle` -> **Java** - read from `java/`
- `*.kt`, `*.kts`, `build.gradle.kts` -> **Java** - Kotlin uses the Java SDK, read from `java/`
- `*.scala`, `build.sbt` -> **Java** - Scala uses the Java SDK, read from `java/`
- `*.go`, `go.mod` -> **Go** - read from `go/`
- `*.rb`, `Gemfile` -> **Ruby** - read from `ruby/`
- `*.cs`, `*.csproj` -> **C#** - read from `csharp/`
- `*.php`, `composer.json` -> **PHP** - read from `php/`
2. **If multiple languages detected** (e.g., both Python and TypeScript files):
- Check which language the user's current file or question relates to
- If still ambiguous, ask: "I detected both Python and TypeScript files. Which language are you using for the Claude API integration?"
3. **If language can't be inferred** (empty project, no source files, or unsupported language):
- Use AskUserQuestion with options: Python, TypeScript, Java, Go, Ruby, cURL/raw HTTP, C#, PHP
- If AskUserQuestion is unavailable, default to Python examples and note: "Showing Python examples. Let me know if you need a different language."
4. **If unsupported language detected** (Rust, Swift, C++, Elixir, etc.):
- Suggest cURL/raw HTTP examples from `curl/` and note that community SDKs may exist
- Offer to show Python or TypeScript examples as reference implementations
5. **If user needs cURL/raw HTTP examples**, read from `curl/`.
### Language-Specific Feature Support
Every SDK language above supports both the beta Tool Runner and Managed Agents (beta) - Python (`@beta_tool` decorator), TypeScript (`betaZodTool` + Zod), Java (annotated classes), Go (`BetaToolRunner` in the `toolrunner` pkg), Ruby (`BaseTool` + `tool_runner`), C# (`BetaToolRunner` + raw JSON schema), PHP (`BetaRunnableTool` + `toolRunner()`); code entry points are in the Tool Use Patterns quick reference below. cURL is raw HTTP (no SDK features) and supports Managed Agents.
> **Managed Agents code examples**: see the reading guide in the `## Managed Agents (Beta)` section below.
---
## Which Surface Should I Use?
> **Start simple.** Default to the simplest tier that meets your needs. Single API calls and workflows handle most use cases - only reach for agents when the task genuinely requires open-ended, model-driven exploration. "Simplest" means the least code you own: for a hosted, scheduled, or memory-backed agent, Managed Agents is usually the simplest option (no loop code, no state files, no scheduler), even though it's a bigger platform.
| Use Case | Tier | Recommended Surface | Why |
| ----------------------------------------------- | --------------- | ------------------------- | ------------------------------------------------------------ |
| Classification, summarization, extraction, Q&A | Single LLM call | **Claude API** | One request, one response |
| Batch processing or embeddings | Single LLM call | **Claude API** | Specialized endpoints |
| Multi-step pipelines with code-controlled logic | Workflow | **Claude API + tool use** | You orchestrate the loop |
| Custom agent with your own tools | Agent | **Claude API + tool use** | Maximum flexibility |
| Server-managed stateful agent with workspace | Agent | **Managed Agents** | Anthropic runs the loop and hosts the tool-execution sandbox |
| Persisted, versioned agent configs | Agent | **Managed Agents** | Agents are stored objects; sessions pin to a version |
| Long-running multi-turn agent with file mounts | Agent | **Managed Agents** | Per-session containers, SSE event stream, Skills + MCP |
| Agent that runs on a schedule (cron, "every night") | Agent | **Managed Agents** - scheduled deployments | Deployments fire sessions autonomously; no client-side scheduler |
> **Note:** Managed Agents is the right choice when you want Anthropic to run the agent loop *and* host the container where tools execute - file ops, bash, code execution all run in the per-session workspace. If you want to host the compute yourself or run your own custom tool runtime, Claude API + tool use is the right choice - use the tool runner for the agentic loop - its per-turn hooks still give you approval gates, logging, error interception, and conditional execution (see `shared/tool-use-concepts.md`) - or the manual loop when you want to own the entire loop yourself.
> **Cloud-provider access.** **Claude Platform on AWS** is Anthropic-operated with same-day API parity - see `shared/claude-platform-on-aws.md` for client setup. For per-feature availability on **Claude Platform on AWS**, **Amazon Bedrock**, **Google Vertex AI**, and **Microsoft Foundry**, see `shared/platform-availability.md` - that table is the single source of truth in this skill; do not infer availability from anywhere else.
### Building an Agent: Four Approaches
Once you've decided you actually need an agent (open-ended, model-driven tool use), there are four distinct ways to build one. Two independent questions separate them: **who supplies the harness** (the agent loop + context management) and **who supplies the deployment** (the infra the agent runs on). The Tool Runner and the Claude Agent SDK both supply a *harness only* - you still host and deploy them yourself - which is why they're easy to conflate. Managed Agents (CMA) is the only option that supplies **both** the harness *and* managed deployment; the manual loop supplies neither.
| # | Approach | You write | Harness & deployment | Tools available | Use when |
|---|----------|-----------|----------------------|-----------------|----------|
| 1 | **Claude API - manual loop** | The `while stop_reason == "tool_use"` loop yourself | You build the harness; you host | Only tools you define | You want to own the *entire* loop - no beta dependency, or a control flow the Tool Runner's per-turn hooks don't fit |
| 2 | **Claude API - Tool Runner** (`client.beta.messages.tool_runner` + `@beta_tool` / `betaZodTool`) | Just the tool functions | SDK supplies the loop (**harness only**); you host | Only tools you define | A custom-tool agent without hand-writing the loop (most cases). Per-turn hooks still give you approval gates, error interception, result modification (e.g. `cache_control`), retries, streaming, and compaction |
| 3 | **Managed Agents** (REST, beta) | Agent config + your tool results | Anthropic supplies the harness **and** hosts a per-session sandbox (**harness + deployment**) | Anthropic-hosted sandbox (bash, files, code exec) + Skills/MCP + your tools | You want Anthropic to run the loop *and* host the per-session workspace; persisted/versioned configs; long-running sessions |
| 4 | **Claude Agent SDK** - *separate product* (`claude-agent-sdk` / `@anthropic-ai/claude-agent-sdk`) | A prompt + options | SDK supplies the Claude Code harness + built-in tools (**harness only**); you host | Built-in Read/Write/Edit/Bash/Glob/Grep/WebSearch/WebFetch + MCP + subagents | You want a batteries-included coding/filesystem agent running on your own infra |
The harness/deployment split is the key mental model: options 1, 2, and 4 all **leave deployment to you**; only option 3 (CMA) adds managed deployment. Options 1-3 are what this skill generates; option 4 is a different library with its own docs - see the disambiguation below.
> **Tool Runner != Claude Agent SDK.** These sound alike but are different packages:
> - **Tool Runner** is part of the regular Anthropic API SDK (`anthropic` / `@anthropic-ai/sdk`), reached via `client.beta.messages.tool_runner`. It automates the request -> execute -> loop cycle *for tools you define*. No built-in tools, no filesystem access, no sandbox - you supply every tool and host the compute. It is option 2 above, a thin helper over `POST /v1/messages`.
> - **Claude Agent SDK** (`claude-agent-sdk` / `@anthropic-ai/claude-agent-sdk`) is Claude Code packaged as a library. It ships built-in tools (file read/write/edit, bash, grep, web search), the full agent loop, context management, hooks, subagents, permissions, and sessions. You call `query(prompt, options)` and it drives everything.
>
> Both are **harness-only - you host and deploy them.** The difference is scope of harness: the Tool Runner loops over tools *you* define (with per-turn hooks for approval, interception, result modification, and retries - but no built-in tools); the Agent SDK is the full Claude Code harness with built-in tools. Neither provides managed deployment - that's what **Managed Agents (CMA)** adds (Anthropic hosts the loop and a per-session sandbox).
>
> **This skill covers the Claude API and Managed Agents (options 1-3); it does not generate Claude Agent SDK code.** If the user actually wants the Claude Agent SDK, point them to its docs (`code.claude.com/docs/en/agent-sdk`) - don't substitute the API Tool Runner for it, or vice-versa.
### Should I Build an Agent?
Before choosing the agent tier, check all four criteria:
- **Complexity** - Is the task multi-step and hard to fully specify in advance? (e.g., "turn this design doc into a PR" vs. "extract the title from this PDF")
- **Value** - Does the outcome justify higher cost and latency?
- **Viability** - Is Claude capable at this task type?
- **Cost of error** - Can errors be caught and recovered from? (tests, review, rollback)
If the answer is "no" to any of these, stay at a simpler tier (single call or workflow).
---
## Architecture
Everything goes through `POST /v1/messages`. Tools and output constraints are features of this single endpoint - not separate APIs.
**User-defined tools** - You define tools (via decorators, Zod schemas, or raw JSON), and the SDK's tool runner handles calling the API, executing your functions, and looping until Claude is done. For full control, you can write the loop manually.
**Server-side tools** - Anthropic-hosted tools that run on Anthropic's infrastructure. Code execution is fully server-side (declare it in `tools`, Claude runs code automatically). Computer use can be server-hosted or self-hosted.
**Structured outputs** - Constrains the Messages API response format (`output_config.format`) and/or tool parameter validation (`strict: true`). The recommended approach is `client.messages.parse()` which validates responses against your schema automatically. Note: the old `output_format` parameter is deprecated; use `output_config: {format: {...}}` on `messages.create()`.
**Supporting endpoints** - Batches (`POST /v1/messages/batches`), Files (`POST /v1/files`), Token Counting (`POST /v1/messages/count_tokens` - see `shared/token-counting.md`), and Models (`GET /v1/models`, `GET /v1/models/{id}` - live capability/context-window discovery) feed into or support Messages API requests.
---
## Current Models (cached: 2026-06-24)
| Model | Model ID | Context | Input $/1M | Output $/1M |
| ----------------- | ------------------- | -------------- | ---------- | ----------- |
| Claude Fable 5.1 | `claude-fable-5-1` | 1M | $10.00 | $50.00 |
| Claude Mythos 5.1 (Project Glasswing only) | `claude-mythos-5-1` | 1M | $10.00 | $50.00 |
| Claude Fable 5 | `claude-fable-5` | 1M | $10.00 | $50.00 |
| Claude Opus 5 | `claude-opus-5` | 1M | $5.00 | $25.00 |
| Claude Opus 4.8 | `claude-opus-4-8` | 1M | $5.00 | $25.00 |
| Claude Opus 4.7 | `claude-opus-4-7` | 1M | $5.00 | $25.00 |
| Claude Opus 4.6 | `claude-opus-4-6` | 1M | $5.00 | $25.00 |
| Claude Sonnet 5 | `claude-sonnet-5` | 1M | $2.00 | $10.00 |
| Claude Sonnet 4.6 | `claude-sonnet-4-6` | 1M | $3.00 | $15.00 |
| Claude Haiku 4.5 | `claude-haiku-4-5` | 200K | $1.00 | $5.00 |
**Partner pricing:** The prices above are Anthropic first-party API rates - they also apply to Claude on Microsoft Foundry, which is billed through the Microsoft Marketplace at standard API rates. Claude on Amazon Bedrock and Vertex AI is partner-operated with separate pricing - see [Bedrock](https://aws.amazon.com/bedrock/pricing/) or [Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/pricing#claude-models). For WebFetch, use the Pricing row in `shared/live-sources.md`.
**ALWAYS use `claude-opus-5` unless the user explicitly names a different model.** This is non-negotiable. Do not use `claude-sonnet-5`, `claude-sonnet-4-6`, or any other model unless the user literally says "use sonnet" or "use haiku". Never downgrade for cost - that's the user's decision, not yours. Use `claude-fable-5-1` only when the user explicitly asks for Claude Fable 5.1, "fable", or Anthropic's most capable model - it has different API behavior than the Opus family (see below) and pricing that exceeds Opus-tier. **Use only the exact model ID strings from the table - they are complete as-is; never append date suffixes** (`claude-sonnet-4-6`, never `claude-sonnet-4-6-20251114` or any other date-suffixed variant you might recall from training data). If the user requests an older model not in the table (e.g., "opus 4.5", "sonnet 3.7"), read `shared/models.md` for the exact ID - do not construct one yourself.
### Claude Fable 5.1 (`claude-fable-5-1`) - most capable widely released model
Claude Fable 5.1 is Anthropic's most capable widely released model, for the most demanding reasoning and long-horizon agentic work; everything below also applies to **Claude Mythos 5.1** (`claude-mythos-5-1`, Project Glasswing - same capabilities, pricing, and API surface; it runs safeguards that depend on the access program, so the `refusal` handling below applies there too; successor to Claude Mythos 5, which ran no safety classifiers). 1M context window (the maximum is also the default), 128K max output. Key API differences from Opus-tier - see `shared/model-migration.md` -> Migrating to Claude Fable 5.1 for details:
- **Thinking is always on** - omit the `thinking` parameter entirely (or send `{type: "adaptive"}`). Any other explicit configuration is rejected: `{type: "disabled"}` and `{type: "enabled", budget_tokens: N}` both return a 400. Control depth with `output_config.effort` (supports `low` through `xhigh` and `max`).
- **The raw chain of thought is never returned** - responses carry regular `thinking` blocks (not `redacted_thinking`): `display: "summarized"` returns a readable summary, `"omitted"` (the default) leaves the `thinking` field as an empty string. Replay rules: pass thinking blocks back unchanged on the same model; other models drop them silently (unbilled - nothing to strip; Claude Mythos 5.1 instead reads them); details in `shared/model-migration.md`.
- **Tokenizer** - same tokenizer as Opus 4.8 (introduced with Opus 4.7). Token counts are roughly unchanged when migrating from Opus 4.7/4.8; per-token pricing differs. Coming from Opus 4.6, Sonnet, Haiku, or older, re-baseline with `count_tokens` (the Opus 4.7 tokenizer uses ~1×-1.35× as many tokens).
- **`refusal` stop reason - handle it, and opt into fallbacks by default** - safety classifiers may decline a request (HTTP 200, `stop_reason: "refusal"`, with a `stop_details` category); always check `stop_reason` before reading `content`. **When you write `claude-fable-5-1` or `claude-opus-5` code, include the server-side `fallbacks` parameter by default.** Simplest form: `betas: ["server-side-fallback-2026-07-01"]` + `fallbacks: "default"`, which routes by refusal category so you never maintain a model list. (The older array form - `betas: ["server-side-fallback-2026-06-01"]` + `fallbacks: [{"model": "claude-opus-4-8"}]` - still works; Claude API and Claude Platform on AWS - on Bedrock, Vertex and Foundry, use the SDKs' client-side `BetaRefusalFallbackMiddleware` + `BetaFallbackState`). Tell the user you've enabled it; drop it only if they decline. Full semantics (billing, mid-stream refusals, credit repricing) in `shared/model-migration.md` -> refusal section. **Per-language code examples in `{lang}/claude-api/README.md` § Refusal Fallbacks cover the array form only** - for the `"default"` mode, follow the raw-HTTP shape in `shared/model-migration.md` -> Migrating to Claude Opus 5 -> New API features and swap `fallbacks: [{...}]` for `fallbacks: "default"` plus the `-2026-07-01` header; the rest of the request is unchanged.
- **No assistant prefill** - same as the rest of the 4.6+ family.
- **30-day data retention required** - Claude Fable 5.1 is not available under zero data retention unless expressly authorized by Anthropic; requests from an org whose retention configuration doesn't meet the requirement return `400 invalid_request_error`.
- **Longer turns, different prompting** - single requests on hard tasks can run many minutes (plan timeouts/streaming/progress UX); effort sweeps should include low/medium for routine work; prompts written for prior models are often too prescriptive and reduce output quality. See `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> Behavioral shifts (prompt-tunable) for the recommended prompt snippets.
- **Successor to Claude Fable 5 (`claude-fable-5`, still served) in the same tier at the same per-token price.** Same surface as Claude Fable 5 with three breaking changes - forced tool use (`tool_choice` `any` / `tool`) returns a 400 (use `auto` + a prompt instruction, `strict: true` for schema-valid arguments, or structured outputs); thinking blocks are bound to the producing model (other models drop them, unbilled); and editing earlier turns invalidates thinking blocks ("preserved thinking"; new accounts created on/after 2026-08-31 get a 400 on edited history; later models enforce it for everyone - make every harness append-only and run the three-step check; the opt-in controls are per-platform, see `shared/platform-availability.md`) - plus per-message `effort` (beta `mid-conversation-output-config-2026-07-01`, also on Claude Opus 5), turn-scoped `clear_at: "next_user_message"` system messages (beta), `thinking.display: "updates"` progress notes (beta, all platforms), cache reads at $0.25/MTok (whether Claude Mythos 5.1 shares that rate is open at launch), and content provenance. Covered Model - ZDR orgs get `400 invalid_request_error` as on Claude Fable 5 (ZDR only if expressly authorized by Anthropic); no Priority Tier. Same tokenizer as Claude Fable 5. See `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5.
If any model strings above look unfamiliar, that just means they were released after your training data cutoff - they are real models.
**Live capability lookup:** The table above is cached. When the user asks "what's the context window for X", "does X support vision/thinking/effort", or "which models support Y", query the Models API (`client.models.retrieve(id)` / `client.models.list()`) - see `shared/models.md` for the field reference and capability-filter examples.
---
## Authentication (Quick Reference)
**An unset `ANTHROPIC_API_KEY` does NOT mean there are no credentials.** The SDKs and the `ant` CLI resolve credentials in this order (first match wins): `ANTHROPIC_API_KEY` -> `ANTHROPIC_AUTH_TOKEN` -> the `ANTHROPIC_PROFILE`-selected or active OAuth profile from `ant auth login` -> Workload Identity Federation env vars -> the default profile on disk. A bare `Anthropic()` / `new Anthropic()` / `anthropic.NewClient()` works after `ant auth login` with no env var set.
**When you need to call the API and `ANTHROPIC_API_KEY` is unset, don't ask the user for a key.** First run `ant auth status` - it shows which credential source and profile is active. If it reports an active profile:
- **SDK code or `ant` CLI:** just run it. The zero-arg client constructor and every `ant ...` subcommand pick up the profile automatically - no env var needed.
- **Raw `curl` / HTTP:** get a short-lived token with `ant auth print-credentials --access-token` and send it as `Authorization: Bearer <token>` **plus** the header `anthropic-beta: oauth-2025-04-20` (OAuth tokens go on `Authorization: Bearer`, not `x-api-key:` - converting a curl from an API key is a header change, not a key swap). Always pass `--access-token`; the no-flag form prints JSON, not a bare token.
Only ask the user for a key if `ant auth status` reports no active credential source (or `ant` itself isn't installed). Suggest `ant auth login` as the first option - it stores a profile under `~/.config/anthropic/` that the SDKs read automatically - and an exported `ANTHROPIC_API_KEY` as the alternative.
Full auth details (named profiles, scopes, the API-key-shadows-profile trap, refresh-token expiry): `shared/anthropic-cli.md`.
---
## Thinking & Effort (Quick Reference)
Use adaptive thinking (`thinking: {type: "adaptive"}`) on every current model - Claude dynamically decides when and how much to think. Per-model rules:
| Model | Thinking config | Omitting `thinking` | `budget_tokens` | Sampling (`temperature`/`top_p`/`top_k`) | Effort levels |
|---|---|---|---|---|---|
| Fable 5 / Claude Fable 5.1 (and the Mythos counterparts) | `{type: "adaptive"}` or omit; explicit `{type: "disabled"}` returns 400 - omit the param instead (Claude Fable 5.1 / Claude Mythos 5.1 also 400 on forced `tool_choice` `any`/`tool`, and run preserved thinking's history-editing check on replayed thinking blocks) | Runs adaptive (thinking is always on) | Removed - `{type: "enabled", budget_tokens: N}` returns 400 | Removed - 400 | `low`/`medium`/`high`/`xhigh`/`max` |
| Claude Opus 5 | `{type: "adaptive"}` or omit; `{type: "disabled"}` accepted **only at effort `high` or below** - 400 at `xhigh`/`max`, and see the disabled-thinking pitfall below | Runs **adaptive** (thinking is on by default - unlike Opus 4.8/4.7) | Removed - 400 | Removed - 400 | `low`-`max` (all five) |
| Opus 4.8 / 4.7 | `{type: "adaptive"}` is the only on-mode; `{type: "disabled"}` accepted | Runs **without** thinking - set `{type: "adaptive"}` explicitly | Removed - 400 | Removed - 400 | `low`/`medium`/`high`/`xhigh`/`max` |
| Sonnet 5 | `{type: "adaptive"}` is the only on-mode; `{type: "disabled"}` accepted | Runs adaptive | Removed - 400 | Removed - 400 | `low`/`medium`/`high`/`xhigh`/`max` |
| Opus 4.6 / Sonnet 4.6 | `{type: "adaptive"}` (recommended; auto-enables interleaved thinking, no beta header) | Set `{type: "adaptive"}` explicitly | Deprecated - do not use in new code; transitional escape hatch only (see below) | Allowed | `low`/`medium`/`high`/`max` (`xhigh` arrived with Opus 4.7) |
| Older (Sonnet 4.5, Haiku 4.5, ...) - only if explicitly requested | `{type: "enabled", budget_tokens: N}` | No thinking | Required for thinking; must be less than `max_tokens`, minimum 1024 - errors otherwise | Allowed | `effort` works on Opus 4.5 (`low`/`medium`/`high` only - no `xhigh`/`max`); errors on Sonnet 4.5 / Haiku 4.5 |
Opus 4.8 keeps the same request surface as 4.7 (no new breaking changes) - see `shared/model-migration.md` -> Migrating to Opus 4.8 for the behavioral re-tuning, and -> Migrating to Opus 4.7 for the full breaking-change list when coming from 4.6 or earlier. With `thinking` disabled, Opus 4.8 may write longer reasoning into the visible response - leave adaptive thinking on, or add a final-answer-only instruction (see the migration guide).
- **Effort (GA, no beta header):** `output_config: {effort: "low"|"medium"|"high"|"xhigh"|"max"}` - inside `output_config`, not top-level; default `high` (equivalent to omitting it). Controls thinking depth and overall token spend; combine with adaptive thinking for the best cost-quality tradeoffs. `xhigh` (added on Opus 4.7, between `high` and `max`) is the best setting for most coding and agentic use cases on Fable 5 / Opus 4.7/4.8 / Sonnet 5, and the default in Claude Code; effort matters more on those models than on any prior model in their tier - re-tune it when migrating, and run long-horizon/agentic tasks at `high`/`xhigh` with the full task spec given up front. Use a minimum of `high` for intelligence-sensitive work, `max` when correctness matters more than cost, and `low` for subagents or simple tasks - lower effort means fewer and more-consolidated tool calls, less preamble, and terser confirmations (`high` is often the sweet spot balancing quality and token efficiency).
- **Choosing an effort level (cost tuning):** Effort is the first quality-trading lever, after the free wins (caching first) - it trades thoroughness against token spend within one model, and the top of the range earns its cost only on hard problems (raise to `max` only when measurement shows headroom at the level below). Which workloads repay higher effort is a property of the workload: coding and long-horizon agentic work respond strongly; chat, classification, and high-volume or latency-sensitive routes often don't and do well at `low`, with `medium` as the cost-saving step-down where quality holds (the per-level defaults above cover the rest). Measure on a sample of real requests before raising a default, and tune per route rather than globally. Before building a multi-model cost cascade, measure the simpler alternative first - the most capable model at lower effort on the same tasks: lower effort on the newest models often matches or exceeds prior-generation performance at high effort (on Fable 5, lower effort often exceeds `xhigh` on prior models), and one model means one cache namespace (caches are model-scoped, so a cascade forfeits cache reuse across its models; a mid-conversation top-level `effort` change still invalidates the messages cache, though the per-message effort system message avoids that on Claude Fable 5.1 / Claude Mythos 5.1 / Claude Opus 5 - `shared/prompt-caching.md` § Invalidation hierarchy). Judge cost per completed task, not per request - a cheaper request that needs more turns or retries to finish the job isn't cheaper. For the measured effort/cost tradeoffs by workload and the full lever order, `shared/cost-optimization.md` § 2.6.
- **Thinking display - `"omitted"` by default on Fable 5 / Claude Fable 5.1 / Mythos 5 / Claude Mythos 5.1 / Opus 5 / 4.8 / 4.7 / Sonnet 5:** `display: "summarized"` returns a readable summary of the reasoning; `"omitted"` (the default on all eight - a silent change from Opus 4.6 and Sonnet 4.6, where it was `"summarized"`) streams `thinking` blocks with empty text. `display` controls visibility only - thinking happens and is billed the same under every setting; the raw chain of thought is never exposed on any model. If you stream reasoning to users, the default looks like a long pause before output - set `thinking: {type: "adaptive", display: "summarized"}` explicitly. (Independent of display, echo thinking blocks back unchanged when continuing on the same model; other models silently ignore them (Claude Fable 5.1 / Claude Mythos 5.1 read them) - see the migration guide.) On Claude Fable 5.1 / Claude Mythos 5.1 / Claude Fable 5, `display: "updates"` (beta `thinking-display-updates-2026-08-18`, every platform) hides reasoning like `"omitted"` but returns the model's between-tool-call progress notes as short `thinking` block summaries - see `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 -> New API features.
- **When the user asks for "extended thinking", a "thinking budget", or `budget_tokens`:** always use Fable 5/5.1, Opus 5, 4.8, 4.7, or 4.6 with `thinking: {type: "adaptive"}` - the fixed thinking-token-budget concept is deprecated and adaptive thinking replaces it. Do NOT use `budget_tokens` for new 4.6/4.7/4.8 code and do NOT switch to an older model just because the user mentions it. *Gradual-migration carve-out:* `budget_tokens` is still functional on Opus 4.6 and Sonnet 4.6 only, as a transitional escape hatch for existing code that needs a hard token ceiling before you've tuned `effort` - see `shared/model-migration.md` -> Transitional escape hatch. It is fully removed on Fable 5/5.1, Opus 5/4.7/4.8, and Sonnet 5.
---
## Compaction (Quick Reference)
**Beta, Fable 5/5.1, Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 5, and Sonnet 4.6.** For long-running conversations that may exceed the 1M context window, enable server-side compaction. The API automatically summarizes earlier context when it approaches the trigger threshold (default: 150K tokens). Requires beta header `compact-2026-01-12`.
**Critical:** Append `response.content` (not just the text) back to your messages on every turn. Compaction blocks in the response must be preserved - the API uses them to replace the compacted history on the next request. Extracting only the text string and appending that will silently lose the compaction state.
See `{lang}/claude-api/README.md` (Compaction section) for code examples. Full docs via WebFetch in `shared/live-sources.md`.
---
## Prompt Caching (Quick Reference)
**Prefix match.** Any byte change anywhere in the prefix invalidates everything after it. Render order is `tools` -> `system` -> `messages`. Keep stable content first (frozen system prompt, deterministic tool list), put volatile content (timestamps, per-request IDs, varying questions) after the last `cache_control` breakpoint.
**Mid-conversation operator instructions** (Claude Opus 5, Claude Opus 4.8, Claude Fable 5, Claude Fable 5.1, Claude Mythos 5, Claude Mythos 5.1; not Claude Sonnet 5; no beta header): append `{"role": "system", ...}` to `messages[]` instead of editing top-level `system`. Preserves the cached history prefix and is the prompt-injection-safe operator channel. See `shared/prompt-caching.md` § Mid-conversation system messages.
**Top-level auto-caching** (`cache_control: {type: "ephemeral"}` on `messages.create()`) is the simplest option when you don't need fine-grained placement. Max 4 breakpoints per request. Minimum cacheable prefix is model-dependent (512-4096 tokens - see `shared/prompt-caching.md` § API reference) - shorter prefixes silently won't cache.
**Verify with `usage.cache_read_input_tokens`** - if it's zero across repeated requests, a silent invalidator is at work (`datetime.now()` in system prompt, unsorted JSON, varying tool set).
For placement patterns, architectural guidance, and the silent-invalidator audit checklist: read `shared/prompt-caching.md`. Language-specific syntax: `{lang}/claude-api/README.md` (Prompt Caching section).
---
## Fast Mode (Quick Reference)
**Research preview, Claude Opus 5 / Opus 4.8 only** - Claude API and Managed Agents, not Bedrock / Google Cloud / Foundry. Opus 4.7 fast mode has been removed: `speed: "fast"` on 4.7 returns an error. Fast mode on Claude Opus 5 is priced at $10 / $50 per MTok. Fast mode runs the same model at up to 2.5x higher output tokens per second, at premium pricing. Three things are required on every request: use the **beta** messages endpoint (`client.beta.messages....`), pass the beta flag `fast-mode-2026-02-01`, and set `speed: "fast"` as a top-level request parameter (not a header, not in `extra_body`).
```python
client.beta.messages.create(
model="claude-opus-5", max_tokens=4096,
speed="fast", betas=["fast-mode-2026-02-01"],
messages=[...],
)
```
| Language | Beta flag | Speed parameter |
|---|---|---|
| Python | `betas=["fast-mode-2026-02-01"]` | `speed="fast"` |
| TypeScript / Ruby | `betas: ["fast-mode-2026-02-01"]` | `speed: "fast"` |
| Go | `[]anthropic.AnthropicBeta{anthropic.AnthropicBetaFastMode2026_02_01}` | `Speed: anthropic.BetaMessageNewParamsSpeedFast` |
| Java | `.addBeta(AnthropicBeta.FAST_MODE_2026_02_01)` | `.speed(MessageCreateParams.Speed.FAST)` |
| C# | `Betas = ["fast-mode-2026-02-01"]` | `Speed = Speed.Fast` (`Anthropic.Models.Beta.Messages`) |
| PHP | `betas: ['fast-mode-2026-02-01']` | `speed: 'fast'` |
| cURL | `anthropic-beta: fast-mode-2026-02-01` header | `"speed": "fast"` in body |
`response.usage.speed` reports which speed was used. Fast mode has its own rate limit separate from standard Opus; on 429, either retry after the `retry-after` delay or drop `speed` and fall back to standard (note: switching speed invalidates prompt cache). Not available with Batch API, Priority Tier, Claude Platform on AWS, or third-party platforms.
**Priority Tier is not supported on every current model.** It is supported on Claude Fable 5, Opus 4.8, and the older current models, but Claude Opus 5, Claude Sonnet 5, Claude Fable 5.1, Claude Mythos 5.1, Claude Mythos 5, and Mythos Preview are excluded - a Priority Tier request naming one of them fails validation.
---
## Task Budgets (Quick Reference)
**Beta, Claude Opus 5 / Fable 5 / Claude Fable 5.1 (confirm at launch) / Sonnet 5 / Opus 4.8 / 4.7.** A task budget gives Claude a token ceiling for an agentic loop so it paces itself and finishes gracefully instead of being cut off - distinct from `max_tokens`, which is an enforced per-response ceiling the model is not aware of. Minimum `total`: 20,000. Set `task_budget` inside `output_config` on `client.beta.messages.stream(...)` with beta flag `task-budgets-2026-03-13` - use streaming so the large `max_tokens` doesn't hit HTTP timeouts (full details: `shared/model-migration.md` -> Task Budgets):
```python
with client.beta.messages.stream(
model="claude-opus-5", max_tokens=128000,
output_config={"effort": "high", "task_budget": {"type": "tokens", "total": 64000}},
betas=["task-budgets-2026-03-13"],
messages=[...], tools=[...],
) as stream:
response = stream.get_final_message()
```
`task_budget` fields: `type` (always `"tokens"`), `total`, and optional `remaining` (defaults to `total`). The server injects a countdown marker Claude sees during generation; the budget counts what Claude generates and the tool results it reads this turn - **not** the full history you resend each request. Not the same thing as **Managed Agents session budgets** - those are hard, dollar-denominated, platform-enforced caps on one CMA session (`shared/managed-agents-core.md` § Session budgets); a task budget is advisory and token-denominated.
**Observing spend:** accumulate `response.usage.output_tokens` (plus the token count of the tool-result blocks you append) across loop iterations if you want to display progress. Leave `remaining` unset in the normal loop - the server tracks the countdown itself, and passing a client-computed `remaining` while also resending full history under-reports the budget. **Only pass `remaining`** when you compact or rewrite history between requests and the server can no longer derive prior spend.
---
## Provider Clients (Quick Reference)
When targeting Claude on a third-party platform, use that platform's dedicated client class - not the first-party `Anthropic()` client with a `base_url` override. After construction the client exposes the same `messages.create` / `.stream` surface as the first-party SDK.
### Amazon Bedrock
Use the **Mantle** client (Messages-API Bedrock endpoint). Bedrock model IDs take an `anthropic.` prefix (e.g. `"anthropic.claude-opus-5"`). Region is required.
| Language | Client |
|---|---|
| Python | `from anthropic import AnthropicBedrockMantle` -> `AnthropicBedrockMantle(aws_region="...")` |
| TypeScript | `import { AnthropicBedrockMantle } from "@anthropic-ai/bedrock-sdk"` -> `new AnthropicBedrockMantle({ awsRegion: "..." })` |
| Go | `bedrock.NewMantleClient(ctx, bedrock.MantleClientConfig{ AWSRegion: "..." })` |
| Java | `AnthropicOkHttpClient.builder().backend(BedrockMantleBackend.fromEnv()).build()` (from `com.anthropic.bedrock.backends`) |
| C# | `new AnthropicBedrockMantleClient(new() { AwsRegion = "..." })` (package `Anthropic.Bedrock`) |
| PHP | `use Anthropic\Bedrock\MantleClient;` -> `new MantleClient(awsRegion: '...')` |
| Ruby | `Anthropic::BedrockMantleClient.new(aws_region: "...")` |
`AnthropicBedrock` / `BedrockClient` / `BedrockBackend` (without `Mantle`) are the legacy `bedrock-runtime` InvokeModel path - prefer the Mantle client for new code.
### Microsoft Foundry
| Language | Client |
|---|---|
| Python | `from anthropic import AnthropicFoundry` -> `AnthropicFoundry(api_key=..., resource="...")` |
| TypeScript | `import AnthropicFoundry from "@anthropic-ai/foundry-sdk"` -> `new AnthropicFoundry({ ... })` |
| Java | `AnthropicOkHttpClient.builder().backend(FoundryBackend.fromEnv()).build()` (from `com.anthropic.foundry.backends`) |
| C# | `new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(...))` (package `Anthropic.Foundry`) |
| PHP | `Foundry\Client::withCredentials(...)` |
The Go and Ruby SDKs do not currently support Foundry. For Ruby, use the standard `Anthropic::Client.new(base_url: "<foundry endpoint>")` as a fallback (Entra ID auth is not built in). For Claude Platform on AWS, see `shared/claude-platform-on-aws.md`.
### Google Cloud Vertex AI
Two required constructor args: GCP `project_id` and `region`. Vertex model IDs take **no prefix** - current-generation models (Opus 4.8/4.7/4.6, Sonnet 5, Sonnet 4.6) use the bare first-party ID (e.g. `"claude-opus-5"`); dated-snapshot models use an `@` version separator (e.g. `claude-opus-4-5@20251101`, **not** `claude-opus-4-5-20251101`). Auth is GCP ADC (`gcloud auth application-default login`); no Anthropic API key. `region` can be `"global"` (recommended), a multi-region (`"us"`/`"eu"`), or a specific region. After construction, use the same `messages.create` / `.stream` surface.
| Language | Client |
|---|---|
| Python | `from anthropic import AnthropicVertex` -> `AnthropicVertex(project_id="...", region="...")` (install `"anthropic[vertex]"`) |
| TypeScript | `import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"` -> `new AnthropicVertex({ projectId, region })` |
| Go | `import "github.com/anthropics/anthropic-sdk-go/vertex"` -> `anthropic.NewClient(vertex.WithGoogleAuth(ctx, region, projectID))` |
| Java | `AnthropicOkHttpClient.builder().backend(VertexBackend.builder().region("...").project("...").build()).build()` (from `com.anthropic.vertex.backends`) |
| C# | `new AnthropicClient { Backend = new VertexBackend(projectId, region) }` (package `Anthropic.Vertex`) |
| PHP | `use Anthropic\Vertex;` -> `Vertex\Client::fromEnvironment(location: '...', projectId: '...')` - note `location`, not `region` |
| Ruby | `Anthropic::VertexClient.new(region: "...", project_id: "...")` |
---
## Context Editing (Quick Reference)
**Beta.** Context editing **clears** old tool results or thinking blocks from the conversation before the model sees it; it is **not compaction** (which summarizes). On `client.beta.messages.*` with beta `context-management-2025-06-27`, pass `context_management.edits` with a strategy type:
```python
client.beta.messages.create(
model="claude-opus-5", max_tokens=4096,
betas=["context-management-2025-06-27"],
context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
tools=[...], messages=[...],
)
```
Strategy types: `clear_tool_uses_20250919` (clears old tool results; optional `clear_tool_inputs: true` also clears the tool_use params) and `clear_thinking_20251015` (clears thinking blocks). Do **not** use `compact_20260112` or beta `compact-2026-01-12` - those are the separate compaction feature.
---
## Mid-Conversation System Messages (Quick Reference)
**Claude Opus 5, Claude Opus 4.8, Claude Fable 5, Claude Fable 5.1, Claude Mythos 5, and Claude Mythos 5.1; not Claude Sonnet 5; no beta header.** Append `{"role": "system", "content": "..."}` to the `messages` array (not the top-level `system` field) to add an operator instruction mid-conversation without invalidating the cached prefix. Use the regular `client.messages.create` - there is no beta. A mid-conversation system message must follow a `user` message (or an `assistant` message ending in server-tool use), and must be either the last entry in `messages` or be followed by an `assistant` turn - it cannot be `messages[0]`. Availability: `shared/platform-availability.md`. See `shared/prompt-caching.md` § Mid-conversation system messages. A beta extension shipped with Claude Fable 5.1: `output_config: {effort: ...}` with `content: []` changes effort from that point on without a cache reset (beta `mid-conversation-output-config-2026-07-01`; Claude Fable 5.1, Claude Mythos 5.1, Claude Opus 5; Claude API). An effort-only message (empty `content`) is exempt from the placement rules above - it can sit anywhere in `messages`, including first or between an assistant turn and the next user turn; the rules apply to text and `clear_at` messages. For a per-turn reminder, give the message `clear_at: "next_user_message"` (beta `mid-conversation-system-clear-at-2026-08-21`): it renders for one turn, then stays in the transcript cleared - never delete earlier copies (on Claude Fable 5.1 deleting one invalidates later thinking blocks); without the beta, a text block after the tool results, earlier copies kept. See `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 -> New API features.
---
## Managed Agents (Beta)
**Managed Agents** is a third surface: server-managed stateful agents with Anthropic-hosted tool execution. You create a persisted, versioned Agent config (`POST /v1/agents`), then start Sessions that reference it. Each session provisions a container as the agent's workspace - bash, file ops, and code execution run there; the agent loop itself runs on Anthropic's orchestration layer and acts on the container via tools. The session streams events; you send messages and tool results back.
Availability: `shared/platform-availability.md`. For agents on Bedrock / Vertex / Foundry (where Managed Agents is unsupported), use Claude API + tool use.
**Mandatory flow:** Agent (once) -> Session (every run). `model`/`system`/`tools` live on the agent, never the session. See `shared/managed-agents-overview.md` for the full reading guide, beta headers, and pitfalls.
**Beta headers:** `managed-agents-2026-04-01` - the SDK sets this automatically for all `client.beta.{agents,environments,sessions,vaults,memory_stores,deployments,deployment_runs}.*` calls. Files API and Skills API are out of beta - no beta header needed (see the API Drift table above for the migration guides).
**Subcommands** - invoke directly with `/claude-api <subcommand>`:
| Subcommand | Action |
|---|---|
| `managed-agents-onboard` | Walk the user through setting up a Managed Agent from scratch. **Read `shared/managed-agents-onboarding.md` immediately** and follow its interview script: **describe -> configure the agent (propose, don't interrogate) -> environment -> session** (same arc as the Console quickstart, auth deferred to the session step) - defaults and inline suggestions do the work, with a silent viability gate (job vs tools/credentials/data) before any code is emitted. Do not summarize - run the interview. |
**Reading guide:** Start with `shared/managed-agents-overview.md`, then the topical `shared/managed-agents-*.md` files (core, environments, tools, events, outcomes, multiagent, webhooks, memory, scheduled-deployments, client-patterns, onboarding, api-reference). For Python, TypeScript, Go, Ruby, PHP, and Java, read `{lang}/managed-agents/README.md` for code examples. For cURL, read `curl/managed-agents.md`. **Agents are persistent - create once, reference by ID.** Define agents and environments as version-controlled YAML applied with the `ant` CLI - this is the recommended flow (see `shared/anthropic-cli.md`): the CLI owns the control plane (creating and updating agents), your code owns the data plane (`sessions.create` with the stored agent ID). Call `agents.create()` in code only when you must provision programmatically; either way, store the returned agent ID and pass it to every subsequent `sessions.create`; never call `agents.create()` in the request path. If a binding you need isn't shown in the language README, WebFetch the relevant entry from `shared/live-sources.md` rather than guess. C# has beta Managed Agents support via `client.Beta.Agents` and related namespaces - see `csharp/claude-api/README.md` for details, or `curl/managed-agents.md` for raw HTTP reference.
**When the user wants to set up a Managed Agent from scratch** (e.g. "how do I get started", "walk me through creating one", "set up a new agent"): read `shared/managed-agents-onboarding.md` and run its interview - same flow as the `managed-agents-onboard` subcommand.
**When the user asks "how do I write the client code for X":** reach for `shared/managed-agents-client-patterns.md` - covers lossless stream reconnect, `processed_at` queued/processed gate, interrupt, `tool_confirmation` round-trip, the correct idle/terminated break gate, post-idle status race, stream-first ordering, file-mount gotchas, etc. For credentials, lead with vault `environment_variable` credentials - the first-class mechanism; secrets are substituted at egress and never enter the sandbox (`shared/managed-agents-tools.md` -> Vaults). Keeping credentials host-side via custom tools is the fallback where vault credentials don't fit (e.g. self-hosted sandboxes).
**When the user wants the agent to run on a schedule** (cron, "every night", "weekly report"): read `shared/managed-agents-scheduled-deployments.md` - deployments fire sessions autonomously on a cron cadence, with per-firing run records and lifecycle controls (pause/unpause/archive).
**When the agent's work fans out** (research across several sources, per-file or per-record work, "look into N things, then summarize") **or one loop would fill its context with reading:** read `shared/managed-agents-multiagent.md` and recommend a multiagent session - start with just `{"type": "self"}` in the roster so the agent can delegate to copies of itself, then move reading-heavy sub-tasks to a cheaper worker agent (e.g. Claude Haiku 4.5) referenced by ID.
---
## Server Tools (Quick Reference)
Server-side tools run on Anthropic's infrastructure - no client-side execution loop. Declare in `tools`; results arrive as content blocks in the same response. **No beta header** unless noted. **Prefer the latest type variant your model supports.** The `_20260209` web search / web fetch variants below (dynamic filtering) require Opus 5/4.8/4.7/4.6, Sonnet 5, or Sonnet 4.6; the basic variants for older models are listed after the table.
| Tool | `type` | `name` | Key optional params | Result block type |
|---|---|---|---|---|
| Web search | `web_search_20260209` | `web_search` | `max_uses`, `allowed_domains`/`blocked_domains`, `user_location` | `web_search_tool_result` -> `.content` is a list of `web_search_result` |
| Web fetch | `web_fetch_20260209` | `web_fetch` | `max_uses`, `allowed_domains`/`blocked_domains`, `citations`, `max_content_tokens` | `web_fetch_tool_result` -> `.content` is a `web_fetch_result` with a `document` block |
| Code execution | `code_execution_20260521` | `code_execution` | none | `bash_code_execution_tool_result` -> `.content.stdout` / `.stderr` / `.return_code` |
| Tool search (regex) | `tool_search_tool_regex_20251119` | `tool_search_tool_regex` | mark other tools `defer_loading: true` | `tool_search_tool_result` |
| Tool search (BM25) | `tool_search_tool_bm25_20251119` | `tool_search_tool_bm25` | mark other tools `defer_loading: true` | `tool_search_tool_result` |
`web_search_20260209` / `web_fetch_20260209` have built-in dynamic filtering - code execution runs under the hood, so do **not** separately declare `code_execution` in `tools` (a second execution environment confuses the model). For models older than Opus 4.6 / Sonnet 4.6, use the basic variants `web_search_20250305` / `web_fetch_20250910` instead; on Vertex AI only basic `web_search_20250305` is available. `code_execution_20260120` (REPL persistence + programmatic tool calling) runs on Opus 4.5+ / Sonnet 4.5+. **Go SDK only**: `code_execution_20260521` lives under `client.Beta.Messages.New` with `Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25"}` (other languages use plain `client.messages.create`); `code_execution_20260120` uses the non-beta `client.Messages.New` in Go like everywhere else. Web fetch only fetches URLs already present in the conversation. Provider availability varies by tool - see `shared/platform-availability.md`. See `shared/tool-use-concepts.md` for `pause_turn` handling.
## Document & File Input (Quick Reference)
**PDF (base64, no beta):** `{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": <b64 string>}}` in user content, placed before the text block. Base64 string must have no newlines. Limits: 32 MB request, 600 pages (100 for 200k-context models). Java: `ContentBlockParam.ofDocument(DocumentBlockParam... Base64PdfSource.builder().data(...))`.
**Files API (no beta):** upload via `client.files.upload(...)` -> response `id` is the `file_id`. Reference it as `{"type": "document", "source": {"type": "file", "file_id": "..."}}` for PDF/text, or `{"type": "image", ...}` for images - the content-block type must match the file's MIME type. To migrate code off `files-api-2025-04-14`, WebFetch the Files API row in `shared/live-sources.md`. Availability: `shared/platform-availability.md`.
**Citations (no beta):** set `citations: {enabled: true}` on each `document` content block (all or none). Response splits into multiple `text` blocks; cited blocks carry a `citations` array. Each citation has `cited_text`, `document_index`, `document_title`, and a location by `type`: `char_location` (`start_char_index`/`end_char_index`) for plain text, `page_location` (`start_page_number`/`end_page_number`, 1-indexed) for PDF, `content_block_location` for custom content. Incompatible with `output_config.format` (returns a 400).
## Tool Use Patterns (Quick Reference)
**Strict tool use (no beta):** set `strict: true` as a top-level field on the tool definition (alongside `name`/`description`/`input_schema`), **not** on `tool_choice`. Schema must have `additionalProperties: false` + `required`. Guarantees `tool_use.input` validates exactly. Go: `Strict: anthropic.Bool(true)` + `additionalProperties` via `InputSchema.ExtraFields`; Java: `.strict(true)` + `.putAdditionalProperty("additionalProperties", JsonValue.from(false))`.
**Parallel tool use (default on):** one assistant message may contain multiple `tool_use` blocks. Execute them concurrently, then return **all** `tool_result` blocks in a **single** user message - splitting them across multiple messages silently trains Claude to stop making parallel calls. For a failed tool, return `tool_result` with `is_error: true` - don't drop it.
**Tool Runner (SDK beta helper):** drives the tool-call loop for you via `client.beta.messages.*`. Python: `@beta_tool` decorator + `client.beta.messages.tool_runner(...)` -> `runner.until_done()`. TypeScript: `betaZodTool({...})` from `@anthropic-ai/sdk/helpers/beta/zod` + `client.beta.messages.toolRunner(...)` -> `await runner`. Go: `toolrunner.NewBetaToolFromJSONSchema(...)` + `client.Beta.Messages.NewToolRunner(...)` -> `.RunToCompletion(ctx)`. Java requires `.addBeta("structured-outputs-2025-11-13")`. Ruby: `Anthropic::BaseTool` subclass + `client.beta.messages.tool_runner(...)`. PHP: `BetaRunnableTool` + `->toolRunner(...)`. C#: raw JSON-schema tools + `BetaToolRunner` via `client.Beta.Messages.ToolRunner(...)`.
**Programmatic tool calling (no beta header):** Claude calls your custom tool from inside code execution. Add `{"type": "code_execution_20260120", "name": "code_execution"}` **and** set `"allowed_callers": ["code_execution_20260120"]` on your custom tool. Opus 4.5+ / Sonnet 4.5+ (availability: `shared/platform-availability.md`). When responding to a pending programmatic call, the user message must contain **only** `tool_result` blocks (no text). Not compatible with `strict: true`, `disable_parallel_tool_use`, forced `tool_choice`, or MCP tools.
## Other API Surfaces (Quick Reference)
**Message Batches (no beta; availability: `shared/platform-availability.md`):** `client.messages.batches.create(requests=[{custom_id, params}, ...])` -> poll `client.messages.batches.retrieve(id).processing_status` until `"ended"` -> stream `client.messages.batches.results(id)`. Each result has `.custom_id` + `.result.type` (`succeeded`/`errored`/`canceled`/`expired`); on success read `.result.message.content`. Python wraps requests as `Request(custom_id=..., params=MessageCreateParamsNonStreaming(...))`. Results arrive in **any order** - key by `custom_id`, never by position.
**Models API (no beta; availability: `shared/platform-availability.md`):** `client.models.list()` (auto-paginates) and `client.models.retrieve("claude-opus-5")`. Each model object has `id`, `display_name`, `created_at`, and - since Mar 2026 - `max_input_tokens` (the context window), `max_tokens` (the output cap), and `capabilities`. There is no `context_window` field.
**Stop details (GA, Opus 4.7+):** `response.stop_details` is populated **only when `stop_reason == "refusal"`** (fields: `type: "refusal"`, `category` - an open set, e.g. `"cyber"`, `"bio"`, `"reasoning_extraction"`, `"frontier_llm"`, or `null`; see the docs for the full list - and `explanation`). It is `null` for every other `stop_reason` (`end_turn`, `max_tokens`, `tool_use`, `pause_turn`, ...) - always guard before reading.
**Admin API (beta, since 2026-08-26):** organization management - members, invites, workspaces and workspace members, API keys, rate limit reports, service accounts, federation issuers/rules, CMEK external keys - under `client.beta.organization` in all seven SDKs and `ant beta:organization` in the CLI. Requires an admin credential: an Admin API key (`sk-ant-admin...`, read from `ANTHROPIC_API_KEY`) or an `org:admin` OAuth token (`ANTHROPIC_AUTH_TOKEN`); regular API keys are rejected. Usage and cost reports and the Claude Enterprise user-management/analytics endpoints are **not** in the SDKs - raw HTTP only. See `shared/admin-api.md`.
**Client config (no beta):** `timeout` default 10 min; **units differ by SDK** - Python/Ruby: seconds; TypeScript: **milliseconds**; Go `option.WithRequestTimeout(time.Duration)`; Java `Duration`; C# `TimeSpan`. TS scales the default up to 60 min for large `max_tokens` on non-streaming requests; Java does so for streaming requests (Java non-streaming scales 30s-10 min). `max_retries`/`maxRetries` default 2 (retries 408/409/429/5xx + connection errors). `base_url` (or `ANTHROPIC_BASE_URL` env). Per-request override: Python `client.with_options(timeout=5.0).messages.create(...)`; TS `client.messages.create({...}, {timeout: 5_000})`; Ruby `request_options: {timeout: 5}`. Timeouts are retried - wall-clock can reach `timeout × (max_retries+1)`.
## Workload Identity Federation (Quick Reference)
**GA, no beta header.** Construct the normal zero-arg client (`Anthropic()` / `new Anthropic()` / `anthropic.NewClient()` / `AnthropicOkHttpClient.fromEnv()`); the SDK auto-detects WIF when **all** of `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, and `ANTHROPIC_IDENTITY_TOKEN_FILE` (or `ANTHROPIC_IDENTITY_TOKEN`) are set, exchanges the JWT at `/v1/oauth/token`, and auto-refreshes. `ANTHROPIC_WORKSPACE_ID` does not gate activation - required only when the federation rule spans multiple workspaces (else 400 `workspace_id_required`), optional for single-workspace rules. `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` (even empty) outrank WIF, and a set `ANTHROPIC_PROFILE` also wins over the federation env vars (a missing named profile is an error, not a fall-through) - unset all three.
---
## Reading Guide
After detecting the language, read the relevant files based on what the user needs. Every `{lang}/...`, `shared/...`, and `curl/...` path cited in this document is relative to this skill's base directory, and none of those files' content is included above - Read each one on demand before relying on what it covers.
**All SDK languages use the same multi-file layout** - directory `{lang}/claude-api/` containing `README.md` (install, client init, basic request, thinking, caching, stop details, misc), `tool-use.md` (tool definitions, agentic loop, Anthropic-defined tools, structured outputs), `streaming.md`, `batches.md`, `files-api.md`. Not every language has every file (e.g., Ruby has no `batches.md`); if a file is absent, that feature's example is not yet documented for that language - fall back to the cURL shape or WebFetch the SDK repo from `shared/live-sources.md`. **cURL** -> `curl/examples.md`.
The Quick Task Reference below uses the `{lang}/claude-api/FILE.md` path notation for all languages.
### Quick Task Reference
**Single text classification/summarization/extraction/Q&A:**
-> Read only `{lang}/claude-api/README.md` - **always read the README first** for any task (installation, quick start, common patterns, error handling)
**Chat UI or real-time response display:**
-> Read `{lang}/claude-api/README.md` + `{lang}/claude-api/streaming.md`
**Long-running conversations (may exceed context window):**
-> Read `{lang}/claude-api/README.md` - see Compaction section
**Migrating to a newer model (Fable 5.1 / Fable 5 / Opus 5 / Opus 4.8 / Opus 4.7 / Opus 4.6 / Sonnet 5 / Sonnet 4.6), replacing a retired model, or translating `budget_tokens` / prefill patterns to the current API:**
-> Read `shared/model-migration.md`
**Upgrading the Anthropic SDK package itself across a major version (`anthropic` 0.x -> 1.x: `httpx2`, awaited async `.with_raw_response`, removed deprecated parameters / aliases / Text Completions, Python >= 3.10) - or writing new code against a project already on 1.x:**
-> Read `{lang}/claude-api/sdk-upgrade.md` (currently Python only; other SDKs have no bundled major-version guide yet - use that SDK's CHANGELOG via `shared/live-sources.md`)
**Prompting or tuning Fable 5/5.1 (long turns, effort, verbosity, autonomous runs, sub-agents):**
-> Read `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> Behavioral shifts (prompt-tunable) + Long-running agent recommendations
**Prompting or tuning Claude Fable 5.1 (progress updates, parallel tool calls, writing density / formatting, autonomy, test sprawl, whole-file rewrites) or making a harness compatible with preserved thinking's history-editing check (history edits, compaction, per-turn reminders):**
-> Read `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 -> New API features + Behavioral shifts (prompt-tunable); for the history-editing check itself (the three-step check, the append-only edit table, compaction shapes), Breaking change 3 in the same section
**Prompt caching / optimize caching / "why is my cache hit rate low":**
-> Read `shared/prompt-caching.md` (prefix-stability design, breakpoint placement, anti-patterns that silently invalidate cache) + `{lang}/claude-api/README.md` (Prompt Caching section)
**Auditing or cleaning up prompts, skills, or tool descriptions ("is this prompt outdated", "remove the cruft", "this was written for an older model"):**
-> Read `shared/prompt-audit.md` - dated-pattern tables with greppable signals, the keep list (what NOT to delete), and the report + proposed-diff output contract
**Count tokens in a file / prompt / diff ("how many tokens is X"):**
-> Read `shared/token-counting.md` - use `messages.count_tokens`, never `tiktoken`
**Reducing or reviewing API spend ("the bill is too high", "make this cheaper", "am I overspending", cost per completed task, cheapest model or effort that holds quality):**
-> Read `shared/cost-optimization.md` - baseline and token profile first, then the levers in order (free wins before tradeoffs) with measured expectations, and a workload-shape -> lever mapping table
**Function calling / tool use / agents:**
-> Read `{lang}/claude-api/README.md` + `shared/tool-use-concepts.md` (conceptual foundations: function calling, code execution, memory, structured outputs) + `{lang}/claude-api/tool-use.md` (language-specific code examples: tool runner, manual loop, code execution, memory, structured outputs)
**Agent design (tool surface, context management, caching strategy):**
-> Read `shared/agent-design.md` (bash vs. dedicated tools, programmatic tool calling, tool search/skills, context editing vs. compaction vs. memory, caching principles)
**Batch processing (non-latency-sensitive; runs asynchronously at 50% cost):**
-> Read `{lang}/claude-api/README.md` + `{lang}/claude-api/batches.md`
**File uploads across multiple requests (same file without re-uploading):**
-> Read `{lang}/claude-api/README.md` + `{lang}/claude-api/files-api.md`
**Organization administration (members, invites, workspaces, API keys, rate limit reports, service accounts, WIF resources, CMEK):**
-> Read `shared/admin-api.md` - `client.beta.organization` endpoint/method table, admin credentials, per-language naming and pagination, what stays curl-only
**Debugging HTTP errors or implementing error handling:**
-> Read `shared/error-codes.md` - per-SDK typed exception class table and the Go `errors.As` pattern
**Latest official documentation:**
-> WebFetch the URLs in `shared/live-sources.md`
**Managed Agents (server-managed stateful agents with workspace):**
-> See the reading guide in the `## Managed Agents (Beta)` section above - it lists every `shared/managed-agents-*.md` file and the language-specific READMEs (`{lang}/managed-agents/README.md`, `curl/managed-agents.md`).
---
## When to Use WebFetch
Use WebFetch to get the latest documentation when:
- User asks for "latest" or "current" information
- Cached data seems incorrect
- User asks about features not covered here
Live documentation URLs are in `shared/live-sources.md`.
## Common Pitfalls
- Don't truncate inputs when passing files or content to the API. If the content is too long to fit in the context window, notify the user and discuss options (chunking, summarization, etc.) rather than silently truncating.
- **Prefill removed (Fable 5, Claude Fable 5.1, Opus 5, Sonnet 5, and the 4.6/4.7/4.8 family):** Assistant message prefills (last-assistant-turn prefills) return a 400 error on Fable 5, Claude Fable 5.1, Opus 5, Sonnet 5, Opus 4.6, Opus 4.7, Opus 4.8, and Sonnet 4.6. Use structured outputs (`output_config.format`) or system prompt instructions to control response format instead. (One exception: the fallback-credit prefill claim - when redeeming a credit with `fallback_has_prefill_claim: true`, the server accepts the echoed assistant message; see the migration guide's refusal section.)
- **Confirm migration scope before editing:** When a user asks to migrate code to a newer Claude model without naming a specific file, directory, or file list, **ask which scope to apply first** - the entire working directory, a specific subdirectory, or a specific set of files. Do not start editing until the user confirms. Imperative phrasings like "migrate my codebase", "move my project to X", "upgrade to Sonnet 4.6", or bare "migrate to Opus 4.8" are **still ambiguous** - they tell you what to do but not where, so ask. Proceed without asking only when the prompt names an exact file, a specific directory, or an explicit file list ("migrate `app.py`", "migrate everything under `services/`", "update `a.py` and `b.py`"). See `shared/model-migration.md` Step 0.
- **`max_tokens` defaults:** Don't lowball `max_tokens` - hitting the cap truncates output mid-thought and requires a retry. For non-streaming requests, default to `~16000` (keeps responses under SDK HTTP timeouts). For streaming requests, default to `~64000` (timeouts aren't a concern, so give the model room). Only go lower when you have a hard reason: classification (`~256`), cost caps, deliberately short outputs, or **`max_tokens: 0`** for cache pre-warming (see `shared/prompt-caching.md` -> Pre-warming).
- **Disabling thinking on Claude Opus 5 has two failure modes - prefer low/medium effort instead.** Only affects code that explicitly opts out; thinking is on by default, so watch for a disabled-thinking setting carried forward from Opus 4.8. With `thinking: {type: "disabled"}`, the model occasionally writes a tool call into its **visible text** instead of a `tool_use` block: the turn succeeds, the call never runs, no error is raised, and in an agentic loop that text pollutes later turns. It can also leak `<thinking>` tags into the response. Turning thinking on and lowering `effort` fixes both and still cuts cost. If a route must stay thinking-off: **delete** any don't-think/don't-reason rule (it makes tag leakage worse), don't name thinking tags, and add the combined instruction *"When you use a tool, you may say a brief sentence first. If no tool can express what the user asked for, say so instead of guessing. Do not include internal or system XML tags in your response."* Details: `shared/model-migration.md` -> Two failure modes when thinking is disabled.
- **128K output tokens:** Fable 5, Claude Fable 5.1, Opus 5, Opus 4.6, Opus 4.7, Opus 4.8, Sonnet 5, and Sonnet 4.6 support up to 128K `max_tokens`, but the SDKs require streaming for values that large to avoid HTTP timeouts. Use `.stream()` with `.get_final_message()` / `.finalMessage()`.
- **Forced tool use removed (Claude Fable 5.1 / Claude Mythos 5.1, as on Mythos Preview):** `tool_choice: {type: "any"}` and `{type: "tool", name: ...}` return a 400 (`tool_choice: type "tool" and "any" are not supported for this model.`), on `count_tokens` and Batches too. Use `{type: "auto"}` plus an explicit instruction naming the tool, `strict: true` on the tool to keep schema-valid arguments, or structured outputs (`output_config.format`) when the forced call only existed to get JSON back. `{type: "none"}` is unaffected; `disable_parallel_tool_use` still works with `auto` (at most one call).
- **Tool call JSON parsing (Fable 5, Claude Fable 5.1, Opus 5, and the 4.6/4.7/4.8 family):** Fable 5, Claude Fable 5.1, Opus 5, Opus 4.6, Opus 4.7, Opus 4.8, and Sonnet 4.6 may produce different JSON string escaping in tool call `input` fields (e.g., Unicode or forward-slash escaping). Always parse tool inputs with `json.loads()` / `JSON.parse()` - never do raw string matching on the serialized input.
- **Structured outputs (all models):** Use `output_config: {format: {...}}` instead of the deprecated `output_format` parameter on `messages.create()`. This is a general API change, not 4.6-specific.
- **Don't reimplement SDK functionality:** The SDK provides high-level helpers - use them instead of building from scratch. Specifically: use `stream.finalMessage()` instead of wrapping `.on()` events in `new Promise()`; use typed exception classes (`Anthropic.RateLimitError`, etc.) instead of string-matching error messages; use SDK types (`Anthropic.MessageParam`, `Anthropic.Tool`, `Anthropic.Message`, etc.) instead of redefining equivalent interfaces.
- **Error handling - catch a chain, not one broad class.** A single `except APIStatusError` / `catch (AnthropicServiceException)` / `rescue APIError` loses the distinction between retryable (429, >=500, network) and non-retryable (400/404) failures. Write a most-specific-first chain - e.g. `NotFoundError` -> `RateLimitError` -> `APIStatusError` -> `APIConnectionError` (or the Go equivalent: `errors.As` into `*anthropic.Error` then `switch apierr.StatusCode { case 404: ...; case 429: ...; default: ... }`). Per-language class names and namespaces are in `shared/error-codes.md`.
- **Don't research SDK types - write first.** If a type name isn't shown in the documentation included in this skill, write the code file from the namespace/package tables in the language-specific doc and let the compiler's error point you to the right name. Do not spend turns on WebFetch, SDK-repo clones, or compiling-and-running a separate reflection program to discover type names before writing - produce the source file first, then fix what the compiler reports. A quick `strings` / `jar tf` / `javap` against the installed SDK is acceptable for locating names (it returns in seconds), but don't escalate beyond that. A file with a wrong type name is recoverable; a session spent on discovery with no file written is not.
- **Bash and text editor tools are Anthropic-defined, schema-less.** Declare `{"type": "bash_20250124", "name": "bash"}` / `{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}` - no `input_schema`. A custom tool with your own schema named `"bash"` is a different tool. Handler paths and security checks are in `shared/tool-use-concepts.md` § Client-Side Tools.
- **Advisor tool model pairing.** The advisor tool's `model` must be at least as capable as the request's top-level `model` - e.g. executor `claude-sonnet-5` -> advisor `claude-opus-4-8` or `claude-opus-4-7`. An invalid pair returns 400. Pairing table in `shared/tool-use-concepts.md` § Advisor. Availability: `shared/platform-availability.md`.
- **Agent Skills != Managed Agents.** To have Claude generate a `.pptx`/`.xlsx`/etc. via Agent Skills, call `client.beta.messages.create` with `container={"skills": [...]}`, the `code_execution_20260521` tool, and the `code-execution-2025-08-25` beta (Skills is out of beta - no `skills-2025-10-02` header needed). Do not use `client.beta.agents` / `sessions` / `environments` here - those are the Managed Agents surface, not Agent Skills.
- **MCP connector needs both halves.** `mcp_servers=[{type:"url", url, name}]` alone is rejected as a validation error - also add `tools=[{type:"mcp_toolset", mcp_server_name:<same name>}]` with beta `mcp-client-2025-11-20`. Availability: `shared/platform-availability.md`.
- **`inference_geo` is a direct top-level request parameter** - `client.messages.create(..., inference_geo="us")` / `.inferenceGeo("us")`. Do not put it in `extra_body` / `putAdditionalBodyProperty`. (Messages API only - on Managed Agents, `inference_geo` instead nests inside the agent's `model` object, never top-level; see `shared/managed-agents-core.md` § Pinning inference geography.) Supported on Opus 4.6 / Sonnet 4.6 and later; availability: `shared/platform-availability.md`. `response.usage.inference_geo` reports where inference ran.
- **Fine-grained tool streaming is not a beta feature.** Set `eager_input_streaming: true` on the tool definition and call the regular `client.messages.stream(...)`. There is no beta header and no `client.beta.*` path.
- **Cache diagnostics is beta.** Use `client.beta.messages.*` with beta `cache-diagnosis-2026-04-07`. Pass `diagnostics: {previous_message_id: null}` on the first turn and `diagnostics: {previous_message_id: <previous response id>}` on subsequent turns; the result is on `response.diagnostics`. Availability: `shared/platform-availability.md`.
- **Memory tool type is `memory_20250818`.** Declare `{"type": "memory_20250818", "name": "memory"}`. Go uses the beta-namespace type `{OfMemoryTool20250818: &anthropic.BetaMemoryTool20250818Param{}}` on `client.Beta.Messages.New`; Python/TypeScript/Ruby/PHP/C# use the non-beta `client.messages.create`; Java has both a non-beta `MemoryTool20250818` and a beta tool-runner path. Python/TypeScript provide `BetaAbstractMemoryTool` / `betaMemoryTool` helpers for implementing the backend.
- **Use a model the feature actually supports.** Some features are restricted to specific model tiers - fast mode is Claude Opus 5 / Opus 4.8 only (and Claude API only), task budgets (Messages API only - Managed Agents session budgets have no model-tier restriction) are Claude Opus 5 / Fable 5 / Claude Fable 5.1 (confirm at launch) / Sonnet 5 / Opus 4.8 / 4.7 only, and the advisor tool requires a valid executor<->advisor pair. If the user's prompt names a model that the feature doesn't support, use a supported model instead and note the substitution in the output.
- **Don't define custom types for SDK data structures:** The SDK exports types for all API objects. Use `Anthropic.MessageParam` for messages, `Anthropic.Tool` for tool definitions, `Anthropic.ToolUseBlock` / `Anthropic.ToolResultBlockParam` for tool results, `Anthropic.Message` for responses. Defining your own `interface ChatMessage { role: string; content: unknown }` duplicates what the SDK already provides and loses type safety.
- **Report and document output:** For tasks that produce reports, documents, or visualizations, the code execution sandbox has `python-docx`, `python-pptx`, `matplotlib`, `pillow`, and `pypdf` pre-installed. Claude can generate formatted files (DOCX, PDF, charts) and return them via the Files API - consider this for "report" or "document" type requests instead of plain stdout text.
- **Server-tool errors don't raise.** Web search and web fetch errors return HTTP 200 with a `web_search_tool_result` / `web_fetch_tool_result` block whose `content` is a single error object (e.g. `{error_code: "max_uses_exceeded"}`) - not a raised exception. For web search, a success `content` is a *list*; an error `content` is an *object* - branch on that before indexing.
- **Managed Agents web tools ignore the environment's `networking`.** `web_search` / `web_fetch` run on Anthropic's servers in cloud *and* self-hosted environments, and Console org-level web settings apply to the Messages API only. Restrict them per tool with `allowed_domains` **or** `blocked_domains` (never both; 1-64 plain hostnames per list, subdomains covered; IPs, bare TLDs, single-label and `localhost`-style names rejected on both tools; a path suffix is allowed only on `web_search`) on the toolset `configs` entry - `shared/managed-agents-tools.md` § Web search & web fetch settings.
- **Code execution output block type:** `code_execution_20260521` returns `bash_code_execution_tool_result` (with `.content.stdout`), **not** the legacy bare `code_execution_tool_result`. Iterate `response.content` and match on the correct type.
- **Tool search: never defer everything.** The search tool itself must not have `defer_loading: true`, and at least one tool in `tools` must be non-deferred, or the API returns 400 `All tools have defer_loading set`.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Anthropic, PBC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
# Claude API - C#
> **Note:** The C# SDK is the official Anthropic SDK for C#. Tool use is supported via the Messages API with a beta `BetaToolRunner` for automatic tool execution loops. The SDK also supports Microsoft.Extensions.AI IChatClient integration with function invocation and Managed Agents (beta).
## Namespace Reference
Types are organized by namespace. If a type you need isn't shown in an example below, locate it via this table first - don't block on fetching SDK source over the network.
| `using` | Contains |
|---|---|
| `Anthropic` | `AnthropicClient`, top-level options |
| `Anthropic.Models.Messages` | non-beta request/response types - `MessageCreateParams`, `Model`, `Role`, `ContentBlock`, `TextBlock`, `ToolUseBlock`, `ToolResultBlockParam`, `Tool*` (tool definition classes) |
| `Anthropic.Models.Beta.Messages` | beta-endpoint equivalents - `MessageCreateParams`, `BetaMessage`, `BetaTool*`, `Speed`, `BetaRequestMcpServerUrlDefinition`, context-editing/compaction configs |
| `Anthropic.Models.Beta` | shared beta constants |
| `Anthropic.Models.Beta.Files` | Files API types |
| `Anthropic.Models.Messages.Batches` | Batch API types |
| `Anthropic.Helpers.Beta` | `BetaToolRunner`, beta helper utilities |
| `Anthropic.Exceptions` | `AnthropicApiException`, `AnthropicRateLimitException`, `Anthropic5xxException`, etc. - see `shared/error-codes.md` |
| `Anthropic.Bedrock` / `Anthropic.Vertex` / `Anthropic.Foundry` / `Anthropic.Aws` | platform clients (separate NuGet packages): `AnthropicBedrockMantleClient`, `AnthropicFoundryClient`, `AnthropicAwsClient` |
`client.Messages.*` uses non-beta types; `client.Beta.Messages.*` uses the `Anthropic.Models.Beta.Messages` types. Both namespaces define a `MessageCreateParams` - pick the one matching the client path you call.
### Key types per feature
Write from this table instead of reflecting the SDK assembly. Endpoint column tells you whether to use `client.Messages.*` or `client.Beta.Messages.*`.
| Feature | Endpoint | Key C# types (namespace per table above) |
|---|---|---|
| User profiles | beta | `client.Beta.UserProfiles.Create(...)` / `.Retrieve(id)` / `.List()`. Pass the returned profile id on the beta messages call. Requires a beta header - check the SDK's beta-headers reference for the current flag. |
| Agent Skills | beta | `BetaContainerParams` (with `Skills = [new BetaSkillParams { ... }]`), `BetaCodeExecutionTool20250825`. `Betas = ["code-execution-2025-08-25"]` (Skills is out of beta - no `skills-2025-10-02`). Download the output via `client.Beta.Files.Download(fileId)`. |
| Advisor tool | beta | `BetaAdvisorTool20260301` - may not be in all SDK releases yet |
| Cache diagnostics | beta | `Diagnostics = new() { PreviousMessageID = ... }`, `BetaCacheControlEphemeral`, `BetaContentBlockParam` |
| Context editing | beta | `ContextManagement = new BetaContextManagementConfig { Edits = [new BetaClearToolUses20250919Edit()] }`. `Betas = ["context-management-2025-06-27"]` (not `compact-2026-01-12` - that's for `BetaCompact20260112Edit`). |
| Memory tool | non-beta | `Tools = [new ToolUnion(new MemoryTool20250818())]` |
| Programmatic tool calling | non-beta | `CodeExecutionTool20260120`, `ToolResultBlockParam`, `ContentBlockParam` |
| Task budgets | beta | `BetaOutputConfig` with `TaskBudget = new BetaTokenTaskBudget { ... }` |
| Tool search | non-beta | `new ToolUnion(new ToolSearchToolRegex20251119 { Type = ToolSearchToolRegex20251119Type.ToolSearchToolRegex20251119 })` - `Type` must be set explicitly. |
| Web search | non-beta | `new ToolUnion(new WebSearchTool20260209())` - the latest variant with dynamic filtering (Claude Fable 5.1 + Claude Opus 5 + Opus 4.8/4.7/4.6 + Claude Sonnet 5 + Sonnet 4.6). For older models or Vertex, use `WebSearchTool20250305()` |
### Discovering type and member names
If a type or member you need isn't in the tables above, `strings ~/.nuget/packages/anthropic/*/lib/*/Anthropic.dll | grep -i <term>` is fast and sufficient for locating class and property names. **Do not escalate to a `dotnet run` reflection probe** to dump members precisely - the first compile is slow enough to be backgrounded in many environments, trapping you in a polling loop. Instead, write `Program.cs` using the names `strings | grep` found; if a member name is wrong the compiler error (`error CS1061: 'X' does not contain a definition for 'Y'`) points at it in a few seconds, faster than any reflection probe.
Note that `strings` will not surface wire-format snake_case field names (`output_tokens`, `stop_reason`) - those are stored in the DLL differently. **C# properties are the PascalCase equivalent of the wire field** (`response.Usage.OutputTokens`, `response.StopReason`). If you know the wire field name from the docs, write the PascalCase property and compile; do not probe for the snake_case string.
### Minimal working skeleton
**Write a plain `Program.cs` body** - `using` statements followed by top-level statements, as below. Do **not** add a `#!/usr/bin/env dotnet` shebang or `#:package Anthropic@*` directive: those are .NET file-based-app syntax and fail with `CS1024: Preprocessor directive expected` when the file is compiled via an existing `.csproj`. The standard project setup (per the [C# quickstart](https://platform.claude.com/docs/en/get-started): `dotnet new console` -> `dotnet add package Anthropic` -> edit `Program.cs` -> `dotnet run`) provides the `.csproj` and package reference.
Start from this - it compiles as-is. Fill in the feature-specific fields; do not spend turns running reflection or XML-doc inspection to discover type names first.
```csharp
using System;
using Anthropic;
using Anthropic.Models.Messages; // or Anthropic.Models.Beta.Messages for beta endpoints
AnthropicClient client = new();
var message = await client.Messages.Create(new MessageCreateParams
{
Model = "claude-opus-5",
MaxTokens = 1024,
Messages = [ new() { Role = Role.User, Content = "Hello, Claude" } ],
});
Console.WriteLine(message);
```
For beta features (anything behind an `anthropic-beta` header), use the beta client path and namespace - same overall shape:
```csharp
using System;
using Anthropic;
using Anthropic.Models.Beta.Messages;
AnthropicClient client = new();
var response = await client.Beta.Messages.Create(new MessageCreateParams
{
Model = "claude-opus-5",
MaxTokens = 4096,
Betas = ["<beta-flag>"],
Messages = [ new() { Role = Role.User, Content = "..." } ],
// Tools = new BetaToolUnion[] { new BetaSomeTool { ... } }, // for tool features
});
Console.WriteLine(response);
```
If a type name the feature needs isn't in this file, write it following the naming pattern in the Namespace Reference above and fix from compiler output - producing a `Program.cs` and iterating beats researching.
### Common C# compile errors
- **CS8803 (top-level statements must precede type declarations):** put any `record`/`class`/`struct` definitions **after** the last top-level statement, at the end of the file. A record defined above `var client = new AnthropicClient()` will not compile.
- **`await foreach` on a `Task<...Page>`:** `client.Models.List()` returns a `Task<ModelListPage>`, which is not directly async-enumerable. Await it first, then iterate: `var page = await client.Models.List(); foreach (var m in page.Items) {...}`. For auto-pagination, check whether the page type exposes `AutoPagingEachAsync()` or similar before reaching for `await foreach`.
## Installation
```bash
dotnet add package Anthropic
```
## Client Initialization
```csharp
using Anthropic;
// Default (uses ANTHROPIC_API_KEY env var)
AnthropicClient client = new();
// Explicit API key (use environment variables - never hardcode keys)
AnthropicClient client = new() {
ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")
};
```
---
## Basic Message Request
```csharp
using Anthropic.Models.Messages;
var parameters = new MessageCreateParams
{
Model = "claude-opus-5",
MaxTokens = 16000,
Messages = [new() { Role = Role.User, Content = "What is the capital of France?" }]
};
var response = await client.Messages.Create(parameters);
// ContentBlock is a union wrapper. .Value unwraps to the variant object,
// then OfType<T> filters to the type you want. Or use the TryPick* idiom
// shown in the Thinking section below.
foreach (var text in response.Content.Select(b => b.Value).OfType<TextBlock>())
{
Console.WriteLine(text.Text);
}
```
---
## Thinking
**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think.
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking (below). `new ThinkingConfigEnabled { BudgetTokens = N }` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `Thinking` runs adaptive (`ThinkingConfigAdaptive` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `ThinkingConfigDisabled` is accepted only at effort `high` or lower; pairing it with `xhigh`/`max` returns a 400.
> **Older models:** Use `new ThinkingConfigEnabled { BudgetTokens = N }` (budget must be < `MaxTokens`, min 1024).
```csharp
using Anthropic.Models.Messages;
var response = await client.Messages.Create(new MessageCreateParams
{
Model = "claude-opus-5",
MaxTokens = 16000,
// ThinkingConfigParam? implicitly converts from the concrete variant classes -
// no wrapper needed.
// display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Claude Opus 5 / Opus 4.8 / 4.7
Thinking = new ThinkingConfigAdaptive { Display = Display.Summarized },
Messages =
[
new() { Role = Role.User, Content = "Solve: 27 * 453" },
],
});
// ThinkingBlock(s) precede TextBlock in Content. TryPick* narrows the union.
foreach (var block in response.Content)
{
if (block.TryPickThinking(out ThinkingBlock? t))
{
Console.WriteLine($"[thinking] {t.Thinking}");
}
else if (block.TryPickText(out TextBlock? text))
{
Console.WriteLine(text.Text);
}
}
```
Alternative to `TryPick*`: `.Select(b => b.Value).OfType<ThinkingBlock>()` (same LINQ pattern as the Basic Message example).
---
## Context Editing / Compaction (Beta)
**Beta-namespace prefix is inconsistent** (source-verified against `src/Anthropic/Models/Beta/Messages/*.cs` @ 12.9.0). No prefix: `MessageCreateParams`, `MessageCountTokensParams`, `Role`, `Speed`. **Everything else has the `Beta` prefix**: `BetaMessageParam`, `BetaMessage`, `BetaContentBlock`, `BetaToolUseBlock`, all block param types. The unprefixed `Role` WILL collide with `Anthropic.Models.Messages.Role` if you import both namespaces (CS0104). Safest: import only Beta; if mixing, alias the beta `Role`:
```csharp
using Anthropic.Models.Beta.Messages;
using NonBeta = Anthropic.Models.Messages; // only if you also need non-beta types
// Now: MessageCreateParams, BetaMessageParam, Role (beta's), NonBeta.Role (if needed)
```
`BetaMessage.Content` is `IReadOnlyList<BetaContentBlock>` - a 15-variant discriminated union. Narrow with `TryPick*`. **Response `BetaContentBlock` is NOT assignable to param `BetaContentBlockParam`** - there's no `.ToParam()` in C#. Round-trip by converting each block:
```csharp
using Anthropic.Models.Beta.Messages;
var betaParams = new MessageCreateParams // no Beta prefix - see unprefixed list above
{
Model = "claude-opus-5",
MaxTokens = 16000,
Betas = ["compact-2026-01-12"],
ContextManagement = new BetaContextManagementConfig
{
Edits = [new BetaCompact20260112Edit()],
},
Messages = messages,
};
BetaMessage resp = await client.Beta.Messages.Create(betaParams);
foreach (BetaContentBlock block in resp.Content)
{
if (block.TryPickCompaction(out BetaCompactionBlock? compaction))
{
// Content is nullable - compaction can fail server-side
Console.WriteLine($"compaction summary: {compaction.Content}");
}
}
// Context-edit metadata lives on a separate nullable field
if (resp.ContextManagement is { } ctx)
{
foreach (var edit in ctx.AppliedEdits)
Console.WriteLine($"cleared {edit.ClearedInputTokens} tokens");
}
// ROUND-TRIP: BetaMessageParam.Content is BetaMessageParamContent (a string|list
// union). It implicit-converts from List<BetaContentBlockParam>, NOT from the
// response's IReadOnlyList<BetaContentBlock>. Convert each block:
List<BetaContentBlockParam> paramBlocks = [];
foreach (var b in resp.Content)
{
if (b.TryPickText(out var t)) paramBlocks.Add(new BetaTextBlockParam { Text = t.Text });
else if (b.TryPickCompaction(out var c)) paramBlocks.Add(new BetaCompactionBlockParam { Content = c.Content });
// ... other variants as needed
}
messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = paramBlocks });
```
All 15 `BetaContentBlock.TryPick*` variants: `Text`, `Thinking`, `RedactedThinking`, `ToolUse`, `ServerToolUse`, `WebSearchToolResult`, `WebFetchToolResult`, `CodeExecutionToolResult`, `BashCodeExecutionToolResult`, `TextEditorCodeExecutionToolResult`, `ToolSearchToolResult`, `McpToolUse`, `McpToolResult`, `ContainerUpload`, `Compaction`.
**`BetaToolUseBlock.Input` is `IReadOnlyDictionary<string, JsonElement>`** - index by key then call the `JsonElement` extractor:
```csharp
if (block.TryPickToolUse(out BetaToolUseBlock? tu))
{
int a = tu.Input["a"].GetInt32();
string s = tu.Input["name"].GetString()!;
}
```
---
## Effort Parameter
Effort is nested under `OutputConfig`, NOT a top-level property. `ApiEnum<string, Effort>` has an implicit conversion from the enum, so assign `Effort.High` directly.
```csharp
OutputConfig = new OutputConfig { Effort = Effort.High },
```
Values: `Effort.Low`, `Effort.Medium`, `Effort.High`, `Effort.Max`. Combine with `Thinking = new ThinkingConfigAdaptive()` for cost-quality control.
---
## Prompt Caching
`System` takes `MessageCreateParamsSystem?` - a union of `string` or `List<TextBlockParam>`. There is no `SystemTextBlockParam`; use plain `TextBlockParam`. The implicit conversion needs the concrete `List<TextBlockParam>` type (array literals won't convert). For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`.
```csharp
System = new List<TextBlockParam> {
new() {
Text = longSystemPrompt,
CacheControl = new CacheControlEphemeral(), // auto-sets Type = "ephemeral"
},
},
```
Optional `Ttl` on `CacheControlEphemeral`: `new() { Ttl = Ttl.Ttl1h }` or `Ttl.Ttl5m`. `CacheControl` also exists on `Tool.CacheControl` and top-level `MessageCreateParams.CacheControl`.
Verify hits via `response.Usage.CacheCreationInputTokens` / `response.Usage.CacheReadInputTokens`.
---
## Token Counting
```csharp
MessageTokensCount result = await client.Messages.CountTokens(new MessageCountTokensParams {
Model = "claude-opus-5",
Messages = [new() { Role = Role.User, Content = "Hello" }],
});
long tokens = result.InputTokens;
```
`MessageCountTokensParams.Tools` uses a different union type (`MessageCountTokensTool`) than `MessageCreateParams.Tools` (`ToolUnion`) - if you're passing tools, the compiler will tell you when it matters.
---
## PDF / Document Input
`DocumentBlockParam` takes a `DocumentBlockParamSource` union: `Base64PdfSource` / `UrlPdfSource` / `PlainTextSource` / `ContentBlockSource`. `Base64PdfSource` auto-sets `MediaType = "application/pdf"` and `Type = "base64"`.
```csharp
new MessageParam {
Role = Role.User,
Content = new List<ContentBlockParam> {
new DocumentBlockParam { Source = new Base64PdfSource { Data = base64String } },
new TextBlockParam { Text = "Summarize this PDF" },
},
}
```
---
## Fast Mode (Beta)
```csharp
var response = await client.Beta.Messages.Create(new MessageCreateParams {
Model = "claude-opus-5", MaxTokens = 4096,
Speed = Speed.Fast,
Betas = ["fast-mode-2026-02-01"],
Messages = [...],
});
```
## Models API
```csharp
var page = await client.Models.List();
foreach (var m in page.Items) { Console.WriteLine(m.ID); }
var one = await client.Models.Retrieve("claude-opus-5");
```
## Long Output (128k) + Prefill
Set `MaxTokens = 128000` on `client.Messages` and use the streaming path (see `streaming.md`). On Claude 4+ models, 128k output is native - no `output-128k-*` beta header or beta namespace is needed.
**Prefilling the assistant message** (putting a trailing `Role.Assistant` message in the input) is **not supported** on Claude Fable 5.1, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6 - requests return a 400. Use `OutputConfig.Format` (structured outputs) instead.
## Stop Details
When `StopReason` is `"refusal"`, the response includes structured `StopDetails`:
```csharp
if (response.StopReason == "refusal" && response.StopDetails is { } details)
{
Console.WriteLine($"Category: {details.Category}");
Console.WriteLine($"Explanation: {details.Explanation}");
}
```
**Refusal fallbacks (Claude Fable 5.1) - opt in by default.** Fallbacks are opt-in: without them a refused request simply stops. In `claude-fable-5-1` code, include `Fallbacks = [new(Model.ClaudeOpus4_8)]` plus the `AnthropicBeta.ServerSideFallback2026_06_01` beta on the beta messages call by default - a policy decline is re-served by the fallback model inside the same call. Not available on Amazon Bedrock, Vertex AI, or Microsoft Foundry - use the client-side handler there: `new AnthropicClient { Handlers = [new BetaRefusalFallbackHandler { Fallbacks = [new(Model.ClaudeOpus4_8)] }] }` (namespace `Anthropic.Helpers`), with per-conversation state via `BetaFallbackState.Create()` scoped with `using (fallbackState.Use()) { ... }`. Full semantics (billing, sticky routing, streaming) and a runnable example: `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason, and the C# SDK repo's `examples/` (WebFetch via `shared/live-sources.md`).
---
## Managed Agents (Beta)
The C# SDK supports Managed Agents via `client.Beta.Agents`, `client.Beta.Sessions`, `client.Beta.Environments`, and related namespaces. See `shared/managed-agents-overview.md` for the architecture and `curl/managed-agents.md` for the wire-level reference.
# Message Batches - C#
## Message Batches API
```csharp
var batch = await client.Messages.Batches.Create(new() {
Requests = [
new() { CustomID = "req-1", Params = new() { Model = "claude-opus-5", MaxTokens = 1024, Messages = [...] } },
],
});
// Poll client.Messages.Batches.Retrieve(batch.ID) until ProcessingStatus == "ended",
// then iterate client.Messages.Batches.Results(batch.ID).
```
# Files API - C#
## Files API
> **Out of beta.** In current SDKs `client.Beta.Files` has breaking shape changes from previous versions, matching the stable `client.Files` - migrate per the Files API row in `shared/live-sources.md`. Examples below predate this.
Files live under `client.Beta.Files` (namespace `Anthropic.Models.Beta.Files`). `BinaryContent` implicit-converts from `Stream` and `byte[]`.
```csharp
using Anthropic.Models.Beta.Files;
using Anthropic.Models.Beta.Messages;
FileMetadata meta = await client.Beta.Files.Upload(
new FileUploadParams { File = File.OpenRead("doc.pdf") });
// Referencing the uploaded file requires Beta message types:
new BetaRequestDocumentBlock {
Source = new BetaFileDocumentSource { FileID = meta.ID },
}
```
The non-beta `DocumentBlockParamSource` union has no file-ID variant - file references need `client.Beta.Messages.Create()`.
---
# Streaming - C#
## Streaming
```csharp
using Anthropic.Models.Messages;
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus4_8,
MaxTokens = 64000,
Messages = [new() { Role = Role.User, Content = "Write a haiku" }]
};
await foreach (RawMessageStreamEvent streamEvent in client.Messages.CreateStreaming(parameters))
{
if (streamEvent.TryPickContentBlockDelta(out var delta) &&
delta.Delta.TryPickText(out var text))
{
Console.Write(text.Text);
}
}
```
**`RawMessageStreamEvent` TryPick methods** (naming drops the `Message`/`Raw` prefix): `TryPickStart`, `TryPickDelta`, `TryPickStop`, `TryPickContentBlockStart`, `TryPickContentBlockDelta`, `TryPickContentBlockStop`. There is no `TryPickMessageStop` - use `TryPickStop`.
---
# Tool Use - C#
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Use
### Defining a tool
`Tool` (NOT `ToolParam`) with an `InputSchema` record. `InputSchema.Type` is auto-set to `"object"` by the constructor - don't set it. `ToolUnion` has an implicit conversion from `Tool`, triggered by the collection expression `[...]`.
```csharp
using System.Text.Json;
using Anthropic.Models.Messages;
var parameters = new MessageCreateParams
{
Model = Model.ClaudeSonnet4_6,
MaxTokens = 16000,
Tools = [
new Tool {
Name = "get_weather",
Description = "Get the current weather in a given location",
InputSchema = new() {
Properties = new Dictionary<string, JsonElement> {
["location"] = JsonSerializer.SerializeToElement(
new { type = "string", description = "City name" }),
},
Required = ["location"],
},
},
],
Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }],
};
```
Derived from `anthropic-sdk-csharp/src/Anthropic/Models/Messages/Tool.cs` and `ToolUnion.cs:799` (implicit conversion).
See [shared tool use concepts](../../shared/tool-use-concepts.md) for the loop pattern.
### Converting response content to the follow-up assistant message
When echoing Claude's response back in the assistant turn, **there is no `.ToParam()` helper** - manually reconstruct each `ContentBlock` variant as its `*Param` counterpart. Do NOT use `new ContentBlockParam(block.Json)`: it compiles and serializes, but `.Value` stays `null` so `TryPick*`/`Validate()` fail (degraded JSON pass-through, not the typed path).
```csharp
using Anthropic.Models.Messages;
Message response = await client.Messages.Create(parameters);
// No .ToParam() - reconstruct per variant. Implicit conversions from each
// *Param type to ContentBlockParam mean no explicit wrapper.
List<ContentBlockParam> assistantContent = [];
List<ContentBlockParam> toolResults = [];
foreach (ContentBlock block in response.Content)
{
if (block.TryPickText(out TextBlock? text))
{
assistantContent.Add(new TextBlockParam { Text = text.Text });
}
else if (block.TryPickThinking(out ThinkingBlock? thinking))
{
// Signature MUST be preserved - the API rejects tampering
assistantContent.Add(new ThinkingBlockParam
{
Thinking = thinking.Thinking,
Signature = thinking.Signature,
});
}
else if (block.TryPickRedactedThinking(out RedactedThinkingBlock? redacted))
{
assistantContent.Add(new RedactedThinkingBlockParam { Data = redacted.Data });
}
else if (block.TryPickToolUse(out ToolUseBlock? toolUse))
{
// ToolUseBlock has required Caller; ToolUseBlockParam.Caller is optional - don't copy it
assistantContent.Add(new ToolUseBlockParam
{
ID = toolUse.ID,
Name = toolUse.Name,
Input = toolUse.Input,
});
// Execute the tool; collect ONE result per tool_use block - the API
// rejects the follow-up if any tool_use ID lacks a matching tool_result.
string result = ExecuteYourTool(toolUse.Name, toolUse.Input);
toolResults.Add(new ToolResultBlockParam
{
ToolUseID = toolUse.ID,
Content = result,
});
}
}
// Follow-up: prior messages + assistant echo + user tool_result(s)
List<MessageParam> followUpMessages =
[
.. parameters.Messages,
new() { Role = Role.Assistant, Content = assistantContent },
new() { Role = Role.User, Content = toolResults },
];
```
`ToolResultBlockParam` has no tuple constructor - use the object initializer. `Content` is a string-or-list union; a plain `string` implicitly converts.
---
## Structured Output
```csharp
OutputConfig = new OutputConfig {
Format = new JsonOutputFormat {
Schema = new Dictionary<string, JsonElement> {
["type"] = JsonSerializer.SerializeToElement("object"),
["properties"] = JsonSerializer.SerializeToElement(
new { name = new { type = "string" } }),
["required"] = JsonSerializer.SerializeToElement(new[] { "name" }),
},
},
},
```
`JsonOutputFormat.Type` is auto-set to `"json_schema"` by the constructor. `Schema` is `required`.
---
## Anthropic-Defined Tools
Web search, bash, text editor, and code execution are Anthropic-defined tools with built-in schemas. Web search and code execution are server-executed; bash and text editor are client-executed (you handle the `tool_use` locally - see `shared/tool-use-concepts.md`). Type names are version-suffixed; constructors auto-set `name`/`type`. **Wrap each in `new ToolUnion(...)` explicitly.**
```csharp
Tools = [
new ToolUnion(new WebSearchTool20260209()),
new ToolUnion(new ToolBash20250124()),
new ToolUnion(new ToolTextEditor20250728()),
new ToolUnion(new CodeExecutionTool20260120()),
],
```
Also available: `new ToolUnion(new WebFetchTool20260209())`, `new ToolUnion(new MemoryTool20250818())`. `WebSearchTool20260209` optionals: `AllowedDomains`, `BlockedDomains`, `MaxUses`, `UserLocation`.
---
## Tool Runner (Beta)
The C# SDK provides a `BetaToolRunner` for automatic tool execution loops. Define tools with raw JSON schemas, and the runner handles the API call -> tool execution -> result feedback loop.
```csharp
using Anthropic.Models.Beta.Messages;
// Define tools and create params as shown in the Tool Use section above,
// but using the beta namespace types (BetaToolUnion, etc.)
var runner = client.Beta.Messages.ToolRunner(betaParams);
await foreach (BetaMessage message in runner)
{
foreach (var block in message.Content)
{
if (block.TryPickText(out var text))
{
Console.WriteLine(text.Text);
}
}
}
```
---
# Claude API - cURL / Raw HTTP
Use these examples when the user needs raw HTTP requests or is working in a language without an official SDK.
## Setup
```bash
export ANTHROPIC_API_KEY="your-api-key"
```
---
## Basic Message Request
```bash
curl https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 16000,
"messages": [
{"role": "user", "content": "What is the capital of France?"}
]
}'
```
### Parsing the response
Use `jq` to extract fields from the JSON response. Do not use `grep`/`sed` -
JSON strings can contain any character and regex parsing will break on quotes,
escapes, or multi-line content.
```bash
# Capture the response, then extract fields
response=$(curl -s https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-opus-5","max_tokens":16000,"messages":[{"role":"user","content":"Hello"}]}')
# Print the first text block (-r strips the JSON quotes)
echo "$response" | jq -r '.content[0].text'
# Read usage fields
input_tokens=$(echo "$response" | jq -r '.usage.input_tokens')
output_tokens=$(echo "$response" | jq -r '.usage.output_tokens')
# Read stop reason (for tool-use loops)
stop_reason=$(echo "$response" | jq -r '.stop_reason')
# Extract all text blocks (content is an array; filter to type=="text")
echo "$response" | jq -r '.content[] | select(.type == "text") | .text'
```
---
## Streaming (SSE)
```bash
curl https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 64000,
"stream": true,
"messages": [{"role": "user", "content": "Write a haiku"}]
}'
```
The response is a stream of Server-Sent Events:
```
event: message_start
data: {"type":"message_start","message":{"id":"msg_...","type":"message",...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}}
event: message_stop
data: {"type":"message_stop"}
```
---
## Tool Use
```bash
curl https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 16000,
"tools": [{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"]
}
}],
"messages": [{"role": "user", "content": "What is the weather in Paris?"}]
}'
```
When Claude responds with a `tool_use` block, send the result back:
```bash
curl https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 16000,
"tools": [{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"]
}
}],
"messages": [
{"role": "user", "content": "What is the weather in Paris?"},
{"role": "assistant", "content": [
{"type": "text", "text": "Let me check the weather."},
{"type": "tool_use", "id": "toolu_abc123", "name": "get_weather", "input": {"location": "Paris"}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_abc123", "content": "72°F and sunny"}
]}
]
}'
```
---
## Prompt Caching
Put `cache_control` on the last block of the stable prefix. See `shared/prompt-caching.md` for placement patterns and the silent-invalidator audit checklist.
```bash
curl https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 16000,
"system": [
{"type": "text", "text": "<large shared prompt...>", "cache_control": {"type": "ephemeral"}}
],
"messages": [{"role": "user", "content": "Summarize the key points"}]
}'
```
For 1-hour TTL: `"cache_control": {"type": "ephemeral", "ttl": "1h"}`. Top-level `"cache_control"` on the request body auto-places on the last cacheable block. Verify hits via the response `usage.cache_creation_input_tokens` / `usage.cache_read_input_tokens` fields.
---
## Extended Thinking
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking. `budget_tokens` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `"thinking"` runs adaptive (`{"type": "adaptive"}` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `{"type": "disabled"}` is accepted only at effort `high` or lower; pairing it with `xhigh`/`max` returns a 400.
> **Older models:** Use `"type": "enabled"` with `"budget_tokens": N` (must be < `max_tokens`, min 1024).
```bash
# Fable 5 / Claude Opus 5 / Opus 4.8 / 4.7 / 4.6: adaptive thinking (recommended)
curl https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 16000,
"thinking": {
"type": "adaptive",
"display": "summarized"
},
"output_config": {
"effort": "high"
},
"messages": [{"role": "user", "content": "Solve this step by step..."}]
}'
```
---
## Refusal Fallbacks (Claude Fable 5.1) - opt in by default
On `claude-fable-5-1`, safety classifiers may decline a request (HTTP 200 with `stop_reason: "refusal"`). Fallbacks are **opt-in**: without them the request simply stops. Include the `fallbacks` parameter and its beta header by default - on a policy decline the API re-runs the same request on the fallback model inside the same call. A decline before any output isn't billed (a mid-stream decline bills the streamed partial); the rescue bills at the fallback model's own rates.
```bash
response=$(curl -s https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: server-side-fallback-2026-06-01" \
-d '{
"model": "claude-fable-5-1",
"max_tokens": 16000,
"fallbacks": [{"model": "claude-opus-4-8"}],
"messages": [{"role": "user", "content": "Hello"}]
}')
# Which model produced the message
echo "$response" | jq -r '.model'
# Refusal on the final response means the whole chain refused
echo "$response" | jq -r '.stop_reason'
# Switch points: one fallback block per model that ran and declined this turn
echo "$response" | jq -r '.content[] | select(.type == "fallback") | "\(.from.model) declined; \(.to.model) continued"'
# Served-by signal - covers sticky turns, which carry no fallback block.
# Pair with stop_reason: the fallback model can itself refuse.
if [ "$(echo "$response" | jq -r '.stop_reason')" != "refusal" ] && \
echo "$response" | jq -e '[.usage.iterations[]? | select(.type == "fallback_message")] | length > 0' > /dev/null; then
echo "fallback model served this turn"
fi
```
The header must be exactly `server-side-fallback-2026-06-01` **for this array form**; the newer `fallbacks: "default"` scalar form uses `server-side-fallback-2026-07-01` instead (see `shared/model-migration.md` -> Migrating to Claude Opus 5 -> New API features), and pairing either header with the other form returns a 400. The parameter is rejected on the Batches API and unavailable on Amazon Bedrock, Vertex AI, and Microsoft Foundry. Full semantics (sticky routing, billing, streaming, echoing fallback turns back): `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason.
---
## Required Headers
| Header | Value | Description |
| ------------------- | ------------------ | -------------------------- |
| `Content-Type` | `application/json` | Required |
| `x-api-key` | Your API key | Authentication |
| `anthropic-version` | `2023-06-01` | API version |
| `anthropic-beta` | Beta feature IDs | Required for beta features |
# Managed Agents - cURL / Raw HTTP
Use these examples when the user needs raw HTTP requests or is working without an SDK.
## Setup
```bash
export ANTHROPIC_API_KEY="your-api-key"
# Common headers
HEADERS=(
-H "Content-Type: application/json"
-H "x-api-key: $ANTHROPIC_API_KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: managed-agents-2026-04-01"
)
```
---
## Create an Environment
```bash
curl -X POST https://api.anthropic.com/v1/environments \
"${HEADERS[@]}" \
-d '{
"name": "my-dev-env",
"config": {
"type": "cloud",
"networking": { "type": "unrestricted" }
}
}'
```
### With restricted networking
```bash
curl -X POST https://api.anthropic.com/v1/environments \
"${HEADERS[@]}" \
-d '{
"name": "restricted-env",
"config": {
"type": "cloud",
"networking": {
"type": "limited",
"allow_package_managers": true,
"allow_mcp_servers": true,
"allowed_hosts": ["api.example.com"]
}
}
}'
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** Under `managed-agents-2026-04-01`, `model`/`system`/`tools` are top-level fields on `POST /v1/agents`, not on the session. Always create the agent first - the session only takes `"agent": {"type": "agent", "id": "..."}`.
### Minimal
```bash
# 1. Create the agent
curl -X POST https://api.anthropic.com/v1/agents \
"${HEADERS[@]}" \
-d '{
"name": "Coding Assistant",
"model": "claude-opus-5",
"tools": [{ "type": "agent_toolset_20260401" }]
}'
# -> { "id": "agent_abc123", ... }
# 2. Start a session
curl -X POST https://api.anthropic.com/v1/sessions \
"${HEADERS[@]}" \
-d '{
"agent": { "type": "agent", "id": "agent_abc123", "version": 1 },
"environment_id": "env_abc123"
}'
# -> { "id": "sesn_abc123", ... }
# Trace: https://platform.claude.com/workspaces/default/sessions/sesn_abc123 (swap 'default' for your workspace ID if the API key is not in the Default workspace)
```
### With system prompt, custom tools, and GitHub repo
```bash
# 1. Create the agent
curl -X POST https://api.anthropic.com/v1/agents \
"${HEADERS[@]}" \
-d '{
"name": "Code Reviewer",
"model": "claude-opus-5",
"system": "You are a senior code reviewer. Be thorough and constructive.",
"tools": [
{ "type": "agent_toolset_20260401" },
{
"type": "custom",
"name": "run_linter",
"description": "Run the project linter on a file",
"input_schema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Path to lint" }
},
"required": ["file_path"]
}
}
]
}'
# 2. Start a session with the repo mounted
curl -X POST https://api.anthropic.com/v1/sessions \
"${HEADERS[@]}" \
-d '{
"agent": { "type": "agent", "id": "agent_abc123", "version": 1 },
"environment_id": "env_abc123",
"title": "Code review session",
"resources": [
{
"type": "github_repository",
"url": "https://github.com/owner/repo",
"mount_path": "/workspace/repo",
"authorization_token": "ghp_...",
"branch": "feature-branch"
}
]
}'
```
### With a session budget
```bash
# Create a session with a hard $25.00 spend cap (list-priced; USD only; create-only).
# amount is in minor units (cents) as an integer string: "2500" = $25.00
curl -X POST https://api.anthropic.com/v1/sessions \
"${HEADERS[@]}" \
-d '{
"agent": { "type": "agent", "id": "agent_abc123" },
"environment_id": "env_abc123",
"budget": {
"type": "limit",
"max_list_cost": { "amount": "2500", "currency": "USD" }
}
}'
# Change the cap - higher or lower, but it must exceed the consumed list cost.
# An accepted update resumes work paused at budget_reached
curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID \
"${HEADERS[@]}" \
-d '{ "budget": { "type": "limit", "max_list_cost": { "amount": "4000", "currency": "USD" } } }'
# Remove the cap entirely - one-way; a removed budget can never be re-added
curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID \
"${HEADERS[@]}" \
-d '{ "budget": null }'
```
See `shared/managed-agents-core.md` § Session budgets for list-cost composition, the settle-event allowlist at the cap, and multiagent semantics.
---
## Send a User Message
```bash
curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID/events \
"${HEADERS[@]}" \
-d '{
"events": [
{
"type": "user.message",
"content": [{ "type": "text", "text": "Review the auth module for security issues" }]
}
]
}'
```
---
## Stream Events (SSE)
```bash
curl -N https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream \
"${HEADERS[@]}"
```
Response format:
```
event: session.status_running
data: {"type":"session.status_running","id":"sevt_...","processed_at":"..."}
event: agent.message
data: {"type":"agent.message","id":"sevt_...","content":[{"type":"text","text":"I'll review..."}],"processed_at":"..."}
event: session.status_idle
data: {"type":"session.status_idle","id":"sevt_...","processed_at":"..."}
```
---
## Poll Events
```bash
# Get all events
curl https://api.anthropic.com/v1/sessions/$SESSION_ID/events \
"${HEADERS[@]}"
# Paginated - get next page of events
curl "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?page=page_abc123" \
"${HEADERS[@]}"
```
---
## Provide Custom Tool Result
When the agent calls a custom tool, send the result back:
```bash
curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID/events \
"${HEADERS[@]}" \
-d '{
"events": [
{
"type": "user.custom_tool_result",
"custom_tool_use_id": "sevt_abc123",
"content": [{ "type": "text", "text": "No linting errors found." }]
}
]
}'
```
---
## Interrupt a Running Session
```bash
curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID/events \
"${HEADERS[@]}" \
-d '{
"events": [
{
"type": "user.interrupt"
}
]
}'
```
---
## Get Session Details
```bash
curl https://api.anthropic.com/v1/sessions/$SESSION_ID \
"${HEADERS[@]}"
```
---
## List Sessions
```bash
curl https://api.anthropic.com/v1/sessions \
"${HEADERS[@]}"
```
---
## Delete a Session
```bash
curl -X DELETE https://api.anthropic.com/v1/sessions/$SESSION_ID \
"${HEADERS[@]}"
```
---
## Upload a File
```bash
curl -X POST https://api.anthropic.com/v1/files \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14" \
-F "file=@path/to/file.txt" \
-F "purpose=agent"
```
---
## List and Download Session Files
List files the agent wrote to `/mnt/session/outputs/` during a session, then download them.
```bash
# List files associated with a session
curl "https://api.anthropic.com/v1/files?scope_id=$SESSION_ID" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14,managed-agents-2026-04-01"
# Download a specific file
curl "https://api.anthropic.com/v1/files/$FILE_ID/content" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14,managed-agents-2026-04-01" \
-o downloaded_file.txt
```
---
## List Agents
```bash
curl https://api.anthropic.com/v1/agents \
"${HEADERS[@]}"
```
---
## MCP Server Integration
```bash
# 1. Agent declares MCP server (no auth here - auth goes in a vault)
curl -X POST https://api.anthropic.com/v1/agents \
"${HEADERS[@]}" \
-d '{
"name": "MCP Agent",
"model": "claude-opus-5",
"mcp_servers": [
{ "type": "url", "name": "my-tools", "url": "https://my-mcp-server.example.com/sse" }
],
"tools": [
{ "type": "agent_toolset_20260401" },
{ "type": "mcp_toolset", "mcp_server_name": "my-tools" }
]
}'
# 2. Session attaches vault containing credentials for that MCP server URL
curl -X POST https://api.anthropic.com/v1/sessions \
"${HEADERS[@]}" \
-d '{
"agent": "agent_abc123",
"environment_id": "env_abc123",
"vault_ids": ["vlt_abc123"]
}'
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.
---
## Tool Configuration
```bash
curl -X POST https://api.anthropic.com/v1/agents \
"${HEADERS[@]}" \
-d '{
"name": "Restricted Agent",
"model": "claude-opus-5",
"tools": [
{
"type": "agent_toolset_20260401",
"default_config": { "enabled": true },
"configs": [
{ "name": "bash", "enabled": false }
]
}
]
}'
```
# Claude API - Go
> **Note:** The Go SDK supports the Claude API and beta tool use with `BetaToolRunner`. Agent SDK is not yet available for Go.
## Installation
```bash
go get github.com/anthropics/anthropic-sdk-go
```
## Client Initialization
```go
import (
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
// Default (uses ANTHROPIC_API_KEY env var)
client := anthropic.NewClient()
// Explicit API key
client := anthropic.NewClient(
option.WithAPIKey("your-api-key"),
)
```
---
## Model Constants
The Go SDK provides typed model constants: `anthropic.ModelClaudeFable5`, `anthropic.ModelClaudeOpus4_8`, `anthropic.ModelClaudeOpus4_7`, `anthropic.ModelClaudeSonnet4_6`, `anthropic.ModelClaudeHaiku4_5_20251001`. Default to Claude Opus 5 unless the user specifies otherwise; if they ask for Fable or the most powerful model, use `anthropic.ModelClaudeFable5` (see `shared/models.md` for the full resolution table).
`anthropic.Model` is an alias for `string`, so a model with no typed constant yet - including Claude Opus 5 - is passed as the plain id: `Model: "claude-opus-5"`. Check the SDK release notes for a typed `Claude Opus 5` constant before assuming one exists.
---
## Basic Message Request
```go
response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
Model: "claude-opus-5",
MaxTokens: 16000,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("What is the capital of France?")),
},
})
if err != nil {
log.Fatal(err)
}
for _, block := range response.Content {
switch variant := block.AsAny().(type) {
case anthropic.TextBlock:
fmt.Println(variant.Text)
}
}
```
---
## Thinking
Enable Claude's internal reasoning by setting `Thinking` in `MessageNewParams`. The response will contain `ThinkingBlock` content before the final `TextBlock`.
**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think. Combine with the `effort` parameter for cost-quality control.
Derived from `anthropic-sdk-go/message.go` (`ThinkingConfigParamUnion`, `ThinkingConfigAdaptiveParam`).
```go
// There is no ThinkingConfigParamOfAdaptive helper - construct the union
// struct-literal directly and take the address of the variant.
adaptive := anthropic.ThinkingConfigAdaptiveParam{}
params := anthropic.MessageNewParams{
Model: anthropic.ModelClaudeSonnet4_6,
MaxTokens: 16000,
Thinking: anthropic.ThinkingConfigParamUnion{OfAdaptive: &adaptive},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("How many r's in strawberry?")),
},
}
resp, err := client.Messages.New(context.Background(), params)
if err != nil {
log.Fatal(err)
}
// ThinkingBlock(s) precede TextBlock in content
for _, block := range resp.Content {
switch b := block.AsAny().(type) {
case anthropic.ThinkingBlock:
fmt.Println("[thinking]", b.Thinking)
case anthropic.TextBlock:
fmt.Println(b.Text)
}
}
```
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking (above). `ThinkingConfigParamOfEnabled(budgetTokens)` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - leaving `Thinking` unset runs adaptive (the adaptive union is equivalent), unlike Opus 4.8/4.7 where leaving it unset meant no thinking.
> **Older models:** Use `anthropic.ThinkingConfigParamOfEnabled(N)` (budget must be < `MaxTokens`, min 1024).
To disable: `anthropic.ThinkingConfigParamUnion{OfDisabled: &anthropic.ThinkingConfigDisabledParam{}}`. On Claude Opus 5 that is accepted only at effort `high` or lower - pairing it with `xhigh`/`max` returns a 400.
---
## Prompt Caching
`System` is `[]TextBlockParam`; set `CacheControl` on the last block to cache tools + system together. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`.
```go
System: []anthropic.TextBlockParam{{
Text: longSystemPrompt,
CacheControl: anthropic.NewCacheControlEphemeralParam(), // default 5m TTL
}},
```
For 1-hour TTL: `anthropic.CacheControlEphemeralParam{TTL: anthropic.CacheControlEphemeralTTLTTL1h}`. There's also a top-level `CacheControl` on `MessageNewParams` that auto-places on the last cacheable block.
Verify hits via `resp.Usage.CacheCreationInputTokens` / `resp.Usage.CacheReadInputTokens`.
---
## Stop Details
When `StopReason` is `anthropic.StopReasonRefusal`, the response includes structured `StopDetails`:
```go
if resp.StopReason == anthropic.StopReasonRefusal {
fmt.Println("Category:", resp.StopDetails.Category) // e.g. "cyber", "bio", "reasoning_extraction", "frontier_llm", or "" - see docs for the full set
fmt.Println("Explanation:", resp.StopDetails.Explanation)
}
```
**Refusal fallbacks (Claude Fable 5.1) - opt in by default.** Fallbacks are opt-in: without them a refused request simply stops. In `claude-fable-5-1` code, include `Fallbacks: []anthropic.BetaFallbackParam{{Model: "claude-opus-4-8"}}` plus the `anthropic.AnthropicBetaServerSideFallback2026_06_01` beta on `client.Beta.Messages.New` by default - a policy decline is re-served by the fallback model inside the same call. Not available on Amazon Bedrock, Vertex AI, or Microsoft Foundry - register the client-side middleware there: `option.WithMiddleware(betafallback.BetaRefusalFallbackMiddleware(...))` from `lib/betafallback`, with per-conversation state via `betafallback.WithBetaFallbackState(&betafallback.BetaFallbackState{})`. Full semantics (billing, sticky routing, streaming) and a runnable example: `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason, and the Go SDK repo's `examples/` (WebFetch via `shared/live-sources.md`).
---
## PDF / Document Input
`NewDocumentBlock` generic helper accepts any source type. `MediaType`/`Type` are auto-set.
```go
b64 := base64.StdEncoding.EncodeToString(pdfBytes)
msg := anthropic.NewUserMessage(
anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{Data: b64}),
anthropic.NewTextBlock("Summarize this document"),
)
```
Other sources: `URLPDFSourceParam{URL: "https://..."}`, `PlainTextSourceParam{Data: "..."}`.
---
## Context Editing / Compaction (Beta)
Use `Beta.Messages.New` with `ContextManagement` on `BetaMessageNewParams`. There is no `NewBetaAssistantMessage` - use `.ToParam()` for the round-trip.
```go
params := anthropic.BetaMessageNewParams{
Model: "claude-opus-5", // also supported: ModelClaudeOpus4_8, ModelClaudeSonnet4_6
MaxTokens: 16000,
Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"},
ContextManagement: anthropic.BetaContextManagementConfigParam{
Edits: []anthropic.BetaContextManagementConfigEditUnionParam{
{OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}},
},
},
Messages: []anthropic.BetaMessageParam{ /* ... */ },
}
resp, err := client.Beta.Messages.New(ctx, params)
if err != nil {
log.Fatal(err)
}
// Round-trip: append response to history via .ToParam()
params.Messages = append(params.Messages, resp.ToParam())
// Read compaction blocks from the response
for _, block := range resp.Content {
if c, ok := block.AsAny().(anthropic.BetaCompactionBlock); ok {
fmt.Println("compaction summary:", c.Content)
}
}
```
Other edit types: `BetaClearToolUses20250919EditParam`, `BetaClearThinking20251015EditParam` - these need `Betas: []anthropic.AnthropicBeta{"context-management-2025-06-27"}`, not `compact-2026-01-12`.
# Files API - Go
## Files API
> **Out of beta.** In current SDKs `client.Beta.Files` has breaking shape changes from previous versions, matching the stable `client.Files` - migrate per the Files API row in `shared/live-sources.md`. Examples below predate this.
Under `client.Beta.Files`. Method is **`Upload`** (NOT `New`/`Create`), params struct is `BetaFileUploadParams`. The `File` field takes an `io.Reader`; use `anthropic.File()` to attach a filename + content-type for the multipart encoding.
```go
f, _ := os.Open("./upload_me.txt")
defer f.Close()
meta, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{
File: anthropic.File(f, "upload_me.txt", "text/plain"),
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14},
})
// meta.ID is the file_id to reference in subsequent message requests
```
Other `Beta.Files` methods: `List`, `Delete`, `Download`, `GetMetadata`.
---
# Streaming - Go
## Streaming
```go
stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 64000,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Write a haiku")),
},
})
for stream.Next() {
event := stream.Current()
switch eventVariant := event.AsAny().(type) {
case anthropic.ContentBlockDeltaEvent:
switch deltaVariant := eventVariant.Delta.AsAny().(type) {
case anthropic.TextDelta:
fmt.Print(deltaVariant.Text)
}
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
```
**Accumulating the final message** (there is no `GetFinalMessage()` on the stream):
```go
stream := client.Messages.NewStreaming(ctx, params)
message := anthropic.Message{}
for stream.Next() {
message.Accumulate(stream.Current())
}
if err := stream.Err(); err != nil { log.Fatal(err) }
// message.Content now has the complete response
```
---
# Tool Use - Go
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Use
### Tool Runner (Beta - Recommended)
**Beta:** The Go SDK provides `BetaToolRunner` for automatic tool use loops via the `toolrunner` package.
```go
import (
"context"
"fmt"
"log"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/toolrunner"
)
// Define tool input with jsonschema tags for automatic schema generation
type GetWeatherInput struct {
City string `json:"city" jsonschema:"required,description=The city name"`
}
// Create a tool with automatic schema generation from struct tags
weatherTool, err := toolrunner.NewBetaToolFromJSONSchema(
"get_weather",
"Get current weather for a city",
func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
return anthropic.BetaToolResultBlockParamContentUnion{
OfText: &anthropic.BetaTextBlockParam{
Text: fmt.Sprintf("The weather in %s is sunny, 72°F", input.City),
},
}, nil
},
)
if err != nil {
log.Fatal(err)
}
// Create a tool runner that handles the conversation loop automatically
runner := client.Beta.Messages.NewToolRunner(
[]anthropic.BetaTool{weatherTool},
anthropic.BetaToolRunnerParams{
BetaMessageNewParams: anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 16000,
Messages: []anthropic.BetaMessageParam{
anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in Paris?")),
},
},
MaxIterations: 5,
},
)
// Run until Claude produces a final response
message, err := runner.RunToCompletion(context.Background())
if err != nil {
log.Fatal(err)
}
// RunToCompletion returns *BetaMessage; content is []BetaContentBlockUnion.
// Narrow via AsAny() switch - note the Beta-namespace types (BetaTextBlock,
// not TextBlock):
for _, block := range message.Content {
switch block := block.AsAny().(type) {
case anthropic.BetaTextBlock:
fmt.Println(block.Text)
}
}
```
**Key features of the Go tool runner:**
- Automatic schema generation from Go structs via `jsonschema` tags
- `RunToCompletion()` for simple one-shot usage
- `All()` iterator for processing each message in the conversation
- `NextMessage()` for step-by-step iteration
- Streaming variant via `NewToolRunnerStreaming()` with `AllStreaming()`
### Manual Loop
Prefer the tool runner above. For interception, validation, logging, or human-in-the-loop approval, gate inside the tool's run function or step the runner with `NextMessage()`/`All()` and inspect each message (the runner's public `Params` field lets you adjust the next request) - a manual loop is not required. Drop to a manual loop only when you need control the runner does not expose: define tools with `ToolParam`, check `StopReason`, execute tools yourself, and feed `tool_result` blocks back.
Derived from `anthropic-sdk-go/examples/tools/main.go`.
```go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
client := anthropic.NewClient()
// 1. Define tools. ToolParam.InputSchema uses a map, no struct tags needed.
addTool := anthropic.ToolParam{
Name: "add",
Description: anthropic.String("Add two integers"),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"a": map[string]any{"type": "integer"},
"b": map[string]any{"type": "integer"},
},
},
}
// ToolParam must be wrapped in ToolUnionParam for the Tools slice
tools := []anthropic.ToolUnionParam{{OfTool: &addTool}}
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("What is 2 + 3?")),
}
for {
resp, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeSonnet4_6,
MaxTokens: 16000,
Messages: messages,
Tools: tools,
})
if err != nil {
log.Fatal(err)
}
// 2. Append the assistant response to history BEFORE processing tool calls.
// resp.ToParam() converts Message -> MessageParam in one call.
messages = append(messages, resp.ToParam())
// 3. Walk content blocks. ContentBlockUnion is a flattened struct;
// use block.AsAny().(type) to switch on the actual variant.
toolResults := []anthropic.ContentBlockParamUnion{}
for _, block := range resp.Content {
switch variant := block.AsAny().(type) {
case anthropic.TextBlock:
fmt.Println(variant.Text)
case anthropic.ToolUseBlock:
// 4. Parse the tool input. Use variant.JSON.Input.Raw() to get the
// raw JSON - block.Input is json.RawMessage, not the parsed value.
var in struct {
A int `json:"a"`
B int `json:"b"`
}
if err := json.Unmarshal([]byte(variant.JSON.Input.Raw()), &in); err != nil {
log.Fatal(err)
}
result := fmt.Sprintf("%d", in.A+in.B)
// 5. NewToolResultBlock(toolUseID, content, isError) builds the
// ContentBlockParamUnion for you. block.ID is the tool_use_id.
toolResults = append(toolResults,
anthropic.NewToolResultBlock(block.ID, result, false))
}
}
// 6. Exit when Claude stops asking for tools
if resp.StopReason != anthropic.StopReasonToolUse {
break
}
// 7. Tool results go in a user message (variadic: all results in one turn)
messages = append(messages, anthropic.NewUserMessage(toolResults...))
}
}
```
**Key API surface:**
| Symbol | Purpose |
|---|---|
| `resp.ToParam()` | Convert `Message` response -> `MessageParam` for history |
| `block.AsAny().(type)` | Type-switch on `ContentBlockUnion` variants |
| `variant.JSON.Input.Raw()` | Raw JSON string of tool input (for `json.Unmarshal`) |
| `anthropic.NewToolResultBlock(id, content, isError)` | Build `tool_result` block |
| `anthropic.NewUserMessage(blocks...)` | Wrap tool results as a user turn |
| `anthropic.StopReasonToolUse` | `StopReason` constant to check loop termination |
| `anthropic.ToolUnionParam{OfTool: &t}` | Wrap `ToolParam` in the union for `Tools:` |
---
## Anthropic-Defined Tools
Version-suffixed struct names with `Param` suffix. `Name`/`Type` are `constant.*` types - zero value marshals correctly, so `{}` works. Wrap in `ToolUnionParam` with the matching `Of*` field. Web search and code execution are server-executed; bash and text editor are client-executed (you handle the `tool_use` locally - see `shared/tool-use-concepts.md`).
```go
Tools: []anthropic.ToolUnionParam{
{OfWebSearchTool20260209: &anthropic.WebSearchTool20260209Param{}},
{OfBashTool20250124: &anthropic.ToolBash20250124Param{}},
{OfTextEditor20250728: &anthropic.ToolTextEditor20250728Param{}},
{OfCodeExecutionTool20260120: &anthropic.CodeExecutionTool20260120Param{}},
},
```
Also available: `WebFetchTool20260209Param`, `ToolSearchToolBm25_20251119Param`, `ToolSearchToolRegex20251119Param`. For the advisor and memory tools, use `BetaAdvisorTool20260301Param` / `BetaMemoryTool20250818Param` in the beta namespace on `client.Beta.Messages.New`.
### Advisor tool (beta)
Server-side - no tool_result round-trip. The advisor model must be >= the executor (top-level) model; invalid pairs return 400.
```go
response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeSonnet4_6,
MaxTokens: 4096,
Tools: []anthropic.BetaToolUnionParam{
{OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{
Model: anthropic.ModelClaudeOpus4_8,
}},
},
Messages: []anthropic.BetaMessageParam{ /* ... */ },
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaAdvisorTool2026_03_01},
})
```
---
# Managed Agents - Go
> **Bindings not shown here:** This README covers the most common managed-agents flows for Go. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Go SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK.
> **Agents are persistent - create once, reference by ID.** Store the agent ID returned by `agents.New` and pass it to every subsequent `sessions.New`; do not call `agents.New` in the request path. **Recommended:** define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md` (its live-docs URL is in `shared/live-sources.md`). The CLI owns the control plane (create/update); your code owns the data plane (sessions with the stored ID). The examples below show in-code creation for when you must provision programmatically; in production the create call belongs in setup, not in the request path.
## Installation
```bash
go get github.com/anthropics/anthropic-sdk-go
```
## Client Initialization
```go
import (
"context"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
// Default (uses ANTHROPIC_API_KEY env var)
client := anthropic.NewClient()
// Explicit API key
client := anthropic.NewClient(
option.WithAPIKey("your-api-key"),
)
ctx := context.Background()
```
---
## Create an Environment
```go
environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{
Name: "my-dev-env",
Config: anthropic.BetaEnvironmentNewParamsConfigUnion{
OfCloud: &anthropic.BetaCloudConfigParams{
Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{
OfUnrestricted: &anthropic.BetaUnrestrictedNetworkParam{},
},
},
},
})
if err != nil {
panic(err)
}
fmt.Println(environment.ID) // env_...
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** `Model`/`System`/`Tools` live on the agent object, not the session. Always start with `Beta.Agents.New()` - the session only takes `Agent: anthropic.BetaSessionNewParamsAgentUnion{OfString: anthropic.String(agent.ID)}` (or the typed `OfBetaManagedAgentsAgents` variant when you need a specific version).
### Minimal
```go
// 1. Create the agent (reusable, versioned)
agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
Name: "Coding Assistant",
Model: anthropic.BetaManagedAgentsModelConfigParams{
ID: "claude-opus-5",
Type: anthropic.BetaManagedAgentsModelConfigParamsTypeModelConfig,
},
System: anthropic.String("You are a helpful coding assistant."),
Tools: []anthropic.BetaAgentNewParamsToolUnion{{
OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{
Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
},
}},
})
if err != nil {
panic(err)
}
// 2. Start a session
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{
OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{
Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent,
ID: agent.ID,
Version: anthropic.Int(agent.Version),
},
},
EnvironmentID: environment.ID,
Title: anthropic.String("Quickstart session"),
})
if err != nil {
panic(err)
}
fmt.Printf("Session ID: %s, status: %s\n", session.ID, session.Status)
fmt.Printf("Trace: https://platform.claude.com/workspaces/default/sessions/%s\n", session.ID) // swap 'default' for your workspace ID if the API key is not in the Default workspace
```
### Updating an Agent
Updates create new versions; the agent object is immutable per version.
```go
updatedAgent, err := client.Beta.Agents.Update(ctx, agent.ID, anthropic.BetaAgentUpdateParams{
Version: agent.Version,
System: anthropic.String("You are a helpful coding agent. Always write tests."),
})
if err != nil {
panic(err)
}
fmt.Printf("New version: %d\n", updatedAgent.Version)
// List all versions
iter := client.Beta.Agents.Versions.ListAutoPaging(ctx, agent.ID, anthropic.BetaAgentVersionListParams{})
for iter.Next() {
version := iter.Current()
fmt.Printf("Version %d: %s\n", version.Version, version.UpdatedAt.Format(time.RFC3339))
}
if err := iter.Err(); err != nil {
panic(err)
}
// Archive the agent
_, err = client.Beta.Agents.Archive(ctx, agent.ID, anthropic.BetaAgentArchiveParams{})
if err != nil {
panic(err)
}
```
---
## Send a User Message
```go
_, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "Review the auth module",
},
}},
},
}},
})
if err != nil {
panic(err)
}
```
> Tip: **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens - stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns).
---
## Stream Events (SSE)
```go
// Open the stream first, then send the user message
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "Summarize the repo README",
},
}},
},
}},
}); err != nil {
panic(err)
}
events:
for stream.Next() {
switch event := stream.Current().AsAny().(type) {
case anthropic.BetaManagedAgentsAgentMessageEvent:
for _, block := range event.Content {
fmt.Print(block.Text)
}
case anthropic.BetaManagedAgentsAgentToolUseEvent:
fmt.Printf("\n[Using tool: %s]\n", event.Name)
case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
break events
case anthropic.BetaManagedAgentsSessionErrorEvent:
fmt.Printf("\n[Error: %s]\n", event.Error.Message)
break events
}
}
if err := stream.Err(); err != nil {
panic(err)
}
```
### Reconnecting and Tailing
When reconnecting mid-session, list past events first to dedupe, then tail live events:
```go
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
// Stream is open and buffering. List history before tailing live.
seenEventIDs := map[string]struct{}{}
history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{})
for history.Next() {
seenEventIDs[history.Current().ID] = struct{}{}
}
if err := history.Err(); err != nil {
panic(err)
}
// Tail live events, skipping anything already seen
tail:
for stream.Next() {
event := stream.Current()
if _, seen := seenEventIDs[event.ID]; seen {
continue
}
seenEventIDs[event.ID] = struct{}{}
switch event := event.AsAny().(type) {
case anthropic.BetaManagedAgentsAgentMessageEvent:
for _, block := range event.Content {
fmt.Print(block.Text)
}
case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
break tail
}
}
if err := stream.Err(); err != nil {
panic(err)
}
```
---
## Provide Custom Tool Result
> Note: The Go managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `github.com/anthropics/anthropic-sdk-go` repository for the corresponding Go params types.
---
## Poll Events
```go
// Auto-paginating iterator
iter := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{})
for iter.Next() {
event := iter.Current()
fmt.Printf("%s: %s\n", event.Type, event.ID)
}
if err := iter.Err(); err != nil {
panic(err)
}
```
---
## Upload a File
```go
csvFile, err := os.Open("data.csv")
if err != nil {
panic(err)
}
defer csvFile.Close()
file, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{
File: csvFile,
})
if err != nil {
panic(err)
}
fmt.Printf("File ID: %s\n", file.ID)
// Mount in a session
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{
OfString: anthropic.String(agent.ID),
},
EnvironmentID: environment.ID,
Resources: []anthropic.BetaSessionNewParamsResourceUnion{{
OfFile: &anthropic.BetaManagedAgentsFileResourceParams{
Type: anthropic.BetaManagedAgentsFileResourceParamsTypeFile,
FileID: file.ID,
MountPath: anthropic.String("/workspace/data.csv"),
},
}},
})
if err != nil {
panic(err)
}
```
### Add and Manage Resources on an Existing Session
```go
// Attach an additional file to an open session
resource, err := client.Beta.Sessions.Resources.Add(ctx, session.ID, anthropic.BetaSessionResourceAddParams{
BetaManagedAgentsFileResourceParams: anthropic.BetaManagedAgentsFileResourceParams{
Type: anthropic.BetaManagedAgentsFileResourceParamsTypeFile,
FileID: file.ID,
},
})
if err != nil {
panic(err)
}
fmt.Println(resource.ID) // "sesrsc_01ABC..."
// List resources on the session
listed, err := client.Beta.Sessions.Resources.List(ctx, session.ID, anthropic.BetaSessionResourceListParams{})
if err != nil {
panic(err)
}
for _, entry := range listed.Data {
fmt.Println(entry.ID, entry.Type)
}
// Detach a resource
if _, err := client.Beta.Sessions.Resources.Delete(ctx, resource.ID, anthropic.BetaSessionResourceDeleteParams{
SessionID: session.ID,
}); err != nil {
panic(err)
}
```
---
## List and Download Session Files
> Note: Listing and downloading files an agent wrote during a session is not yet documented for Go in this skill or in the apps source examples. See `shared/managed-agents-events.md` and the `github.com/anthropics/anthropic-sdk-go` repository for the `Beta.Files.List` and `Beta.Files.Download` Go params types.
---
## Session Management
```go
// List environments
environments, err := client.Beta.Environments.List(ctx, anthropic.BetaEnvironmentListParams{})
if err != nil {
panic(err)
}
// Retrieve a specific environment
env, err := client.Beta.Environments.Get(ctx, environment.ID, anthropic.BetaEnvironmentGetParams{})
if err != nil {
panic(err)
}
// Archive an environment (read-only, existing sessions continue)
_, err = client.Beta.Environments.Archive(ctx, environment.ID, anthropic.BetaEnvironmentArchiveParams{})
if err != nil {
panic(err)
}
// Delete an environment (only if no sessions reference it)
_, err = client.Beta.Environments.Delete(ctx, environment.ID, anthropic.BetaEnvironmentDeleteParams{})
if err != nil {
panic(err)
}
// Delete a session
_, err = client.Beta.Sessions.Delete(ctx, session.ID, anthropic.BetaSessionDeleteParams{})
if err != nil {
panic(err)
}
```
---
## MCP Server Integration
```go
// Agent declares MCP server (no auth here - auth goes in a vault)
agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
Name: "GitHub Assistant",
Model: anthropic.BetaManagedAgentsModelConfigParams{
ID: "claude-opus-5",
Type: anthropic.BetaManagedAgentsModelConfigParamsTypeModelConfig,
},
MCPServers: []anthropic.BetaManagedAgentsURLMCPServerParams{{
Type: anthropic.BetaManagedAgentsURLMCPServerParamsTypeURL,
Name: "github",
URL: "https://api.githubcopilot.com/mcp/",
}},
Tools: []anthropic.BetaAgentNewParamsToolUnion{
{
OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{
Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
},
},
{
OfMCPToolset: &anthropic.BetaManagedAgentsMCPToolsetParams{
Type: anthropic.BetaManagedAgentsMCPToolsetParamsTypeMCPToolset,
MCPServerName: "github",
},
},
},
})
if err != nil {
panic(err)
}
// Session attaches vault(s) containing credentials for those MCP server URLs
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{
OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{
Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent,
ID: agent.ID,
Version: anthropic.Int(agent.Version),
},
},
EnvironmentID: environment.ID,
VaultIDs: []string{vault.ID},
})
if err != nil {
panic(err)
}
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.
---
## Vaults
```go
// Create a vault
vault, err := client.Beta.Vaults.New(ctx, anthropic.BetaVaultNewParams{
DisplayName: "Alice",
Metadata: map[string]string{"external_user_id": "usr_abc123"},
})
if err != nil {
panic(err)
}
// Add an OAuth credential
credential, err := client.Beta.Vaults.Credentials.New(ctx, vault.ID, anthropic.BetaVaultCredentialNewParams{
DisplayName: anthropic.String("Alice's Slack"),
Auth: anthropic.BetaVaultCredentialNewParamsAuthUnion{
OfMCPOAuth: &anthropic.BetaManagedAgentsMCPOAuthCreateParams{
Type: anthropic.BetaManagedAgentsMCPOAuthCreateParamsTypeMCPOAuth,
MCPServerURL: "https://mcp.slack.com/mcp",
AccessToken: "xoxp-...",
ExpiresAt: anthropic.Time(time.Date(2026, time.April, 15, 0, 0, 0, 0, time.UTC)),
Refresh: anthropic.BetaManagedAgentsMCPOAuthRefreshParams{
TokenEndpoint: "https://slack.com/api/oauth.v2.access",
ClientID: "1234567890.0987654321",
Scope: anthropic.String("channels:read chat:write"),
RefreshToken: "xoxe-1-...",
TokenEndpointAuth: anthropic.BetaManagedAgentsMCPOAuthRefreshParamsTokenEndpointAuthUnion{
OfClientSecretPost: &anthropic.BetaManagedAgentsTokenEndpointAuthPostParam{
Type: anthropic.BetaManagedAgentsTokenEndpointAuthPostParamTypeClientSecretPost,
ClientSecret: "abc123...",
},
},
},
},
},
})
if err != nil {
panic(err)
}
// Rotate the credential (e.g., after a token refresh)
_, err = client.Beta.Vaults.Credentials.Update(ctx, credential.ID, anthropic.BetaVaultCredentialUpdateParams{
VaultID: vault.ID,
Auth: anthropic.BetaVaultCredentialUpdateParamsAuthUnion{
OfMCPOAuth: &anthropic.BetaManagedAgentsMCPOAuthUpdateParams{
Type: anthropic.BetaManagedAgentsMCPOAuthUpdateParamsTypeMCPOAuth,
AccessToken: anthropic.String("xoxp-new-..."),
ExpiresAt: anthropic.Time(time.Date(2026, time.May, 15, 0, 0, 0, 0, time.UTC)),
Refresh: anthropic.BetaManagedAgentsMCPOAuthRefreshUpdateParams{
RefreshToken: anthropic.String("xoxe-1-new-..."),
},
},
},
})
if err != nil {
panic(err)
}
// Archive a vault
_, err = client.Beta.Vaults.Archive(ctx, vault.ID, anthropic.BetaVaultArchiveParams{})
if err != nil {
panic(err)
}
```
---
## GitHub Repository Integration
Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential):
```go
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{OfString: anthropic.String(agent.ID)},
EnvironmentID: environment.ID,
VaultIDs: []string{vault.ID},
Resources: []anthropic.BetaSessionNewParamsResourceUnion{
{
OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{
Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository,
URL: "https://github.com/org/repo",
MountPath: anthropic.String("/workspace/repo"),
AuthorizationToken: "ghp_your_github_token",
},
},
},
})
if err != nil {
panic(err)
}
```
Multiple repositories on the same session:
```go
resources := []anthropic.BetaSessionNewParamsResourceUnion{
{
OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{
Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository,
URL: "https://github.com/org/frontend",
MountPath: anthropic.String("/workspace/frontend"),
AuthorizationToken: "ghp_your_github_token",
},
},
{
OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{
Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository,
URL: "https://github.com/org/backend",
MountPath: anthropic.String("/workspace/backend"),
AuthorizationToken: "ghp_your_github_token",
},
},
}
```
Rotating a repository's authorization token:
```go
listed, err := client.Beta.Sessions.Resources.List(ctx, session.ID, anthropic.BetaSessionResourceListParams{})
if err != nil {
panic(err)
}
repoResourceID := listed.Data[0].ID
_, err = client.Beta.Sessions.Resources.Update(ctx, repoResourceID, anthropic.BetaSessionResourceUpdateParams{
SessionID: session.ID,
AuthorizationToken: "ghp_your_new_github_token",
})
if err != nil {
panic(err)
}
```
# Claude API - Java
> **Note:** The Java SDK supports the Claude API and beta tool use with annotated classes. Agent SDK is not yet available for Java.
## Package Reference
Types are organized by package. If a class you need isn't shown in an example below, locate it via this table first - don't block on fetching SDK source over the network.
| `import` prefix | Contains |
|---|---|
| `com.anthropic.client` / `com.anthropic.client.okhttp` | `AnthropicClient`, `AnthropicOkHttpClient` |
| `com.anthropic.models.messages` | non-beta request/response types - `MessageCreateParams`, `Model`, `Message`, `TextBlockParam`, `ContentBlockParam`, `ToolUseBlockParam`, `ToolResultBlockParam`, `CacheControlEphemeral`, `Tool*` (e.g. `ToolBash20250124`, `ToolTextEditor20250728`), `StopReason`, `StructuredMessage*` |
| `com.anthropic.models.messages.batches` | Batch API - `BatchResultsParams`, `MessageBatchIndividualResponse` |
| `com.anthropic.models.beta` | `AnthropicBeta` (beta-flag constants) |
| `com.anthropic.models.beta.messages` | beta-endpoint types - `MessageCreateParams`, `BetaMessage`, `BetaStopReason`, `BetaContextManagementConfig`, `BetaMcpToolset`, `BetaRequestMcpServerUrlDefinition`, `BetaTool*` |
| `com.anthropic.core` | `JsonValue`, `JsonField`, `JsonSchemaLocalValidation`, `com.anthropic.core.http.StreamResponse` |
| `com.anthropic.errors` | typed exceptions - `AnthropicServiceException`, `RateLimitException`, `NotFoundException`, etc. (see `shared/error-codes.md`) |
`client.messages()` uses `com.anthropic.models.messages.*`; `client.beta().messages()` uses `com.anthropic.models.beta.messages.*`. Both packages define a `MessageCreateParams` - import the one matching the client path you call.
### Key types per feature
Write from this table instead of `javap`/jar inspection. Endpoint column tells you whether to use `client.messages()` or `client.beta().messages()`.
| Feature | Endpoint | Key Java types / builder calls |
|---|---|---|
| User profiles | beta | `client.beta().userProfiles().create(...)` / `.retrieve(id)` / `.list()`. Pass the returned profile id on the beta `MessageCreateParams`. Requires a beta header - check the SDK's beta-headers reference for the current flag. |
| Agent Skills | beta | `BetaContainerParams`, `BetaSkillParams`, `BetaCodeExecutionTool20250825`. `.addBeta("code-execution-2025-08-25")` (Skills is out of beta - no `skills-2025-10-02`). Download the output via `client.beta().files().download(fileId)`. |
| Cache diagnostics | beta | `BetaDiagnosticsParam`, `BetaCacheControlEphemeral` |
| Context editing | beta | `.contextManagement(BetaContextManagementConfig.builder()...)`. The edit strategy is a `BetaClearToolUses20250919Edit` (or `BetaClearThinking20251015Edit`); its trigger is a `BetaInputTokensTrigger` built separately and passed to the edit's builder - there is no direct `.inputTokensTrigger(N)` shortcut on the edit builder. `javap` the edit and trigger classes for the exact setter names. |
| Memory tool | non-beta | `.addTool(MemoryTool20250818.builder().build())` from `com.anthropic.models.messages` |
| Programmatic tool calling | non-beta | `CodeExecutionTool20260120`, `Tool`, `ContentBlockParam` |
| Strict tool use | non-beta | `Tool`, `Tool.InputSchema` |
| Task budgets | beta | `.outputConfig(BetaOutputConfig.builder().taskBudget(BetaTokenTaskBudget.builder()...))` |
| Tool search | non-beta | `.addTool(ToolSearchToolRegex20251119.builder()...)` from `com.anthropic.models.messages` |
| Web search | non-beta | `WebSearchTool20260209` from `com.anthropic.models.messages` - the latest variant with dynamic filtering (Claude Fable 5.1 + Claude Opus 5 + Opus 4.8/4.7/4.6 + Claude Sonnet 5 + Sonnet 4.6). For older models or Vertex, use `WebSearchTool20250305` |
### Discovering type and member names
If a class or builder method you need isn't in the tables above, `jar tf <anthropic-java-core jar> | grep -i <term>` or `javap -classpath <jar> com.anthropic.models....` is fast enough to locate names. **Do not compile and run a separate reflection program** to enumerate members - the first build is slow enough to be backgrounded in many environments, trapping you in a polling loop. Write the script with the names you found and let the compiler error (`cannot find symbol`) point at any wrong member.
## Installation
Maven:
```xml
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
<version>2.34.0</version>
</dependency>
```
Gradle:
```groovy
implementation("com.anthropic:anthropic-java:2.34.0")
```
## Client Initialization
```java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
// Default (reads ANTHROPIC_API_KEY from environment)
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
// Explicit API key
AnthropicClient client = AnthropicOkHttpClient.builder()
.apiKey("your-api-key")
.build();
```
---
## Basic Message Request
```java
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Message;
MessageCreateParams params = MessageCreateParams.builder()
.model("claude-opus-5") // .model(String) overload - use it for ids with no typed Model constant yet
.maxTokens(16000L)
.addUserMessage("What is the capital of France?")
.build();
Message response = client.messages().create(params);
response.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> System.out.println(textBlock.text()));
```
---
## Thinking
**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think. The builder has a direct `.thinking(ThinkingConfigAdaptive)` overload - no manual union wrapping.
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking (below). `ThinkingConfigEnabled.builder().budgetTokens(N)` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `.thinking(...)` runs adaptive (`ThinkingConfigAdaptive` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `ThinkingConfigDisabled` is accepted only at effort `HIGH` or lower; pairing it with `XHIGH`/`MAX` returns a 400.
> **Older models:** Use `.thinking(ThinkingConfigEnabled.builder().budgetTokens(N).build())` (budget must be < `maxTokens`, min 1024).
```java
import com.anthropic.models.messages.ContentBlock;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.ThinkingConfigAdaptive;
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_SONNET_4_6)
.maxTokens(16000L)
.thinking(ThinkingConfigAdaptive.builder().build())
.addUserMessage("Solve this step by step: 27 * 453")
.build();
for (ContentBlock block : client.messages().create(params).content()) {
block.thinking().ifPresent(t -> System.out.println("[thinking] " + t.thinking()));
block.text().ifPresent(t -> System.out.println(t.text()));
}
```
`ContentBlock` narrowing: `.thinking()` / `.text()` return `Optional<T>` - use `.ifPresent(...)` or `.stream().flatMap(...)`. Alternative: `isThinking()` / `asThinking()` boolean+unwrap pairs (throws on wrong variant).
---
## Effort Parameter
Effort is nested inside `OutputConfig` - there is NO `.effort()` directly on `MessageCreateParams.Builder`.
```java
import com.anthropic.models.messages.OutputConfig;
.outputConfig(OutputConfig.builder()
.effort(OutputConfig.Effort.HIGH) // or LOW, MEDIUM, XHIGH, MAX
.build())
```
Combine with `Thinking = ThinkingConfigAdaptive` for cost-quality control.
---
## Prompt Caching
System message as a list of `TextBlockParam` with `CacheControlEphemeral`. Use `.systemOfTextBlockParams(...)` - the plain `.system(String)` overload can't carry cache control. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`.
```java
import com.anthropic.models.messages.TextBlockParam;
import com.anthropic.models.messages.CacheControlEphemeral;
.systemOfTextBlockParams(List.of(
TextBlockParam.builder()
.text(longSystemPrompt)
.cacheControl(CacheControlEphemeral.builder()
.ttl(CacheControlEphemeral.Ttl.TTL_1H) // optional; also TTL_5M
.build())
.build()))
```
There's also a top-level `.cacheControl(CacheControlEphemeral)` on `MessageCreateParams.Builder` and on `Tool.builder()`.
Verify hits via `response.usage().cacheCreationInputTokens()` / `response.usage().cacheReadInputTokens()`.
---
## Token Counting
```java
import com.anthropic.models.messages.MessageCountTokensParams;
long tokens = client.messages().countTokens(
MessageCountTokensParams.builder()
.model(Model.CLAUDE_SONNET_4_6)
.addUserMessage("Hello")
.build()
).inputTokens();
```
---
## PDF / Document Input
`DocumentBlockParam` builder has source shortcuts. Wrap in `ContentBlockParam.ofDocument()` and pass via `.addUserMessageOfBlockParams()`.
```java
import com.anthropic.models.messages.DocumentBlockParam;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.TextBlockParam;
DocumentBlockParam doc = DocumentBlockParam.builder()
.source(Base64PdfSource.builder().data(base64String).build())
// or .source(UrlPdfSource.builder().url("https://...").build())
.title("My Document") // optional
.build();
```
For **Files API** document references, use the beta path and beta types - see `files-api.md`: `BetaRequestDocumentBlock.builder().source(BetaFileDocumentSource.builder().fileId(id).build())`.
```java
.addUserMessageOfBlockParams(List.of(
ContentBlockParam.ofDocument(doc),
ContentBlockParam.ofText(TextBlockParam.builder().text("Summarize this").build())))
```
---
## Stop Details
When `stopReason()` is `"refusal"`, the response includes structured `stopDetails()`:
```java
response.stopDetails().ifPresent(details -> {
System.out.println("Category: " + details.category());
System.out.println("Explanation: " + details.explanation());
});
```
**Refusal fallbacks (Claude Fable 5.1) - opt in by default.** Fallbacks are opt-in: without them a refused request simply stops. New `claude-fable-5-1` code should include the server-side `fallbacks` parameter (beta header `server-side-fallback-2026-06-01`, fallback model `claude-opus-4-8`, on the beta messages call) by default. The exact Java builder methods (and the client-side middleware for providers without server-side support) are not documented here - WebFetch the Java SDK repo's `examples/` from `shared/live-sources.md`; full semantics in `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason.
---
## Error Type
`AnthropicServiceException` exposes `.errorType()` returning `Optional<ErrorType>` for programmatic error classification:
```java
try {
client.messages().create(params);
} catch (AnthropicServiceException e) {
e.errorType().ifPresent(type ->
System.out.println("Error type: " + type) // RATE_LIMIT_ERROR, OVERLOADED_ERROR, etc.
);
}
```
---
# Files API - Java
## Files API
> **Out of beta.** In current SDKs `client.beta().files()` has breaking shape changes from previous versions, matching the stable `client.files()` - migrate per the Files API row in `shared/live-sources.md`. Examples below predate this.
Under `client.beta().files()`. File references in messages need the beta message types (non-beta `DocumentBlockParam.Source` has no file-ID variant).
```java
import com.anthropic.models.beta.files.FileUploadParams;
import com.anthropic.models.beta.files.FileMetadata;
import com.anthropic.models.beta.messages.BetaRequestDocumentBlock;
import com.anthropic.models.beta.messages.BetaFileDocumentSource;
import java.nio.file.Paths;
FileMetadata meta = client.beta().files().upload(
FileUploadParams.builder()
.file(Paths.get("/path/to/doc.pdf")) // or .file(InputStream) or .file(byte[])
.build());
// Reference in a beta message:
BetaRequestDocumentBlock doc = BetaRequestDocumentBlock.builder()
.source(BetaFileDocumentSource.builder().fileId(meta.id()).build())
.build();
```
Other methods: `.list()`, `.delete(String fileId)`, `.download(String fileId)`, `.retrieveMetadata(String fileId)`.
# Streaming - Java
## Streaming
```java
import com.anthropic.core.http.StreamResponse;
import com.anthropic.models.messages.RawMessageStreamEvent;
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(64000L)
.addUserMessage("Write a haiku")
.build();
try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(params)) {
streamResponse.stream()
.flatMap(event -> event.contentBlockDelta().stream())
.flatMap(deltaEvent -> deltaEvent.delta().text().stream())
.forEach(textDelta -> System.out.print(textDelta.text()));
}
```
---
# Tool Use - Java
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Use (Beta)
The Java SDK supports beta tool use with annotated classes. Tool classes implement `Supplier<String>` for automatic execution via `BetaToolRunner`.
### Tool Runner (automatic loop)
```java
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.helpers.BetaToolRunner;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.function.Supplier;
@JsonClassDescription("Get the weather in a given location")
static class GetWeather implements Supplier<String> {
@JsonPropertyDescription("The city and state, e.g. San Francisco, CA")
public String location;
@Override
public String get() {
return "The weather in " + location + " is sunny and 72°F";
}
}
BetaToolRunner toolRunner = client.beta().messages().toolRunner(
MessageCreateParams.builder()
.model("claude-opus-5")
.maxTokens(16000L)
.putAdditionalHeader("anthropic-beta", "structured-outputs-2025-11-13")
.addTool(GetWeather.class)
.addUserMessage("What's the weather in San Francisco?")
.build());
for (BetaMessage message : toolRunner) {
System.out.println(message);
}
```
### Memory Tool
The Java SDK provides `BetaMemoryToolHandler` for implementing the memory tool backend. You supply a handler that manages file storage, and the `BetaToolRunner` handles memory tool calls automatically.
```java
import com.anthropic.helpers.BetaMemoryToolHandler;
import com.anthropic.helpers.BetaToolRunner;
import com.anthropic.models.beta.messages.BetaMemoryTool20250818;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.beta.messages.ToolRunnerCreateParams;
// Implement BetaMemoryToolHandler with your storage backend (e.g., filesystem)
BetaMemoryToolHandler memoryHandler = new FileSystemMemoryToolHandler(sandboxRoot);
MessageCreateParams createParams = MessageCreateParams.builder()
.model("claude-opus-5")
.maxTokens(4096L)
.addTool(BetaMemoryTool20250818.builder().build())
.addUserMessage("Remember that my favorite color is blue")
.build();
BetaToolRunner toolRunner = client.beta().messages().toolRunner(
ToolRunnerCreateParams.builder()
.betaMemoryToolHandler(memoryHandler)
.initialMessageParams(createParams)
.build());
for (BetaMessage message : toolRunner) {
System.out.println(message);
}
```
See the [shared memory tool concepts](../../shared/tool-use-concepts.md) for more details on the memory tool.
### Non-Beta Tool Declaration (manual JSON schema)
`Tool.InputSchema.Properties` is a freeform `Map<String, JsonValue>` wrapper - build property schemas via `putAdditionalProperty`. `type: "object"` is the default. The builder has a direct `.addTool(Tool)` overload that wraps in `ToolUnion` automatically.
```java
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.Tool;
Tool tool = Tool.builder()
.name("get_weather")
.description("Get the current weather in a given location")
.inputSchema(Tool.InputSchema.builder()
.properties(Tool.InputSchema.Properties.builder()
.putAdditionalProperty("location", JsonValue.from(Map.of("type", "string")))
.build())
.required(List.of("location"))
.build())
.build();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_SONNET_4_6)
.maxTokens(16000L)
.addTool(tool)
.addUserMessage("Weather in Paris?")
.build();
```
For manual tool loops, handle `tool_use` blocks in the response, send `tool_result` back, loop until `stop_reason` is `"end_turn"`. See [shared tool use concepts](../../shared/tool-use-concepts.md).
### Building `MessageParam` with Content Blocks (Tool Result Round-Trip)
`MessageParam.Content` is an inner union class (string | list). Use the builder's `.contentOfBlockParams(List<ContentBlockParam>)` alias - there is NO separate `MessageParamContent` class with a static `ofBlockParams`:
```java
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.ToolResultBlockParam;
List<ContentBlockParam> results = List.of(
ContentBlockParam.ofToolResult(ToolResultBlockParam.builder()
.toolUseId(toolUseBlock.id())
.content(yourResultString)
.build())
);
MessageParam toolResultMsg = MessageParam.builder()
.role(MessageParam.Role.USER)
.contentOfBlockParams(results) // builder alias for Content.ofBlockParams(...)
.build();
```
---
## Structured Output
The class-based overload auto-derives the JSON schema from your POJO and gives you a typed `.text()` return - no manual schema, no manual parsing.
```java
import com.anthropic.models.messages.StructuredMessageCreateParams;
record Book(String title, String author) {}
record BookList(List<Book> books) {}
StructuredMessageCreateParams<BookList> params = MessageCreateParams.builder()
.model(Model.CLAUDE_SONNET_4_6)
.maxTokens(16000L)
.outputConfig(BookList.class) // returns a typed builder
.addUserMessage("List 3 classic novels")
.build();
client.messages().create(params).content().stream()
.flatMap(cb -> cb.text().stream())
.forEach(typed -> {
// typed.text() returns BookList, not String
for (Book b : typed.text().books()) System.out.println(b.title());
});
```
Supports Jackson annotations: `@JsonPropertyDescription`, `@JsonIgnore`, `@ArraySchema(minItems=...)`. Manual schema path: `OutputConfig.builder().format(JsonOutputFormat.builder().schema(...).build())`.
---
## Anthropic-Defined Tools
Version-suffixed types; `name`/`type` auto-set by builder. Direct `.addTool()` overloads exist for most tool types; where one is missing (newer or less-common tools - see the advisor note below), wrap via the union type's static factory: `.addTool(BetaToolUnion.of<ToolName>(builder...build()))`. Web search and code execution are server-executed; bash and text editor are client-executed (you handle the `tool_use` locally - see `shared/tool-use-concepts.md`).
```java
import com.anthropic.models.messages.WebSearchTool20260209;
import com.anthropic.models.messages.ToolBash20250124;
import com.anthropic.models.messages.ToolTextEditor20250728;
import com.anthropic.models.messages.CodeExecutionTool20260120;
.addTool(WebSearchTool20260209.builder()
.maxUses(5L) // optional
.allowedDomains(List.of("example.com")) // optional
.build())
.addTool(ToolBash20250124.builder().build())
.addTool(ToolTextEditor20250728.builder().build())
.addTool(CodeExecutionTool20260120.builder().build())
```
Also available: `WebFetchTool20260209`, `MemoryTool20250818`, `ToolSearchToolBm25_20251119`. For the advisor tool, use `BetaAdvisorTool20260301` in the beta namespace with `.addBeta("advisor-tool-2026-03-01")` (server-side; advisor model >= executor model). There is no direct `.addTool(BetaAdvisorTool20260301)` overload on the beta builder - wrap it via the `BetaToolUnion` static factory for the advisor type; if `javac` rejects the specific factory method name, `javap com.anthropic.models.beta.messages.BetaToolUnion | grep -i advisor` shows the exact one.
### Beta namespace (MCP, compaction)
For beta-only features use `com.anthropic.models.beta.messages.*` - class names have a `Beta` prefix AND live in the beta package. The beta `MessageCreateParams.Builder` has direct `.addTool(BetaToolBash20250124)` overloads AND `.addMcpServer()`:
```java
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.beta.messages.BetaToolBash20250124;
import com.anthropic.models.beta.messages.BetaCodeExecutionTool20260120;
import com.anthropic.models.beta.messages.BetaRequestMcpServerUrlDefinition;
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(16000L)
.addBeta("mcp-client-2025-11-20")
.addTool(BetaToolBash20250124.builder().build())
.addTool(BetaCodeExecutionTool20260120.builder().build())
.addMcpServer(BetaRequestMcpServerUrlDefinition.builder()
.name("my-server")
.url("https://example.com/mcp")
.build())
.addUserMessage("...")
.build();
client.beta().messages().create(params);
```
`BetaTool*` types are NOT interchangeable with non-beta `Tool*` - pick one namespace per request.
**Reading server-tool blocks in the response:** `ServerToolUseBlock` has `.id()`, `.name()` (enum), and `._input()` returning raw `JsonValue` - there is NO typed `.input()`. For code execution results, unwrap two levels:
```java
for (ContentBlock block : response.content()) {
block.serverToolUse().ifPresent(stu -> {
System.out.println("tool: " + stu.name() + " input: " + stu._input());
});
block.codeExecutionToolResult().ifPresent(r -> {
r.content().resultBlock().ifPresent(result -> {
System.out.println("stdout: " + result.stdout());
System.out.println("stderr: " + result.stderr());
System.out.println("exit: " + result.returnCode());
});
});
}
```
---
# Managed Agents - Java
> **Bindings not shown here:** This README covers the most common managed-agents flows for Java. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Java SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK.
> **Agents are persistent - create once, reference by ID.** Store the agent ID returned by `client.beta().agents().create` and pass it to every subsequent `client.beta().sessions().create`; do not call `agents().create` in the request path. **Recommended:** define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md` (its live-docs URL is in `shared/live-sources.md`). The CLI owns the control plane (create/update); your code owns the data plane (sessions with the stored ID). The examples below show in-code creation for when you must provision programmatically; in production the create call belongs in setup, not in the request path.
## Installation
```xml
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
</dependency>
```
## Client Initialization
```java
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
// Default (uses ANTHROPIC_API_KEY env var)
var client = AnthropicOkHttpClient.fromEnv();
```
---
## Create an Environment
```java
import com.anthropic.models.beta.environments.BetaCloudConfigParams;
import com.anthropic.models.beta.environments.BetaUnrestrictedNetwork;
import com.anthropic.models.beta.environments.EnvironmentCreateParams;
var environment = client.beta().environments().create(EnvironmentCreateParams.builder()
.name("my-dev-env")
.config(BetaCloudConfigParams.builder()
.networking(BetaUnrestrictedNetwork.builder().build())
.build())
.build());
System.out.println("Environment ID: " + environment.id()); // env_...
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** Model, system, and tools live on the agent object, not the session. Always start with `client.beta().agents().create()` - the session takes either `.agent(agent.id())` or the typed `BetaManagedAgentsAgentParams.builder()...build()`.
### Minimal
```java
import com.anthropic.models.beta.agents.AgentCreateParams;
import com.anthropic.models.beta.agents.BetaManagedAgentsAgentToolset20260401Params;
import com.anthropic.models.beta.sessions.BetaManagedAgentsAgentParams;
import com.anthropic.models.beta.sessions.SessionCreateParams;
// 1. Create the agent (reusable, versioned)
var agent = client.beta().agents().create(AgentCreateParams.builder()
.name("Coding Assistant")
.model("claude-opus-5")
.system("You are a helpful coding assistant.")
.addTool(BetaManagedAgentsAgentToolset20260401Params.builder()
.type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401)
.build())
.build());
// 2. Start a session
var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(BetaManagedAgentsAgentParams.builder()
.type(BetaManagedAgentsAgentParams.Type.AGENT)
.id(agent.id())
.version(agent.version())
.build())
.environmentId(environment.id())
.title("Quickstart session")
.build());
System.out.println("Session ID: " + session.id());
System.out.println("Trace: https://platform.claude.com/workspaces/default/sessions/" + session.id()); // swap 'default' for your workspace ID if the API key is not in the Default workspace
```
### Updating an Agent
Updates create new versions; the agent object is immutable per version.
```java
import com.anthropic.models.beta.agents.AgentUpdateParams;
var updatedAgent = client.beta().agents().update(agent.id(), AgentUpdateParams.builder()
.version(agent.version())
.system("You are a helpful coding agent. Always write tests.")
.build());
System.out.println("New version: " + updatedAgent.version());
// List all versions
for (var version : client.beta().agents().versions().list(agent.id()).autoPager()) {
System.out.println("Version " + version.version() + ": " + version.updatedAt());
}
// Archive the agent
var archived = client.beta().agents().archive(agent.id());
System.out.println("Archived at: " + archived.archivedAt().orElseThrow());
```
---
## Send a User Message
```java
import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams;
import com.anthropic.models.beta.sessions.events.EventSendParams;
client.beta().sessions().events().send(session.id(), EventSendParams.builder()
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("Review the auth module")
.build())
.build());
```
> Tip: **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens - stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns).
---
## Stream Events (SSE)
```java
import com.anthropic.models.beta.sessions.events.StreamEvents;
// Open the stream first, then send the user message
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
client.beta().sessions().events().send(session.id(), EventSendParams.builder()
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("Summarize the repo README")
.build())
.build());
for (var event : (Iterable<StreamEvents>) stream.stream()::iterator) {
if (event.isAgentMessage()) {
event.asAgentMessage().content().forEach(block -> System.out.print(block.text()));
} else if (event.isAgentToolUse()) {
System.out.println("\n[Using tool: " + event.asAgentToolUse().name() + "]");
} else if (event.isSessionStatusIdle()) {
break;
} else if (event.isSessionError()) {
System.out.println("\n[Error]");
break;
}
}
}
```
### Reconnecting and Tailing
When reconnecting mid-session, list past events first to dedupe, then tail live events. The cross-variant `id` field is read from the raw `_json()` value:
```java
import com.anthropic.core.JsonValue;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
// Stream is open and buffering. List history before tailing live.
var seenEventIds = new HashSet<String>();
for (var past : client.beta().sessions().events().list(session.id()).autoPager()) {
Optional<Map<String, JsonValue>> obj = past._json().orElseThrow().asObject();
seenEventIds.add(obj.orElseThrow().get("id").asStringOrThrow());
}
// Tail live events, skipping anything already seen
for (var event : (Iterable<StreamEvents>) stream.stream()::iterator) {
Optional<Map<String, JsonValue>> obj = event._json().orElseThrow().asObject();
if (!seenEventIds.add(obj.orElseThrow().get("id").asStringOrThrow())) continue;
if (event.isAgentMessage()) {
event.asAgentMessage().content().forEach(block -> System.out.print(block.text()));
} else if (event.isSessionStatusIdle()) {
break;
}
}
}
```
---
## Provide Custom Tool Result
> Note: The Java managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `anthropic-java` repository for the corresponding params types.
---
## Poll Events
```java
for (var event : client.beta().sessions().events().list(session.id()).autoPager()) {
System.out.println(event.type() + ": " + event);
}
```
---
## Upload a File
```java
import com.anthropic.models.beta.files.FileUploadParams;
import com.anthropic.models.beta.sessions.BetaManagedAgentsFileResourceParams;
import java.nio.file.Path;
var dataCsv = Path.of("data.csv");
var file = client.beta().files().upload(FileUploadParams.builder()
.file(dataCsv)
.build());
System.out.println("File ID: " + file.id());
// Mount in a session
var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(agent.id())
.environmentId(environment.id())
.addResource(BetaManagedAgentsFileResourceParams.builder()
.type(BetaManagedAgentsFileResourceParams.Type.FILE)
.fileId(file.id())
.mountPath("/workspace/data.csv")
.build())
.build());
```
### Add and Manage Resources on an Existing Session
```java
import com.anthropic.models.beta.sessions.resources.ResourceAddParams;
import com.anthropic.models.beta.sessions.resources.ResourceDeleteParams;
// Attach an additional file to an open session
var resource = client.beta().sessions().resources().add(session.id(), ResourceAddParams.builder()
.betaManagedAgentsFileResourceParams(BetaManagedAgentsFileResourceParams.builder()
.type(BetaManagedAgentsFileResourceParams.Type.FILE)
.fileId(file.id())
.build())
.build());
System.out.println(resource.id()); // "sesrsc_01ABC..."
// List resources on the session - entries are a discriminated union
var listed = client.beta().sessions().resources().list(session.id());
for (var entry : listed.data()) {
if (entry.isFile()) {
var fileResource = entry.asFile();
System.out.println(fileResource.id() + " " + fileResource.type());
} else if (entry.isGitHubRepository()) {
var repoResource = entry.asGitHubRepository();
System.out.println(repoResource.id() + " " + repoResource.type());
}
}
// Detach a resource
client.beta().sessions().resources().delete(resource.id(), ResourceDeleteParams.builder()
.sessionId(session.id())
.build());
```
---
## List and Download Session Files
> Note: Listing and downloading files an agent wrote during a session is not yet documented for Java in this skill or in the apps source examples. See `shared/managed-agents-events.md` and the `anthropic-java` repository for the file list/download bindings.
---
## Session Management
```java
// List environments
var environments = client.beta().environments().list();
// Retrieve a specific environment
var env = client.beta().environments().retrieve(environment.id());
// Archive an environment (read-only, existing sessions continue)
client.beta().environments().archive(environment.id());
// Delete an environment (only if no sessions reference it)
client.beta().environments().delete(environment.id());
// Delete a session
client.beta().sessions().delete(session.id());
```
---
## MCP Server Integration
```java
import com.anthropic.models.beta.agents.BetaManagedAgentsMcpToolsetParams;
import com.anthropic.models.beta.agents.BetaManagedAgentsUrlMcpServerParams;
// Agent declares MCP server (no auth here - auth goes in a vault)
var agent = client.beta().agents().create(AgentCreateParams.builder()
.name("GitHub Assistant")
.model("claude-opus-5")
.addMcpServer(BetaManagedAgentsUrlMcpServerParams.builder()
.type(BetaManagedAgentsUrlMcpServerParams.Type.URL)
.name("github")
.url("https://api.githubcopilot.com/mcp/")
.build())
.addTool(BetaManagedAgentsAgentToolset20260401Params.builder()
.type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401)
.build())
.addTool(BetaManagedAgentsMcpToolsetParams.builder()
.type(BetaManagedAgentsMcpToolsetParams.Type.MCP_TOOLSET)
.mcpServerName("github")
.build())
.build());
// Session attaches vault(s) containing credentials for those MCP server URLs
var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(BetaManagedAgentsAgentParams.builder()
.type(BetaManagedAgentsAgentParams.Type.AGENT)
.id(agent.id())
.version(agent.version())
.build())
.environmentId(environment.id())
.addVaultId(vault.id())
.build());
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.
---
## Vaults
```java
import com.anthropic.core.JsonValue;
import com.anthropic.models.beta.vaults.VaultCreateParams;
import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthCreateParams;
import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthRefreshParams;
import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthRefreshUpdateParams;
import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthUpdateParams;
import com.anthropic.models.beta.vaults.credentials.CredentialCreateParams;
import com.anthropic.models.beta.vaults.credentials.CredentialUpdateParams;
import java.time.OffsetDateTime;
// Create a vault
var vault = client.beta().vaults().create(VaultCreateParams.builder()
.displayName("Alice")
.metadata(VaultCreateParams.Metadata.builder()
.putAdditionalProperty("external_user_id", JsonValue.from("usr_abc123"))
.build())
.build());
System.out.println(vault.id()); // "vlt_01ABC..."
// Add an OAuth credential
var credential = client.beta().vaults().credentials().create(vault.id(),
CredentialCreateParams.builder()
.displayName("Alice's Slack")
.auth(BetaManagedAgentsMcpOAuthCreateParams.builder()
.type(BetaManagedAgentsMcpOAuthCreateParams.Type.MCP_OAUTH)
.mcpServerUrl("https://mcp.slack.com/mcp")
.accessToken("xoxp-...")
.expiresAt(OffsetDateTime.parse("2026-04-15T00:00:00Z"))
.refresh(BetaManagedAgentsMcpOAuthRefreshParams.builder()
.tokenEndpoint("https://slack.com/api/oauth.v2.access")
.clientId("1234567890.0987654321")
.scope("channels:read chat:write")
.refreshToken("xoxe-1-...")
.clientSecretPostTokenEndpointAuth("abc123...")
.build())
.build())
.build());
// Rotate the credential (e.g., after a token refresh)
client.beta().vaults().credentials().update(credential.id(),
CredentialUpdateParams.builder()
.vaultId(vault.id())
.auth(BetaManagedAgentsMcpOAuthUpdateParams.builder()
.type(BetaManagedAgentsMcpOAuthUpdateParams.Type.MCP_OAUTH)
.accessToken("xoxp-new-...")
.expiresAt(OffsetDateTime.parse("2026-05-15T00:00:00Z"))
.refresh(BetaManagedAgentsMcpOAuthRefreshUpdateParams.builder()
.refreshToken("xoxe-1-new-...")
.build())
.build())
.build());
// Archive a vault
client.beta().vaults().archive(vault.id());
```
---
## GitHub Repository Integration
Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential):
```java
import com.anthropic.models.beta.sessions.BetaManagedAgentsGitHubRepositoryResourceParams;
var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(agent.id())
.environmentId(environment.id())
.addVaultId(vault.id())
.addResource(BetaManagedAgentsGitHubRepositoryResourceParams.builder()
.type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY)
.url("https://github.com/org/repo")
.mountPath("/workspace/repo")
.authorizationToken("ghp_your_github_token")
.build())
.build());
```
Multiple repositories on the same session:
```java
import java.util.List;
var resources = List.of(
BetaManagedAgentsGitHubRepositoryResourceParams.builder()
.type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY)
.url("https://github.com/org/frontend")
.mountPath("/workspace/frontend")
.authorizationToken("ghp_your_github_token")
.build(),
BetaManagedAgentsGitHubRepositoryResourceParams.builder()
.type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY)
.url("https://github.com/org/backend")
.mountPath("/workspace/backend")
.authorizationToken("ghp_your_github_token")
.build());
```
Rotating a repository's authorization token:
```java
import com.anthropic.models.beta.sessions.resources.ResourceUpdateParams;
var listed = client.beta().sessions().resources().list(session.id());
var repoResourceId = listed.data().get(0).asGitHubRepository().id();
client.beta().sessions().resources().update(repoResourceId, ResourceUpdateParams.builder()
.sessionId(session.id())
.authorizationToken("ghp_your_new_github_token")
.build());
```
# Claude API - PHP
> **Note:** The PHP SDK is the official Anthropic SDK for PHP. A beta tool runner is available via `$client->beta->messages->toolRunner()`. Structured output helpers are supported via `StructuredOutputModel` classes. Agent SDK is not available. Bedrock, Vertex AI, and Foundry clients are supported.
## Installation
```bash
composer require "anthropic-ai/sdk"
```
## Client Initialization
```php
use Anthropic\Client;
// Using API key from environment variable
$client = new Client(apiKey: getenv("ANTHROPIC_API_KEY"));
```
### Amazon Bedrock
```php
use Anthropic\Bedrock\MantleClient;
// Messages-API Bedrock endpoint. Reads AWS credentials from env.
$client = new MantleClient(awsRegion: 'us-east-1');
```
Model IDs on Bedrock take an `anthropic.` prefix - e.g. `model: 'anthropic.claude-opus-5'`.
### Google Vertex AI
```php
use Anthropic\Vertex;
// Constructor is private. Parameter is `location`, not `region`.
$client = Vertex\Client::fromEnvironment(
location: 'us-east5',
projectId: 'my-project-id',
);
```
### Anthropic Foundry
```php
use Anthropic\Foundry;
// Constructor is private. baseUrl or resource is required.
$client = Foundry\Client::withCredentials(
apiKey: getenv('ANTHROPIC_FOUNDRY_API_KEY'),
baseUrl: 'https://<resource>.services.ai.azure.com/anthropic/v1',
);
```
---
## Basic Message Request
```php
$message = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
messages: [
['role' => 'user', 'content' => 'What is the capital of France?'],
],
);
// content is an array of polymorphic blocks (TextBlock, ToolUseBlock,
// ThinkingBlock). Accessing ->text on content[0] without checking the block
// type will throw if the first block is not a TextBlock (e.g., when extended
// thinking is enabled and a ThinkingBlock comes first). Always guard:
foreach ($message->content as $block) {
if ($block->type === 'text') {
echo $block->text;
}
}
```
If you only want the first text block:
```php
foreach ($message->content as $block) {
if ($block->type === 'text') {
echo $block->text;
break;
}
}
```
---
## Extended Thinking
**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think.
```php
use Anthropic\Messages\ThinkingBlock;
$message = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
thinking: ['type' => 'adaptive', 'display' => 'summarized'], // display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Claude Opus 5 / Opus 4.8 / 4.7
messages: [
['role' => 'user', 'content' => 'Solve: 27 * 453'],
],
);
// ThinkingBlock(s) precede TextBlock in content
foreach ($message->content as $block) {
if ($block instanceof ThinkingBlock) {
echo "Thinking:\n{$block->thinking}\n\n";
// $block->signature is an opaque string - preserve verbatim if
// passing thinking blocks back in multi-turn conversations
} elseif ($block->type === 'text') {
echo "Answer: {$block->text}\n";
}
}
```
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking (above). `['type' => 'enabled', 'budgetTokens' => N]` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `thinking:` runs adaptive (`['type' => 'adaptive']` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `['type' => 'disabled']` is accepted only at effort `high` or lower; pairing it with `xhigh`/`max` returns a 400.
> **Older models:** Use `thinking: ['type' => 'enabled', 'budgetTokens' => N]` (budget must be < `maxTokens`, min 1024).
`$block->type === 'thinking'` also works for the check; `instanceof` narrows for PHPStan.
---
## Prompt Caching
`system:` takes an array of text blocks; set `cacheControl` on the last block. Array-shape syntax (camelCase keys) is idiomatic. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`.
```php
$message = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
system: [
['type' => 'text', 'text' => $longSystemPrompt, 'cacheControl' => ['type' => 'ephemeral']],
],
messages: [['role' => 'user', 'content' => 'Summarize the key points']],
);
```
For 1-hour TTL: `'cacheControl' => ['type' => 'ephemeral', 'ttl' => '1h']`. There's also a top-level `cacheControl:` on `messages->create(...)` that auto-places on the last cacheable block.
Verify hits via `$message->usage->cacheCreationInputTokens` / `$message->usage->cacheReadInputTokens`.
---
## Stop Details
When `stopReason` is `'refusal'`, the response includes structured `stopDetails`:
```php
if ($message->stopReason === 'refusal' && $message->stopDetails !== null) {
echo "Category: " . $message->stopDetails->category . "\n"; // e.g. "cyber", "bio", "reasoning_extraction", "frontier_llm", or null - see docs for the full set
echo "Explanation: " . $message->stopDetails->explanation . "\n";
}
```
**Refusal fallbacks (Claude Fable 5.1) - opt in by default.** Fallbacks are opt-in: without them a refused request simply stops. New `claude-fable-5-1` code should include the server-side `fallbacks` parameter (beta header `server-side-fallback-2026-06-01`, fallback model `claude-opus-4-8`, on the beta messages call) by default. The exact PHP binding (and the client-side middleware for providers without server-side support) is not documented here - WebFetch the PHP SDK repo's `examples/` from `shared/live-sources.md`; full semantics in `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason.
---
## Error Type
`APIStatusException` exposes a `->type` property for programmatic error classification:
```php
try {
$client->messages->create(...);
} catch (\Anthropic\Core\Exceptions\APIStatusException $e) {
echo $e->type?->value; // "rate_limit_error", "overloaded_error", etc.
}
```
# Message Batches - PHP
## Message Batches API
```php
$batch = $client->messages->batches->create(requests: [
['customId' => 'req-1', 'params' => ['model' => 'claude-opus-5', 'maxTokens' => 1024, 'messages' => [...]]],
['customId' => 'req-2', 'params' => [...]],
]);
// Poll $client->messages->batches->retrieve($batch->id) until processingStatus === 'ended',
// then iterate $client->messages->batches->results($batch->id).
```
---
# Files API - PHP
## Files API
> **Out of beta.** In current SDKs `$client->beta->files` has breaking shape changes from previous versions, matching the stable `$client->files` - migrate per the Files API row in `shared/live-sources.md`. Example below predates this.
```php
$file = $client->beta->files->upload(
file: fopen('upload_me.txt', 'r'),
betas: ['files-api-2025-04-14'],
);
// Reference $file->id as a file content block on ->beta->messages->create().
```
# Streaming - PHP
## Streaming
> **Requires SDK v0.5.0+.** v0.4.0 and earlier used a single `$params` array; calling with named parameters throws `Unknown named parameter $model`. Upgrade: `composer require "anthropic-ai/sdk:^0.7"`
```php
use Anthropic\Messages\RawContentBlockDeltaEvent;
use Anthropic\Messages\TextDelta;
$stream = $client->messages->createStream(
model: 'claude-opus-5',
maxTokens: 64000,
messages: [
['role' => 'user', 'content' => 'Write a haiku'],
],
);
foreach ($stream as $event) {
if ($event instanceof RawContentBlockDeltaEvent && $event->delta instanceof TextDelta) {
echo $event->delta->text;
}
}
```
---
# Tool Use - PHP
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Use
### Tool Runner (Beta)
**Beta:** The PHP SDK provides a tool runner via `$client->beta->messages->toolRunner()`. Define tools with `BetaRunnableTool` - a definition array plus a `run` closure:
```php
use Anthropic\Lib\Tools\BetaRunnableTool;
$weatherTool = new BetaRunnableTool(
definition: [
'name' => 'get_weather',
'description' => 'Get the current weather for a location.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'location' => ['type' => 'string', 'description' => 'City and state'],
],
'required' => ['location'],
],
],
run: function (array $input): string {
return "The weather in {$input['location']} is sunny and 72°F.";
},
);
$runner = $client->beta->messages->toolRunner(
maxTokens: 16000,
messages: [['role' => 'user', 'content' => 'What is the weather in Paris?']],
model: 'claude-opus-5',
tools: [$weatherTool],
);
foreach ($runner as $message) {
foreach ($message->content as $block) {
if ($block->type === 'text') {
echo $block->text;
}
}
}
```
### Manual Loop
Tools are passed as arrays. **The SDK uses camelCase keys** (`inputSchema`, `toolUseID`, `stopReason`) and auto-maps to the API's snake_case on the wire - since v0.5.0. See [shared tool use concepts](../../shared/tool-use-concepts.md) for the loop pattern.
```php
use Anthropic\Messages\ToolUseBlock;
$tools = [
[
'name' => 'get_weather',
'description' => 'Get the current weather in a given location',
'inputSchema' => [ // camelCase, not input_schema
'type' => 'object',
'properties' => [
'location' => ['type' => 'string', 'description' => 'City and state'],
],
'required' => ['location'],
],
],
];
$messages = [['role' => 'user', 'content' => 'What is the weather in SF?']];
$response = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
tools: $tools,
messages: $messages,
);
while ($response->stopReason === 'tool_use') { // camelCase property
$toolResults = [];
foreach ($response->content as $block) {
if ($block instanceof ToolUseBlock) {
// $block->name : string - tool name to dispatch on
// $block->input : array<string,mixed> - parsed JSON input
// $block->id : string - pass back as toolUseID
$result = executeYourTool($block->name, $block->input);
$toolResults[] = [
'type' => 'tool_result',
'toolUseID' => $block->id, // camelCase, not tool_use_id
'content' => $result,
];
}
}
// Append assistant turn + user turn with tool results
$messages[] = ['role' => 'assistant', 'content' => $response->content];
$messages[] = ['role' => 'user', 'content' => $toolResults];
$response = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
tools: $tools,
messages: $messages,
);
}
// Final text response
foreach ($response->content as $block) {
if ($block->type === 'text') {
echo $block->text;
}
}
```
`$block->type === 'tool_use'` also works; `instanceof ToolUseBlock` narrows for PHPStan.
---
## Structured Outputs
### Using StructuredOutputModel (Recommended)
Define a PHP class implementing `StructuredOutputModel` and pass it as `outputConfig`:
```php
use Anthropic\Lib\Contracts\StructuredOutputModel;
use Anthropic\Lib\Concerns\StructuredOutputModelTrait;
use Anthropic\Lib\Attributes\Constrained;
class Person implements StructuredOutputModel
{
use StructuredOutputModelTrait;
#[Constrained(description: 'Full name')]
public string $name;
public int $age;
public ?string $email = null; // nullable = optional field
}
$message = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
messages: [['role' => 'user', 'content' => 'Generate a profile for Alice, age 30']],
outputConfig: ['format' => Person::class],
);
$person = $message->parsedOutput(); // Person instance
echo $person->name;
```
Types are inferred from PHP type hints. Use `#[Constrained(description: '...')]` to add descriptions. Nullable properties (`?string`) become optional fields.
### Raw Schema
```php
$message = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
messages: [['role' => 'user', 'content' => 'Extract: John ([email protected]), Enterprise plan']],
outputConfig: [
'format' => [
'type' => 'json_schema',
'schema' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'email' => ['type' => 'string'],
'plan' => ['type' => 'string'],
],
'required' => ['name', 'email', 'plan'],
'additionalProperties' => false,
],
],
],
);
// First text block contains valid JSON
foreach ($message->content as $block) {
if ($block->type === 'text') {
$data = json_decode($block->text, true);
break;
}
}
```
---
## Beta Features & Anthropic-Defined Tools
**`betas:` is NOT a param on `$client->messages->create()`** - it only exists on the beta namespace. Use it for features that need an explicit opt-in header:
```php
use Anthropic\Beta\Messages\BetaRequestMCPServerURLDefinition;
$response = $client->beta->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
mcpServers: [
BetaRequestMCPServerURLDefinition::with(
name: 'my-server',
url: 'https://example.com/mcp',
),
],
betas: ['mcp-client-2025-11-20'], // only valid on ->beta->messages
messages: [['role' => 'user', 'content' => 'Use the MCP tools']],
);
```
### Task budgets
```php
$response = $client->beta->messages->create(
model: 'claude-opus-5',
maxTokens: 16000,
outputConfig: ['taskBudget' => ['type' => 'tokens', 'total' => 64000]],
tools: [...],
messages: [...],
betas: ['task-budgets-2026-03-13'],
);
```
### Cache diagnostics
Pass the previous response's `id` on the next request; print the `diagnostics` object on the response:
```php
$r2 = $client->beta->messages->create(
model: 'claude-opus-5', maxTokens: 1024,
diagnostics: ['previousMessageId' => $r1->id],
betas: ['cache-diagnosis-2026-04-07'],
messages: [...],
);
```
**Anthropic-defined tools** (bash, web_search, text_editor, code_execution) are GA and work on both paths. Of these, web_search and code_execution are server-executed; bash and text_editor are client-executed (you handle the `tool_use` locally) - `Anthropic\Messages\ToolBash20250124` / `WebSearchTool20260209` / `ToolTextEditor20250728` / `CodeExecutionTool20260120` for non-beta, `Anthropic\Beta\Messages\BetaToolBash20250124` / `BetaWebSearchTool20260209` / `BetaToolTextEditor20250728` / `BetaCodeExecutionTool20260120` for beta. No `betas:` header needed for these.
### Tool search (non-beta, server-side)
```php
tools: [
['type' => 'tool_search_tool_regex_20251119', 'name' => 'tool_search_tool_regex'],
['name' => 'get_weather', 'description' => '...', 'inputSchema' => [...], 'deferLoading' => true],
// ... other user tools with 'deferLoading' => true
],
```
### Memory tool (non-beta, client-executed)
Declare `['type' => 'memory_20250818', 'name' => 'memory']`. Handle the `tool_use` by reading/writing files under a fixed `/memories` directory. **Validate every model-supplied path**: resolve to its canonical form and verify it remains within the memory directory; reject traversal (`..`, symlinks) - see `shared/tool-use-concepts.md` § Client-Side Tools.
---
# Managed Agents - PHP
> **Bindings not shown here:** This README covers the most common managed-agents flows for PHP. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the PHP SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK.
> **Agents are persistent - create once, reference by ID.** Store the agent ID returned by `$client->beta->agents->create` and pass it to every subsequent `->sessions->create`; do not call `agents->create` in the request path. **Recommended:** define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md` (its live-docs URL is in `shared/live-sources.md`). The CLI owns the control plane (create/update); your code owns the data plane (sessions with the stored ID). The examples below show in-code creation for when you must provision programmatically; in production the create call belongs in setup, not in the request path.
## Installation
```bash
composer require "anthropic-ai/sdk" "guzzlehttp/guzzle:^7"
```
## Client Initialization
```php
use Anthropic\Client;
// Default (uses ANTHROPIC_API_KEY env var)
$client = new Client();
// Explicit API key
$client = new Client(apiKey: 'your-api-key');
```
---
## Create an Environment
```php
$environment = $client->beta->environments->create(
name: 'my-dev-env',
config: ['type' => 'cloud', 'networking' => ['type' => 'unrestricted']],
);
echo "Environment ID: {$environment->id}\n"; // env_...
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** `model`/`system`/`tools` live on the agent object, not the session. Always start with `$client->beta->agents->create()` - the session takes either `agent: $agent->id` or the typed `BetaManagedAgentsAgentParams::with(type: 'agent', id: $agent->id, version: $agent->version)`.
### Minimal
```php
use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params;
// 1. Create the agent (reusable, versioned)
$agent = $client->beta->agents->create(
name: 'Coding Assistant',
model: 'claude-opus-5',
system: 'You are a helpful coding assistant.',
tools: [
BetaManagedAgentsAgentToolset20260401Params::with(
type: 'agent_toolset_20260401',
),
],
);
// 2. Start a session
$session = $client->beta->sessions->create(
agent: ['type' => 'agent', 'id' => $agent->id, 'version' => $agent->version],
environmentID: $environment->id,
title: 'Quickstart session',
);
echo "Session ID: {$session->id}\n";
echo "Trace: https://platform.claude.com/workspaces/default/sessions/{$session->id}\n"; // swap 'default' for your workspace ID if the API key is not in the Default workspace
```
### Updating an Agent
Updates create new versions; the agent object is immutable per version.
```php
$updatedAgent = $client->beta->agents->update(
$agent->id,
version: $agent->version,
system: 'You are a helpful coding agent. Always write tests.',
);
echo "New version: {$updatedAgent->version}\n";
// List all versions
foreach ($client->beta->agents->versions->list($agent->id)->pagingEachItem() as $version) {
echo "Version {$version->version}: {$version->updatedAt->format(DateTimeInterface::ATOM)}\n";
}
// Archive the agent
$archived = $client->beta->agents->archive($agent->id);
echo "Archived at: {$archived->archivedAt->format(DateTimeInterface::ATOM)}\n";
```
---
## Send a User Message
```php
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.message',
'content' => [['type' => 'text', 'text' => 'Review the auth module']],
],
],
);
```
> Tip: **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens - stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns).
---
## Stream Events (SSE)
> Note: **Streaming transporter:** PHP's default buffered PSR-18 client never returns for the open-ended session event stream. Use a streaming Guzzle transporter for `streamStream()` calls - other calls keep the default client.
```php
$streamingClient = new GuzzleHttp\Client(['stream' => true]);
// Open the stream first, then send the user message
$stream = $client->beta->sessions->events->streamStream(
$session->id,
requestOptions: ['transporter' => $streamingClient],
);
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.message',
'content' => [['type' => 'text', 'text' => 'Summarize the repo README']],
],
],
);
foreach ($stream as $event) {
match ($event->type) {
'agent.message' => array_walk(
$event->content,
static fn($block) => $block->type === 'text' ? print($block->text) : null,
),
'agent.tool_use' => print("\n[Using tool: {$event->name}]\n"),
'session.error' => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'),
default => null,
};
if ($event->type === 'session.status_idle' || $event->type === 'session.error') {
break;
}
}
$stream->close();
```
### Reconnecting and Tailing
When reconnecting mid-session, list past events first to dedupe, then tail live events:
```php
$stream = $client->beta->sessions->events->streamStream(
$session->id,
requestOptions: ['transporter' => $streamingClient],
);
// Stream is open and buffering. List history before tailing live.
$seenEventIds = [];
foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) {
$seenEventIds[$event->id] = true;
}
// Tail live events, skipping anything already seen
foreach ($stream as $event) {
if (isset($seenEventIds[$event->id])) {
continue;
}
$seenEventIds[$event->id] = true;
match ($event->type) {
'agent.message' => array_walk(
$event->content,
static fn($block) => $block->type === 'text' ? print($block->text) : null,
),
default => null,
};
if ($event->type === 'session.status_idle') {
break;
}
}
$stream->close();
```
---
## Provide Custom Tool Result
> Note: The PHP managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `anthropic-ai/sdk` PHP repository for the corresponding params.
---
## Poll Events
```php
foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) {
echo "{$event->type}: {$event->id}\n";
}
```
---
## Upload a File
> Note: **PHP file upload:** The PHP SDK's beta managed-agents file upload binding is not shown in the apps source examples; the canonical PHP example uses raw cURL against `POST /v1/files`. If your codebase prefers the SDK, WebFetch the `anthropic-ai/sdk` PHP repository for the latest binding before writing code.
```php
use Anthropic\Beta\Sessions\BetaManagedAgentsFileResourceParams;
// Raw cURL upload (canonical example from the apps source)
$csvPath = 'data.csv';
$ch = curl_init('https://api.anthropic.com/v1/files');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('ANTHROPIC_API_KEY'),
'anthropic-version: 2023-06-01',
'anthropic-beta: files-api-2025-04-14',
],
CURLOPT_POSTFIELDS => ['file' => new CURLFile($csvPath, 'text/csv', 'data.csv')],
]);
$file = json_decode(curl_exec($ch));
echo "File ID: {$file->id}\n";
// Mount in a session
$session = $client->beta->sessions->create(
agent: $agent->id,
environmentID: $environment->id,
resources: [
BetaManagedAgentsFileResourceParams::with(
type: 'file',
fileID: $file->id,
mountPath: '/workspace/data.csv',
),
],
);
```
### Add and Manage Resources on an Existing Session
```php
// Attach an additional file to an open session
$resource = $client->beta->sessions->resources->add(
$session->id,
type: 'file',
fileID: $file->id,
);
echo "{$resource->id}\n"; // "sesrsc_01ABC..."
// List resources on the session
$listed = $client->beta->sessions->resources->list($session->id);
foreach ($listed->data as $entry) {
echo "{$entry->id} {$entry->type}\n";
}
// Detach a resource
$client->beta->sessions->resources->delete($resource->id, sessionID: $session->id);
```
---
## List and Download Session Files
```php
$files = $client->beta->files->list(
scopeID: 'sesn_abc123',
betas: ['managed-agents-2026-04-01'],
);
$content = $client->beta->files->download($files->data[0]->id);
file_put_contents('output.txt', $content);
```
---
## Session Management
```php
// List environments
$environments = $client->beta->environments->list();
// Retrieve a specific environment
$env = $client->beta->environments->retrieve($environment->id);
// Archive an environment (read-only, existing sessions continue)
$client->beta->environments->archive($environment->id);
// Delete an environment (only if no sessions reference it)
$client->beta->environments->delete($environment->id);
// Delete a session
$client->beta->sessions->delete($session->id);
```
---
## MCP Server Integration
```php
use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params;
use Anthropic\Beta\Agents\BetaManagedAgentsMCPToolsetParams;
use Anthropic\Beta\Agents\BetaManagedAgentsURLMCPServerParams;
use Anthropic\Beta\Sessions\BetaManagedAgentsAgentParams;
// Agent declares MCP server (no auth here - auth goes in a vault)
$agent = $client->beta->agents->create(
name: 'GitHub Assistant',
model: 'claude-opus-5',
mcpServers: [
BetaManagedAgentsURLMCPServerParams::with(
type: 'url',
name: 'github',
url: 'https://api.githubcopilot.com/mcp/',
),
],
tools: [
BetaManagedAgentsAgentToolset20260401Params::with(type: 'agent_toolset_20260401'),
BetaManagedAgentsMCPToolsetParams::with(
type: 'mcp_toolset',
mcpServerName: 'github',
),
],
);
// Session attaches vault(s) containing credentials for those MCP server URLs
$session = $client->beta->sessions->create(
agent: BetaManagedAgentsAgentParams::with(
type: 'agent',
id: $agent->id,
version: $agent->version,
),
environmentID: $environment->id,
vaultIDs: [$vault->id],
);
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.
---
## Vaults
```php
// Create a vault
$vault = $client->beta->vaults->create(
displayName: 'Alice',
metadata: ['external_user_id' => 'usr_abc123'],
);
echo $vault->id . "\n"; // "vlt_01ABC..."
// Add an OAuth credential
$credential = $client->beta->vaults->credentials->create(
vaultID: $vault->id,
displayName: "Alice's Slack",
auth: [
'type' => 'mcp_oauth',
'mcp_server_url' => 'https://mcp.slack.com/mcp',
'access_token' => 'xoxp-...',
'expires_at' => '2026-04-15T00:00:00Z',
'refresh' => [
'token_endpoint' => 'https://slack.com/api/oauth.v2.access',
'client_id' => '1234567890.0987654321',
'scope' => 'channels:read chat:write',
'refresh_token' => 'xoxe-1-...',
'token_endpoint_auth' => [
'type' => 'client_secret_post',
'client_secret' => 'abc123...',
],
],
],
);
// Rotate the credential (e.g., after a token refresh)
$client->beta->vaults->credentials->update(
$credential->id,
vaultID: $vault->id,
auth: [
'type' => 'mcp_oauth',
'access_token' => 'xoxp-new-...',
'expires_at' => '2026-05-15T00:00:00Z',
'refresh' => ['refresh_token' => 'xoxe-1-new-...'],
],
);
// Archive a vault
$client->beta->vaults->archive($vault->id);
```
---
## GitHub Repository Integration
Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential):
```php
$session = $client->beta->sessions->create(
agent: $agent->id,
environmentID: $environment->id,
vaultIDs: [$vault->id],
resources: [
[
'type' => 'github_repository',
'url' => 'https://github.com/org/repo',
'mount_path' => '/workspace/repo',
'authorization_token' => 'ghp_your_github_token',
],
],
);
```
Multiple repositories on the same session:
```php
$resources = [
[
'type' => 'github_repository',
'url' => 'https://github.com/org/frontend',
'mount_path' => '/workspace/frontend',
'authorization_token' => 'ghp_your_github_token',
],
[
'type' => 'github_repository',
'url' => 'https://github.com/org/backend',
'mount_path' => '/workspace/backend',
'authorization_token' => 'ghp_your_github_token',
],
];
```
Rotating a repository's authorization token:
```php
$listed = $client->beta->sessions->resources->list($session->id);
$repoResourceId = $listed->data[0]->id;
$client->beta->sessions->resources->update(
$repoResourceId,
sessionID: $session->id,
authorizationToken: 'ghp_your_new_github_token',
);
```
# Claude API - Python
## Installation
```bash
pip install anthropic
```
## Client Initialization
```python
import anthropic
# Default - resolves credentials from the environment:
# ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN, or an `ant auth login` profile.
# Prefer this for local dev; don't hardcode a key.
client = anthropic.Anthropic()
# Explicit API key (only when you must inject a specific key)
client = anthropic.Anthropic(api_key="your-api-key")
# Async client
async_client = anthropic.AsyncAnthropic()
```
---
## Client Configuration
### Per-request overrides
Use `with_options()` to override client settings for a single call without mutating the client:
```python
client.with_options(timeout=5.0, max_retries=5).messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
```
### Timeouts
Default request timeout is 10 minutes. Pass a float (seconds) or an `anthropic.Timeout` for granular control. On timeout the SDK raises `anthropic.APITimeoutError` (and retries per `max_retries`).
```python
client = anthropic.Anthropic(timeout=20.0)
client = anthropic.Anthropic(
timeout=anthropic.Timeout(60.0, read=5.0, write=10.0, connect=2.0),
)
```
`anthropic` 1.x is built on [`httpx2`](https://pypi.org/project/httpx2/), not `httpx`. `anthropic.Timeout` is `httpx2.Timeout`; if you import the HTTP library yourself, write `import httpx2 as httpx` - an object from the `httpx` package (`httpx.Timeout`, `httpx.Client`, transports, limits) is rejected or fails at request time. Existing `httpx`-era code is covered by the [v1 migration guide](https://github.com/anthropics/anthropic-sdk-python/blob/main/MIGRATION.md) and `/claude-api upgrade python`.
### Retries
The SDK auto-retries connection errors, 408, 409, 429, and >=500 with exponential backoff (default 2 retries). Set `max_retries` on the client or via `with_options()`; `max_retries=0` disables.
### Async performance (aiohttp backend)
For high-concurrency async workloads, install `anthropic[aiohttp]` and pass `DefaultAioHttpClient` instead of the default httpx2 backend:
```python
from anthropic import AsyncAnthropic, DefaultAioHttpClient
async with AsyncAnthropic(http_client=DefaultAioHttpClient()) as client:
...
```
### Custom HTTP client (proxy, base URL)
Use `DefaultHttpxClient` / `DefaultAsyncHttpxClient` - not a raw `httpx2.Client` (and never a client from the `httpx` package) - so the SDK's default timeouts and connection limits are preserved:
```python
from anthropic import Anthropic, DefaultHttpxClient
client = Anthropic(
base_url="http://my.test.server.example.com:8083", # or ANTHROPIC_BASE_URL env var
http_client=DefaultHttpxClient(proxy="http://my.test.proxy.example.com"),
)
```
### Logging
Set `ANTHROPIC_LOG=debug` (or `info`) to enable SDK logging via the standard `logging` module.
---
## Basic Message Request
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[
{"role": "user", "content": "What is the capital of France?"}
]
)
# response.content is a list of content block objects (TextBlock, ThinkingBlock,
# ToolUseBlock, ...). Check .type before accessing .text.
for block in response.content:
if block.type == "text":
print(block.text)
```
---
## System Prompts
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system="You are a helpful coding assistant. Always provide examples in Python.",
messages=[{"role": "user", "content": "How do I read a JSON file?"}]
)
```
### Mid-conversation system messages (model-gated)
For operator instructions that arrive mid-conversation (mode switches, injected state), append `{"role": "system", ...}` to `messages` instead of editing top-level `system` - this preserves the cached prefix and carries operator authority. Must follow a user message (or an `assistant` message ending in server-tool use), and must be either the last entry in `messages` or be followed by an `assistant` turn; cannot be `messages[0]`. Unsupported models return a 400 (`role 'system' is not supported on this model`). See `shared/prompt-caching.md` for when to use this vs. top-level `system`.
```python
response = client.messages.create(
model=MODEL_ID, # must support mid-conversation system messages
max_tokens=16000,
system=[{"type": "text", "text": STABLE_SYSTEM, "cache_control": {"type": "ephemeral"}}],
messages=history + [
{"role": "user", "content": user_message},
{"role": "system", "content": "Terse mode enabled - keep responses under 40 words."},
],
) # No beta header needed - use regular client.messages.create
```
---
## Vision (Images)
### Base64
```python
import base64
with open("image.png", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data
}
},
{"type": "text", "text": "What's in this image?"}
]
}]
)
```
### URL
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/image.png"
}
},
{"type": "text", "text": "Describe this image"}
]
}]
)
```
---
## Prompt Caching
Cache large context to reduce costs (up to 90% savings). **Caching is a prefix match** - any byte change anywhere in the prefix invalidates everything after it. For placement patterns, architectural guidance (frozen system prompt, deterministic tool order, where to put volatile content), and the silent-invalidator audit checklist, read `shared/prompt-caching.md`.
### Automatic Caching (Recommended)
Use top-level `cache_control` to automatically cache the last cacheable block in the request - no need to annotate individual content blocks:
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
cache_control={"type": "ephemeral"}, # auto-caches the last cacheable block
system="You are an expert on this large document...",
messages=[{"role": "user", "content": "Summarize the key points"}]
)
```
### Manual Cache Control
For fine-grained control, add `cache_control` to specific content blocks:
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system=[{
"type": "text",
"text": "You are an expert on this large document...",
"cache_control": {"type": "ephemeral"} # default TTL is 5 minutes
}],
messages=[{"role": "user", "content": "Summarize the key points"}]
)
# With explicit TTL (time-to-live)
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system=[{
"type": "text",
"text": "You are an expert on this large document...",
"cache_control": {"type": "ephemeral", "ttl": "1h"} # 1 hour TTL
}],
messages=[{"role": "user", "content": "Summarize the key points"}]
)
```
### Verifying Cache Hits
```python
print(response.usage.cache_creation_input_tokens) # tokens written to cache (~1.25x cost)
print(response.usage.cache_read_input_tokens) # tokens served from cache (~0.1x cost)
print(response.usage.input_tokens) # uncached tokens (full cost)
```
If `cache_read_input_tokens` is zero across repeated identical-prefix requests, a silent invalidator is at work - `datetime.now()` or a UUID in the system prompt, unsorted `json.dumps()`, or a varying tool set. See `shared/prompt-caching.md` for the full audit table.
---
## Extended Thinking
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking. `budget_tokens` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `thinking` runs adaptive (`{"type": "adaptive"}` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `{"type": "disabled"}` is accepted only at effort `high` or lower; pairing it with `xhigh`/`max` returns a 400.
> **Older models:** Use `thinking: {type: "enabled", budget_tokens: N}` (must be < `max_tokens`, min 1024).
```python
# Fable 5 / Claude Opus 5 / Opus 4.8 / 4.7 / 4.6: adaptive thinking (recommended)
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
thinking={"type": "adaptive", "display": "summarized"}, # display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Claude Opus 5 / Opus 4.8 / 4.7
output_config={"effort": "high"}, # low | medium | high | xhigh | max
messages=[{"role": "user", "content": "Solve this step by step..."}]
)
# Access thinking and response
for block in response.content:
if block.type == "thinking":
print(f"Thinking: {block.thinking}")
elif block.type == "text":
print(f"Response: {block.text}")
```
---
## Error Handling
```python
import anthropic
try:
response = client.messages.create(...)
except anthropic.BadRequestError as e:
print(f"Bad request: {e.message}")
except anthropic.AuthenticationError:
print("Invalid API key")
except anthropic.PermissionDeniedError:
print("API key lacks required permissions")
except anthropic.NotFoundError:
print("Invalid model or endpoint")
except anthropic.RateLimitError as e:
retry_after = int(e.response.headers.get("retry-after", "60"))
print(f"Rate limited. Retry after {retry_after}s.")
except anthropic.APIStatusError as e:
if e.status_code >= 500:
print(f"Server error ({e.status_code}). Retry later.")
else:
print(f"API error: {e.message}")
except anthropic.APIConnectionError:
print("Network error. Check internet connection.")
```
---
## Response Helpers
Every response object exposes `_request_id` (populated from the `request-id` header) - log it when reporting failures to Anthropic. Despite the underscore prefix, this property is public.
```python
message = client.messages.create(...)
print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG
print(message.to_json()) # serialize the Pydantic model
print(message.to_dict()) # plain dict
```
To access raw headers or other response metadata, use `.with_raw_response`:
```python
raw = client.messages.with_raw_response.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(raw.headers.get("request-id"))
message = raw.parse() # the Message object messages.create() would have returned
```
---
## Multi-Turn Conversations
The API is stateless - send the full conversation history each time.
```python
class ConversationManager:
"""Manage multi-turn conversations with the Claude API."""
def __init__(self, client: anthropic.Anthropic, model: str, system: str = None):
self.client = client
self.model = model
self.system = system
self.messages = []
def send(self, user_message: str, **kwargs) -> str:
"""Send a message and get a response."""
self.messages.append({"role": "user", "content": user_message})
response = self.client.messages.create(
model=self.model,
max_tokens=kwargs.get("max_tokens", 16000),
system=self.system,
messages=self.messages,
**kwargs
)
assistant_message = next(
(b.text for b in response.content if b.type == "text"), ""
)
self.messages.append({"role": "assistant", "content": assistant_message})
return assistant_message
# Usage
conversation = ConversationManager(
client=anthropic.Anthropic(),
model="claude-opus-5",
system="You are a helpful assistant."
)
response1 = conversation.send("My name is Alice.")
response2 = conversation.send("What's my name?") # Claude remembers "Alice"
```
**Rules:**
- Consecutive same-role messages are allowed - the API combines them into a single turn
- First message must be `user`
- `role: "system"` messages are allowed mid-conversation on supporting models (no beta header needed) - see § Mid-conversation system messages above
---
### Compaction (long conversations)
> **Beta, Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6.** When conversations approach the 200K context window, compaction automatically summarizes earlier context server-side. The API returns a `compaction` block; you must pass it back on subsequent requests - append `response.content`, not just the text.
```python
import anthropic
client = anthropic.Anthropic()
messages = []
def chat(user_message: str) -> str:
messages.append({"role": "user", "content": user_message})
response = client.beta.messages.create(
betas=["compact-2026-01-12"],
model="claude-opus-5",
max_tokens=16000,
messages=messages,
context_management={
"edits": [{"type": "compact_20260112"}]
}
)
# Append full content - compaction blocks must be preserved
messages.append({"role": "assistant", "content": response.content})
return next(block.text for block in response.content if block.type == "text")
# Compaction triggers automatically when context grows large
print(chat("Help me build a Python web scraper"))
print(chat("Add support for JavaScript-rendered pages"))
print(chat("Now add rate limiting and error handling"))
```
---
## Stop Reasons
The `stop_reason` field in the response indicates why the model stopped generating:
| Value | Meaning |
|-------|---------|
| `end_turn` | Claude finished its response naturally |
| `max_tokens` | Hit the `max_tokens` limit - increase it or use streaming |
| `stop_sequence` | Hit a custom stop sequence |
| `tool_use` | Claude wants to call a tool - execute it and continue |
| `pause_turn` | Model paused and can be resumed (agentic flows) |
| `refusal` | Claude refused for safety reasons - check `stop_details` |
### Structured Stop Details
When `stop_reason` is `"refusal"`, the response includes a `stop_details` object with structured information about the refusal:
```python
if response.stop_reason == "refusal" and response.stop_details:
print(f"Category: {response.stop_details.category}") # e.g. "cyber", "bio", "reasoning_extraction", "frontier_llm", or None - see docs for the full set
print(f"Explanation: {response.stop_details.explanation}")
```
### Refusal Fallbacks (Claude Fable 5.1) - opt in by default
Fallbacks are **opt-in**: without them a refused request simply stops. Include the server-side `fallbacks` parameter in `claude-fable-5-1` code by default - on a policy decline the API re-runs the same request on the fallback model inside the same call. A decline before any output isn't billed (a mid-stream decline bills the streamed partial); the rescue bills at the fallback model's own rates, with cache repricing applied automatically.
```python
response = client.beta.messages.create(
model="claude-fable-5-1",
max_tokens=16000,
betas=["server-side-fallback-2026-06-01"],
fallbacks=[{"model": "claude-opus-4-8"}],
messages=[{"role": "user", "content": "..."}],
)
# Switch points: one fallback block per model that ran and declined this turn
for block in response.content:
if block.type == "fallback":
print(f"{block.from_.model} declined; {block.to.model} continued")
# Served-by signal - covers sticky turns, which carry no fallback block.
# Pair with stop_reason: the fallback model can itself refuse.
fallback_ran = any(
entry.type == "fallback_message" for entry in response.usage.iterations or []
)
if fallback_ran and response.stop_reason != "refusal":
print(f"Served by {response.model}")
```
A `stop_reason: "refusal"` on the final response means the whole chain refused. The header must be exactly `server-side-fallback-2026-06-01` **for this array form**; the newer `fallbacks: "default"` scalar form uses `server-side-fallback-2026-07-01` instead (see `shared/model-migration.md` -> Migrating to Claude Opus 5 -> New API features), and pairing either header with the other form returns a 400. The parameter is rejected on the Batches API and unavailable on Amazon Bedrock, Vertex AI, and Microsoft Foundry - register the client-side `BetaRefusalFallbackMiddleware` on the client there instead. Full semantics (sticky routing, billing, streaming, echoing fallback turns back): `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason.
---
## Cost Optimization Strategies
### 1. Use Prompt Caching for Repeated Context
```python
# Automatic caching (simplest - caches the last cacheable block)
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
cache_control={"type": "ephemeral"},
system=large_document_text, # e.g., 50KB of context
messages=[{"role": "user", "content": "Summarize the key points"}]
)
# First request: full cost
# Subsequent requests: ~90% cheaper for cached portion
```
### 2. Choose the Right Model
```python
# Default to Opus for most tasks
response = client.messages.create(
model="claude-opus-5", # $5.00/$25.00 per 1M tokens
max_tokens=16000,
messages=[{"role": "user", "content": "Explain quantum computing"}]
)
# Use Sonnet for high-volume production workloads
standard_response = client.messages.create(
model="claude-sonnet-5", # $2.00/$10.00 per 1M tokens
max_tokens=16000,
messages=[{"role": "user", "content": "Summarize this document"}]
)
# Use Haiku only for simple, speed-critical tasks
simple_response = client.messages.create(
model="claude-haiku-4-5", # $1.00/$5.00 per 1M tokens
max_tokens=256,
messages=[{"role": "user", "content": "Classify this as positive or negative"}]
)
```
### 3. Use Token Counting Before Requests
```python
count_response = client.messages.count_tokens(
model="claude-opus-5",
messages=messages,
system=system
)
estimated_input_cost = count_response.input_tokens * 0.000005 # $5/1M tokens
print(f"Estimated input cost: ${estimated_input_cost:.4f}")
```
---
## Retry with Exponential Backoff
> **Note:** The Anthropic SDK automatically retries rate limit (429) and server errors (5xx) with exponential backoff. You can configure this with `max_retries` (default: 2). Only implement custom retry logic if you need behavior beyond what the SDK provides.
```python
import time
import random
import anthropic
def call_with_retry(
client: anthropic.Anthropic,
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0,
**kwargs
):
"""Call the API with exponential backoff retry."""
last_exception = None
for attempt in range(max_retries):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError as e:
last_exception = e
except anthropic.APIStatusError as e:
if e.status_code >= 500:
last_exception = e
else:
raise # Client errors (4xx except 429) should not be retried
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
print(f"Retry {attempt + 1}/{max_retries} after {delay:.1f}s")
time.sleep(delay)
raise last_exception
```
# Message Batches API - Python
The Batches API (`POST /v1/messages/batches`) processes Messages API requests asynchronously at 50% of standard prices.
## Key Facts
- Up to 100,000 requests or 256 MB per batch
- Most batches complete within 1 hour; maximum 24 hours
- Results available for 29 days after creation
- 50% cost reduction on all token usage
- All Messages API features supported (vision, tools, caching, etc.)
---
## Create a Batch
```python
import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
client = anthropic.Anthropic()
message_batch = client.messages.batches.create(
requests=[
Request(
custom_id="request-1",
params=MessageCreateParamsNonStreaming(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Summarize climate change impacts"}]
)
),
Request(
custom_id="request-2",
params=MessageCreateParamsNonStreaming(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Explain quantum computing basics"}]
)
),
]
)
print(f"Batch ID: {message_batch.id}")
print(f"Status: {message_batch.processing_status}")
```
---
## Poll for Completion
```python
import time
while True:
batch = client.messages.batches.retrieve(message_batch.id)
if batch.processing_status == "ended":
break
print(f"Status: {batch.processing_status}, processing: {batch.request_counts.processing}")
time.sleep(60)
print("Batch complete!")
print(f"Succeeded: {batch.request_counts.succeeded}")
print(f"Errored: {batch.request_counts.errored}")
```
---
## Retrieve Results
> **Note:** Examples below use `match/case` syntax, requiring Python 3.10+. For earlier versions, use `if/elif` chains instead.
```python
for result in client.messages.batches.results(message_batch.id):
match result.result.type:
case "succeeded":
msg = result.result.message
text = next((b.text for b in msg.content if b.type == "text"), "")
print(f"[{result.custom_id}] {text[:100]}")
case "errored":
if result.result.error.type == "invalid_request":
print(f"[{result.custom_id}] Validation error - fix request and retry")
else:
print(f"[{result.custom_id}] Server error - safe to retry")
case "canceled":
print(f"[{result.custom_id}] Canceled")
case "expired":
print(f"[{result.custom_id}] Expired - resubmit")
```
---
## Cancel a Batch
```python
cancelled = client.messages.batches.cancel(message_batch.id)
print(f"Status: {cancelled.processing_status}") # "canceling"
```
---
## List Batches (auto-pagination)
Iterating the return value of any `list()` call auto-paginates across all pages - do not index into `.data` if you want the full set:
```python
for batch in client.messages.batches.list(limit=20):
print(batch.id, batch.processing_status)
```
For manual control, use `first_page.has_next_page()` / `first_page.get_next_page()` / `first_page.next_page_info()`; `first_page.data` holds the current page's items and `first_page.last_id` is the cursor.
---
## Batch with Prompt Caching
```python
shared_system = [
{"type": "text", "text": "You are a literary analyst."},
{
"type": "text",
"text": large_document_text, # Shared across all requests
"cache_control": {"type": "ephemeral"}
}
]
message_batch = client.messages.batches.create(
requests=[
Request(
custom_id=f"analysis-{i}",
params=MessageCreateParamsNonStreaming(
model="claude-opus-5",
max_tokens=16000,
system=shared_system,
messages=[{"role": "user", "content": question}]
)
)
for i, question in enumerate(questions)
]
)
```
---
## Full End-to-End Example
```python
import anthropic
import time
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
client = anthropic.Anthropic()
# 1. Prepare requests
items_to_classify = [
"The product quality is excellent!",
"Terrible customer service, never again.",
"It's okay, nothing special.",
]
requests = [
Request(
custom_id=f"classify-{i}",
params=MessageCreateParamsNonStreaming(
model="claude-haiku-4-5",
max_tokens=50,
messages=[{
"role": "user",
"content": f"Classify as positive/negative/neutral (one word): {text}"
}]
)
)
for i, text in enumerate(items_to_classify)
]
# 2. Create batch
batch = client.messages.batches.create(requests=requests)
print(f"Created batch: {batch.id}")
# 3. Wait for completion
while True:
batch = client.messages.batches.retrieve(batch.id)
if batch.processing_status == "ended":
break
time.sleep(10)
# 4. Collect results
results = {}
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
msg = result.result.message
results[result.custom_id] = next((b.text for b in msg.content if b.type == "text"), "")
for custom_id, classification in sorted(results.items()):
print(f"{custom_id}: {classification}")
```
# Files API - Python
The Files API uploads files for use in Messages API requests. Reference files via `file_id` in content blocks, avoiding re-uploads across multiple API calls.
The Files API is out of beta. In current SDKs `client.beta.files` has breaking shape changes from previous versions, matching the stable `client.files` - migrate per the Files API row in `shared/live-sources.md`. Examples below predate this.
## Key Facts
- Maximum file size: 500 MB
- Total storage: 100 GB per organization
- Files persist until deleted
- File operations (upload, list, delete) are free; content used in messages is billed as input tokens
- Not available on Amazon Bedrock or Google Vertex AI
---
## Upload a File
The `file` argument accepts a `(filename, content, content_type)` tuple, a `pathlib.Path` (or any `PathLike` - read for you, async-safe with `AsyncAnthropic`), or an open binary file object.
```python
import anthropic
from pathlib import Path
client = anthropic.Anthropic()
uploaded = client.beta.files.upload(
file=("report.pdf", open("report.pdf", "rb"), "application/pdf"),
)
# or: client.beta.files.upload(file=Path("report.pdf"))
print(f"File ID: {uploaded.id}")
print(f"Size: {uploaded.size_bytes} bytes")
```
---
## Use a File in Messages
### PDF / Text Document
```python
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Summarize the key findings in this report."},
{
"type": "document",
"source": {"type": "file", "file_id": uploaded.id},
"title": "Q4 Report", # optional
"citations": {"enabled": True} # optional, enables citations
}
]
}],
betas=["files-api-2025-04-14"],
)
for block in response.content:
if block.type == "text":
print(block.text)
```
### Image
```python
image_file = client.beta.files.upload(
file=("photo.png", open("photo.png", "rb"), "image/png"),
)
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{
"type": "image",
"source": {"type": "file", "file_id": image_file.id}
}
]
}],
betas=["files-api-2025-04-14"],
)
```
---
## Manage Files
### List Files
Iterate the list result directly - the SDK auto-paginates across all pages. Only use `.data` if you want the first page only.
```python
for f in client.beta.files.list():
print(f"{f.id}: {f.filename} ({f.size_bytes} bytes)")
```
### Get File Metadata
```python
file_info = client.beta.files.retrieve_metadata("file_011CNha8iCJcU1wXNR6q4V8w")
print(f"Filename: {file_info.filename}")
print(f"MIME type: {file_info.mime_type}")
```
### Delete a File
```python
client.beta.files.delete("file_011CNha8iCJcU1wXNR6q4V8w")
```
### Download a File
Only files created by the code execution tool or skills can be downloaded (not user-uploaded files).
```python
file_content = client.beta.files.download("file_011CNha8iCJcU1wXNR6q4V8w")
file_content.write_to_file("output.txt")
```
---
## Full End-to-End Example
Upload a document once, ask multiple questions about it:
```python
import anthropic
client = anthropic.Anthropic()
# 1. Upload once
uploaded = client.beta.files.upload(
file=("contract.pdf", open("contract.pdf", "rb"), "application/pdf"),
)
print(f"Uploaded: {uploaded.id}")
# 2. Ask multiple questions using the same file_id
questions = [
"What are the key terms and conditions?",
"What is the termination clause?",
"Summarize the payment schedule.",
]
for question in questions:
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": question},
{
"type": "document",
"source": {"type": "file", "file_id": uploaded.id}
}
]
}],
betas=["files-api-2025-04-14"],
)
print(f"\nQ: {question}")
text = next((b.text for b in response.content if b.type == "text"), "")
print(f"A: {text[:200]}")
# 3. Clean up when done
client.beta.files.delete(uploaded.id)
```
# Upgrading the `anthropic` Python SDK: 0.x -> 1.x
> **If you arrived via `/claude-api upgrade`:** this is the right file. Execute the steps below in order - do not summarize them back to the user. Start with Step 0 before touching any file.
`anthropic` 1.x is deliberately a small step from the last 0.x release: no method was restructured and no new pattern is required. Long-deprecated surface was removed, the HTTP layer moved from `httpx` to its maintained fork `httpx2`, and the minimum Python version is now 3.10. Almost every required edit is mechanical, and a type checker flags nearly all of them once 1.x is installed - which makes `pyright` / `mypy` output a good cross-check for the inventory below.
The SDK repository's `MIGRATION.md` is the authoritative change list - WebFetch it (URL in `shared/live-sources.md` -> SDK major-version upgrade guides) when you can, and if it disagrees with this file, follow `MIGRATION.md` and say so in your report. The other Python files in this skill may still show 0.x-era details; for a project on 1.x, this file takes precedence.
---
## Step 0: Confirm scope, current version, and target
**Scope - ask before editing unless it is already unambiguous.** Same rule as model migration: if the request does not name an exact file, a specific directory, or an explicit file list, ask one question offering (1) the whole working directory, (2) a specific subdirectory, (3) specific files - and wait. `upgrade`, `upgrade python`, "move my project to anthropic v1" are all scope-ambiguous. A trailing path in the subcommand (`upgrade python src/`) is a scope. Dependency manifests and lockfiles at the project root (`pyproject.toml`, `requirements*.txt`, `setup.py`/`setup.cfg`, `Pipfile`, `uv.lock`, `poetry.lock`) count as in scope whenever any code under them is - say so when you confirm the scope.
**Current version.** Read the declared requirement (`anthropic...` in the manifests above) and, if a project environment is available, the installed one (`python -c "import anthropic; print(anthropic.__version__)"`). If the project is already on 1.x, skip the dependency bump and treat this as a call-site cleanup. If nothing in scope declares the dependency (a bare scripts directory, or `anthropic` arrives transitively), don't invent a manifest - upgrade the code and put the install command in the report.
**Target version.** Before writing any pin, confirm a 1.x release is actually published: `pip index versions anthropic` (or `curl -s https://pypi.org/pypi/anthropic/json` and read `info.version`). Use the newest 1.x you find. If no 1.x release exists yet, stop and tell the user - do not write an uninstallable requirement. If you cannot check (no network), proceed with `>=1,<2` and list the unverified pin in your report.
If the scope is under git, check `git status` before editing - unexpected modifications mean a concurrent process; stop and investigate before proceeding.
## Step 1: Inventory the call sites
Search the scope for each signal below (`rg -n -F` for the literal strings; exclude virtualenvs, `.git`, build output and vendored code) and keep the hit list - it is your checklist and, re-run at the end, your verification.
| Signal | What it finds | Section |
|---|---|---|
| `requires-python`, `python_requires`, `python-version`, `py39`, `3.9` in manifests, CI config, `tox.ini`, `noxfile.py`, `.python-version`, `Dockerfile` | a Python 3.9 floor | Step 2 |
| `anthropic` entries in manifests / lockfiles; `httpx-aiohttp`, `httpx_aiohttp` | the pins to change | Step 2 |
| `import httpx`, `from httpx` | modules that may hand `httpx` objects to the SDK | Step 3 |
| `respx`, `pytest_httpx` / `httpx_mock`, `vcr`, `MockTransport`; `HTTPXClientInstrumentor` / `opentelemetry.instrumentation.httpx`, `HttpxIntegration` (Sentry) | HTTP mocking and tracing / APM instrumentation that patch `httpx` and silently stop seeing SDK traffic | Step 3 |
| `with_raw_response` | raw-response call sites | Step 4 |
| `LegacyAPIResponse`, `_legacy_response` | annotations / imports of the removed class | Step 4 |
| `completions.create`, `HUMAN_PROMPT`, `AI_PROMPT`, `max_tokens_to_sample` | the removed Text Completions API | Step 5 |
| `temperature`, `top_p`, `top_k` (keyword arguments and quoted dict keys) | removed sampling parameters - only hits that feed Anthropic SDK calls count | Step 6 |
| `output_format` | raw `output_format={...}` dicts vs the unchanged `output_format=Model` helper argument | Step 6 |
| `BetaBase64PDFBlockParam`, `READ_MAX_BYTES`, `ProxiesTypes` / `Transport` imported from `anthropic`, `AsyncTransport` / `ProxiesDict` imported from `anthropic._types` | renamed / removed exports | Step 7 |
| `.parse(` calls that pass `stream=` | `messages.parse(stream=...)` | Step 8 |
| `compaction_control` | client-side tool-runner compaction | Step 8 |
| `body=` on `client.get` / `post` / `put` / `patch` / `delete` calls whose value is `bytes` (`b"..."`, `.encode()`, a bytes variable) | raw bytes passed as `body=` | Step 8 |
| `isinstance(` checks against `Stream` / `AsyncStream` | checks aimed at message streams | Step 8 |
| `default_headers`, `extra_headers`, `ANTHROPIC_CUSTOM_HEADERS` | header maps to check for duplicate casings / `bytes` values | Step 9 |
| `AnthropicBedrock(`, `AsyncAnthropicBedrock(` | Bedrock clients that may rely on the old region fallback | Step 10 |
Classify each hit before editing: **SDK call site** (edit), **unrelated use of the same name** (leave - e.g. `httpx` calls to other services, `urllib.parse`, a pydantic `.parse_obj`, a `temperature` variable for a thermostat), **test** (edit, and keep the test meaningful), **docs / README snippet or notebook inside the scope** (edit - for `.ipynb`, the greps match inside the JSON cell sources; edit the source strings, `%pip install` lines included, and keep the JSON valid). Never touch installed packages or vendored third-party code.
## Step 2: Environment - Python >= 3.10 and the dependency pins
- **[DECIDE] Python floor.** 1.x requires Python 3.10+. If the project still declares or tests 3.9 (`requires-python = ">=3.9"`, trove classifiers, a `3.9` CI matrix entry, tox/nox envs, a `python:3.9` base image), that is the user's decision, not a silent edit: propose the floor bump and the CI-matrix change as their own hunk and call it out in the report. On 3.9, `pip` simply keeps resolving the last 0.x release, so nothing breaks until they move.
- **[BREAKS] The `anthropic` requirement.** Rewrite it in the file's existing style - `anthropic>=1,<2` for a range, `anthropic~=1.0` / Poetry `^1.0` for compatible-release styles, `anthropic==<latest 1.x from Step 0>` where the project pins exactly. Extras (`anthropic[bedrock]`, `[vertex]`, `[aiohttp]`) are unchanged. Regenerate the lockfile with the project's own tool (`uv lock`, `poetry lock`, `pip-compile`, `pipenv lock`) if you can run it; otherwise give the user the exact command.
- **`httpx-aiohttp`.** If it is pinned only so `DefaultAioHttpClient()` works, remove it - the aiohttp transport now ships inside the SDK and the `aiohttp` extra installs only `aiohttp`.
- **`httpx2` / `httpx`.** After Step 3, if any project module imports `httpx2` directly, add `httpx2` to the declared dependencies (it arrives transitively with `anthropic`, but direct imports should be declared). `httpx2` has its own version line starting at 2.0 - write `httpx2>=2.0` (or match what `anthropic` resolved: `pip index versions httpx2`), never a specifier copied from the old `httpx` pin such as `>=0.27`. Keep `httpx` declared only if the project still uses it for something other than the SDK.
Pydantic v1 and v2 both remain supported; nothing else about the environment changes.
## Step 3: `httpx` -> `httpx2`, only where objects cross the SDK boundary
`httpx2` is the API-compatible, maintained fork of `httpx` (same classes, same behaviour). The change only matters for `httpx` objects handed **to** the SDK or received **from** it; plain values (`timeout=30.0`, `max_retries=3`) need nothing.
- **[BREAKS] Objects passed in.** `httpx.Timeout`, `httpx.Limits`, transports (`httpx.HTTPTransport(...)`, `AsyncHTTPTransport`, `MockTransport`), and whole clients (`httpx.Client` / `AsyncClient` as `http_client=`) must come from `httpx2`. An old-`httpx` client passed as `http_client=` raises `TypeError` at construction. This includes the project's own middleware, not just the outermost object handed to `Anthropic(...)`: a `class TracingTransport(httpx.BaseTransport)` subclass, the inner `httpx.HTTPTransport()` a wrapper delegates to, an `httpx.Auth` flow, and the annotations on `event_hooks` callables all re-base onto `httpx2` - a wrapper left delegating to an old-`httpx` transport hands the SDK `httpx.Response` objects. If the module uses `httpx` only for the SDK, alias the import (`import httpx2 as httpx`) and nothing else changes; if it also talks to other services with `httpx`, import both and switch only the SDK-bound objects to `httpx2`. Prefer the SDK's own re-exports where they let you drop the import entirely: `anthropic.Timeout`, `anthropic.DefaultHttpxClient`, `anthropic.DefaultAsyncHttpxClient`, `anthropic.DefaultAioHttpClient` (all already `httpx2`-based, all unchanged).
```python
# Before
import httpx
from anthropic import Anthropic, DefaultHttpxClient
client = Anthropic(
timeout=httpx.Timeout(60.0, connect=5.0),
http_client=DefaultHttpxClient(proxy="http://proxy.example", transport=httpx.HTTPTransport(retries=1)),
)
# After
import httpx2 as httpx
from anthropic import Anthropic, DefaultHttpxClient
client = Anthropic(
timeout=httpx.Timeout(60.0, connect=5.0),
http_client=DefaultHttpxClient(proxy="http://proxy.example", transport=httpx.HTTPTransport(retries=1)),
)
```
- **[DECIDE] Or alias process-wide, for applications.** `httpx2.alias_httpx()` makes `import httpx` / `import httpcore` resolve to `httpx2` / `httpcore2` for the whole process, so nothing else needs editing. Reach for it instead of the import edits when the scope is an **application** that shares clients, transports or exception types between the SDK and other `httpx` code, or that relies on tooling which patches `httpx` itself (tracing / APM instrumentation, HTTP mocking - see **Instrumentation and tests** below). Two hard rules: it must run before anything imports `httpx` or `httpcore` (otherwise it raises `RuntimeError`; calling it twice is a no-op), so it goes at the very top of the entry point; and it is for applications only - never add it to a **library's** import path on behalf of that library's users (edit the imports there instead). Say which you chose and why in the report.
```python
# the very first lines of the application's entry point
import httpx2
httpx2.alias_httpx()
import httpx # now the httpx2 module: httpx.Client is httpx2.Client
```
- **[BREAKS] Objects coming out.** `APIStatusError.response`, `APIConnectionError.request`, `.http_response` / `.headers` / `.url` on raw and streaming responses, the `request` / `response` arguments your `http_client` event hooks receive, and `cast_to=httpx.Response` on the low-level `client.get/post/...` methods are now `httpx2` types with identical attributes. Only `isinstance` checks and annotations naming `httpx.Response` / `httpx.Request` / `httpx.Headers` / `httpx.URL` change (`httpx2.Response`, ...).
- **Removed re-exports.** `anthropic.Transport` and `anthropic.ProxiesTypes` (and `AsyncTransport` / `ProxiesDict` from `anthropic._types`) are gone; use `httpx2.BaseTransport`, `httpx2.AsyncBaseTransport`, `httpx2.Proxy` (or a proxy URL string).
- **Instrumentation and tests.** Libraries that observe or stub HTTP by patching `httpx` - OpenTelemetry's `HTTPXClientInstrumentor`, Sentry's `httpx` integration, `respx`, `pytest-httpx`, `vcrpy` - keep importing fine but silently stop seeing the SDK's requests, so nothing fails loudly. The fix is the same `httpx2.alias_httpx()` call - not swapping in some `*-httpx2` instrumentation package (verify any such name is a real, populated release before depending on it) - made before any of them (or `httpx`) is imported: at the top of the application entry point for instrumentation, and under pytest as an early plugin so it runs before `respx` / `pytest-httpx` and the test modules load:
```python
# tests/_alias_httpx.py
import httpx2
httpx2.alias_httpx() # `import httpx` / `import httpcore` now resolve to httpx2 / httpcore2
```
```toml
# pyproject.toml
[tool.pytest.ini_options]
addopts = "-p tests._alias_httpx"
pythonpath = ["."]
```
Merge into an existing `addopts` rather than replacing it (`pytest.ini` / `setup.cfg` / `tox.ini` equivalents work the same way). Transport-level fakes (`httpx2.Client(transport=httpx2.MockTransport(handler))`, a handler typed `httpx2.Request -> httpx2.Response`) only need the import swap.
## Step 4: `.with_raw_response` returns `APIResponse` / `AsyncAPIResponse`
`.with_raw_response` used to return `LegacyAPIResponse` on both clients; it now returns the same classes `.with_streaming_response` already used. Two consequences:
- **[BREAKS] On async clients, reading the body is awaited** - `parse()`, `json()`, `text()`, `read()` are coroutines. Decide sync vs async from the client the accessor hangs off (`AsyncAnthropic` and the other `Async*` platform clients) or an `await` on the `.with_raw_response...(...)` call itself - not from the enclosing function alone.
- **[BREAKS] `.text` and `.content` are methods now, on the sync client too:** `.text` -> `.text()`, `.content` -> `.read()`. The new classes also expose `json()` and the `iter_bytes()` / `iter_text()` / `iter_lines()` iterators directly; 0.x code reached those through `r.http_response`, which still works and need not be rewritten.
| 0.x (`LegacyAPIResponse`) | 1.x sync (`APIResponse`) | 1.x async (`AsyncAPIResponse`) |
|---|---|---|
| `r.parse()` | `r.parse()` | `await r.parse()` |
| `r.text` | `r.text()` | `await r.text()` |
| `r.content` | `r.read()` | `await r.read()` |
| - (only `r.http_response.json()`) | `r.json()` | `await r.json()` |
| - (only `r.http_response.iter_bytes()` ...) | `r.iter_bytes()` / `.iter_text()` / `.iter_lines()` | `async for chunk in r.iter_bytes():` ... |
| `.headers`, `.status_code`, `.url`, `.request_id`, `.retries_taken`, `.http_response`, `.elapsed` | unchanged | unchanged (plain attributes - never awaited) |
```python
# Before (async client)
raw = await client.messages.with_raw_response.create(...)
print(raw.headers["request-id"], raw.text)
message = raw.parse()
# After
raw = await client.messages.with_raw_response.create(...)
print(raw.headers["request-id"], await raw.text())
message = await raw.parse()
```
Anchor every edit on a value that demonstrably comes from a `.with_raw_response.` call (follow it through variables, return values and fixtures); do not touch `.parse()` / `.text` on unrelated objects, and do not double-await. Annotations and imports of `anthropic._legacy_response.LegacyAPIResponse` become `anthropic.APIResponse` / `anthropic.AsyncAPIResponse`. `.with_streaming_response` code is unchanged.
## Step 5: Text Completions -> Messages (the one non-mechanical change)
**[BREAKS]** `client.completions.create()` (`/v1/complete`), the `Completion` types, and the `anthropic.HUMAN_PROMPT` / `anthropic.AI_PROMPT` constants are removed (also from `AnthropicBedrock`). Port each call to `client.messages.create()`:
- the `f"{HUMAN_PROMPT} ...{AI_PROMPT}"` prompt string becomes `messages=[{"role": "user", "content": "..."}]`; text that preceded the first `HUMAN_PROMPT` as instructions becomes `system=`; alternating `HUMAN_PROMPT`/`AI_PROMPT` turns become alternating `user`/`assistant` messages;
- `max_tokens_to_sample=` -> `max_tokens=`; `stop_sequences=` carries over; drop `temperature`/`top_p`/`top_k` (Step 6);
- `completion.completion` -> the text blocks of `message.content` (`"".join(b.text for b in message.content if b.type == "text")`); `stop_reason` values carry over (`"stop_sequence"`, `"max_tokens"`), with `"end_turn"` as the new normal-completion value;
- `stream=True` completions -> `client.messages.stream(...)` and its `text_stream`.
```python
# Before
from anthropic import AI_PROMPT, HUMAN_PROMPT
completion = client.completions.create(
model="claude-2.1",
max_tokens_to_sample=256,
prompt=f"{HUMAN_PROMPT} Why is the sky blue?{AI_PROMPT}",
)
print(completion.completion)
# After
message = client.messages.create(
model="claude-opus-5",
max_tokens=256,
messages=[{"role": "user", "content": "Why is the sky blue?"}],
)
print("".join(block.text for block in message.content if block.type == "text"))
```
**[DECIDE] The model.** Code still on Text Completions usually pins a retired model (`claude-2.x`, `claude-instant-*`), which 404s regardless of SDK version. Keep a model that is still served; otherwise switch to `claude-opus-5` so the code runs, say so prominently in the report, and point the user at `/claude-api migrate` for validating prompts against the new model - a completions-era prompt is exactly what `shared/prompt-audit.md` exists for.
## Step 6: Removed request parameters
- **[BREAKS] `temperature`, `top_p`, `top_k`** are no longer accepted by `messages.create()` / `.stream()` / `.parse()`, their `beta.messages` counterparts, or `beta.messages.tool_runner()` (passing them is a `TypeError`), and are gone from the per-request `params` TypedDict of `messages.batches.create()` (a type checker flags the key; at runtime the SDK still forwards it). Delete them - they are gone from the 1.x signatures, not from the API, and whether a model still honours them is a model question (`shared/model-migration.md`): Opus 4.7 and later return a 400 for any request that carries one (the default value included), Claude Sonnet 5 rejects non-default values, and every still-served model before those accepts them - the Claude 4.6 / 4.5 line (Opus 4.6, Sonnet 4.6, Opus 4.5, Sonnet 4.5, Haiku 4.5) and the deprecated-but-still-served Claude 4 models (`shared/models.md` -> Deprecated Models). So **[DECIDE]** when the call pins one of those accepting models and visibly depends on the setting (a documented determinism requirement, an A/B on temperature), move it into `extra_body` instead of deleting it - `extra_body={"temperature": 0.2}` is merged into the request JSON as-is - and for a `messages.batches.create()` request leave the key in that request's `params` dict (it is forwarded, see above). A call that pins a retired model (`shared/models.md` -> Retired Models) is the `migrate` flow's problem first: it needs a replacement model, and the replacement decides whether the setting survives. Say which calls kept a setting this way in the report. When a test existed only to assert that these parameters pass through, keep it meaningful by asserting on parameters that still exist (`stop_sequences`, `metadata`, `service_tier`, `max_tokens`) rather than deleting it.
```python
# Before
client.messages.create(..., model="claude-sonnet-4-6", temperature=0.2)
# After (only when the pinned model accepts it and the code depends on it)
client.messages.create(..., model="claude-sonnet-4-6", extra_body={"temperature": 0.2})
```
- **[BREAKS] `output_format={...}` as a raw dict/TypedDict** - on `beta.messages.create()`, `beta.messages.count_tokens()` and batch params (where the parameter is gone) and on the `messages.stream()` / `messages.count_tokens()` / `beta.messages.stream()` helpers (which used to accept a dict as well and now raise `TypeError` for one) -> `output_config={"format": {...}}` (merge into an existing `output_config` if one is already passed, e.g. alongside `effort`). **Leave `output_format=SomeModel` alone** when the value is a *type* (a Pydantic model / class passed to the `parse()`, `stream()` or `tool_runner()` helpers, or to the non-beta `messages.count_tokens()`) - that is the one form the helpers still take (`beta.messages.count_tokens()` only ever took the dict form, and has no `output_format` at all now). Tell them apart by the value: dict literal / `{"type": "json_schema", ...}` -> migrate; a class name -> keep.
```python
# Before
client.beta.messages.create(..., temperature=0.2, output_format={"type": "json_schema", "schema": Order.model_json_schema()})
# After
client.beta.messages.create(..., output_config={"format": {"type": "json_schema", "schema": Order.model_json_schema()}})
# or, usually better: client.beta.messages.parse(..., output_format=Order)
```
## Step 7: Renamed and removed names (pure renames)
**[BREAKS]** Replace imports and every reference; the replacement types are identical.
| Removed | Replacement |
|---|---|
| `anthropic.types.beta.BetaBase64PDFBlockParam` | `anthropic.types.beta.BetaRequestDocumentBlockParam` |
| `anthropic.Transport` / `anthropic.ProxiesTypes` (and `anthropic._types.AsyncTransport` / `ProxiesDict`) | `httpx2.BaseTransport` / `httpx2.Proxy` (`httpx2.AsyncBaseTransport`) |
| `anthropic.HUMAN_PROMPT` / `anthropic.AI_PROMPT` | none - Step 5 |
| `anthropic.lib.tools.agent_toolset.READ_MAX_BYTES` | `anthropic.lib.tools.agent_toolset.DEFAULT_MAX_FILE_BYTES` |
## Step 8: Removed helper arguments and behaviour
- **[BREAKS] `messages.parse(..., stream=True)`** (and `beta.messages.parse`): the argument is gone (it never streamed). Use the streaming helper, which supports the same structured-output types:
```python
# Before
result = client.messages.parse(..., output_format=Order, stream=True)
# After
with client.messages.stream(..., output_format=Order) as stream:
order = stream.get_final_message().parsed_output
```
A `parse(..., stream=False)` just loses the argument.
- **[BREAKS] `tool_runner(compaction_control=...)`** - client-side compaction is removed in favour of server-side compaction. Carry the old `context_token_threshold` over as the trigger value (the API minimum is 50,000; raise smaller values to that and mention it):
```python
# Before
runner = client.beta.messages.tool_runner(..., compaction_control={"enabled": True, "context_token_threshold": 100_000})
# After
runner = client.beta.messages.tool_runner(
...,
betas=["compact-2026-01-12"],
context_management={"edits": [{"type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 100_000}}]},
)
```
If the loop around the runner rebuilds `messages` itself, make sure it appends the full `message.content` (compaction blocks included) - see the Compaction section of `python/claude-api/README.md`.
- **[BREAKS] Raw `bytes` as `body=`** on `client.get/post/put/patch/delete`: `body=` is always JSON-serialised now; raw payloads (and iterators, for streaming uploads) go through `content=`:
```python
# Before
client.post("/v1/example", body=b"raw payload", cast_to=httpx.Response)
# After
client.post("/v1/example", content=b"raw payload", cast_to=httpx2.Response)
```
- **[BREAKS] `isinstance(x, anthropic.Stream)` / `AsyncStream` meant to match `client.messages.stream()` objects** now returns `False` (the compatibility shim and its `DeprecationWarning` are gone). Check for `anthropic.lib.streaming.MessageStream` / `AsyncMessageStream` instead; keep `Stream` only where the value really is a raw `create(stream=True)` stream.
## Step 9: Header names are matched case-insensitively
Usually nothing to edit. The SDK now merges `default_headers`, `extra_headers`, `with_options(default_headers=...)` and `ANTHROPIC_CUSTOM_HEADERS` case-insensitively: a later entry replaces an earlier header of the same name whatever its casing (including headers the SDK sets itself), and `omit` removes one the same way. Scan the Step 1 hits for two things and fix only those: **[DECIDE]** the same header name spelled with two casings where the code relied on both lines being sent (send one comma-joined value instead), and **[BREAKS]** `bytes` header values, which now raise - `.decode()` them.
## Step 10: Bedrock - a region is required
**[DECIDE]** `AnthropicBedrock()` / `AsyncAnthropicBedrock()` used to warn and fall back to `us-east-1` when no region was configured; they now raise `ValueError` at construction. Resolution order: `aws_region=` -> `AWS_REGION` / `AWS_DEFAULT_REGION` -> the region configured for the boto3 session / `aws_profile` (the profile is now honoured for region lookup). For each construction without `aws_region=`, check whether the deployment provides a region (env files, Dockerfiles, deployment manifests, AWS profile config in the repo). If it demonstrably does, nothing to do; if you cannot tell, do **not** invent a region - list the call site in the report as needing `aws_region=` or `AWS_REGION`, and only hardcode `"us-east-1"` if the user confirms that the old implicit default is what they were actually using.
Streaming from Bedrock also changes: event types the SDK does not know are now skipped instead of yielded - the only known case is the `amazon-bedrock-invocationMetrics` frame. Code that filtered those frames out can be deleted; code that *consumed* invocation metrics loses them on 1.x - **[DECIDE]** list it in the report (the SDK asks such users to open an issue).
## Step 11: Verify
1. Re-run the Step 1 greps over the scope. Every remaining hit needs a reason (unrelated `httpx` use, `Raw*` names, helper `output_format=Model`, ...) - put the reasons in the report.
2. `python -m compileall -q <scope>` must pass. If the project has a type checker configured, run it - nearly every missed call site is a type error on 1.x. Run the test suite if it is runnable without credentials.
3. If 1.x is installed in the environment: `python -c "import anthropic, httpx2; print(anthropic.__version__)"`.
## Step 12: Report
Lead with the outcome, then:
- what changed, grouped by the steps above, with file counts and the notable files;
- **decisions the user owns** - Python floor / CI matrix (Step 2), import edits vs `alias_httpx()` (Step 3), sampling-parameter reliance (Step 6), the model chosen for ported completions calls (Step 5), duplicate-casing headers (Step 9), Bedrock regions and invocation metrics (Step 10);
- if you introduced `httpx2` anywhere, one provenance line, because reviewers and supply-chain scanners flag unfamiliar package names as possible typosquats: it is the SDK's own HTTP dependency, the maintained fork of `httpx` by its original author, published by Pydantic (`github.com/pydantic/httpx2`), version line 2.x;
- what you could not verify (offline PyPI check, no type checker, tests not runnable, pre-commit hooks that need the new packages installed) and the exact commands to finish: the install / lock command and, if relevant, `pip uninstall httpx-aiohttp`.
## Checklist
- [ ] **[BREAKS]** `anthropic` requirement moved to 1.x in the project's pin style; lockfile regenerated or command given
- [ ] **[DECIDE]** Python >= 3.10 floor and CI matrix proposed as a separate hunk
- [ ] **[BREAKS]** `httpx` objects passed to / received from the SDK (custom transports, auth flows and event hooks included) come from `httpx2` - or **[DECIDE]** `httpx2.alias_httpx()` at the top of an application entry point; `httpx`-patching instrumentation / mocking (`respx`, `pytest-httpx`, `vcrpy`, OpenTelemetry, Sentry) covered by the alias; `httpx-aiohttp` dropped; `httpx2>=2.0` declared if imported
- [ ] **[BREAKS]** async `.with_raw_response`: `await` on `parse()/json()/text()/read()`; `.text` -> `.text()`, `.content` -> `.read()` everywhere; `LegacyAPIResponse` annotations replaced
- [ ] **[BREAKS]** `completions.create` / `HUMAN_PROMPT` / `AI_PROMPT` ported to Messages; **[DECIDE]** model choice surfaced
- [ ] **[BREAKS]** `temperature` / `top_p` / `top_k` removed from SDK calls - or, **[DECIDE]**, moved to `extra_body` only where the call pins an older model *and* visibly depends on the setting; raw `output_format={...}` -> `output_config={"format": ...}` everywhere (helpers included); helper `output_format=Model` untouched
- [ ] **[BREAKS]** `BetaBase64PDFBlockParam` -> `BetaRequestDocumentBlockParam`; `Transport`/`AsyncTransport`/`ProxiesTypes` -> `httpx2` names; `READ_MAX_BYTES` -> `DEFAULT_MAX_FILE_BYTES`
- [ ] **[BREAKS]** `parse(stream=)` -> `messages.stream()`; `compaction_control` -> server-side compaction; `body=bytes` -> `content=`; `Stream` isinstance checks retargeted
- [ ] **[DECIDE]** duplicate-casing headers joined; **[BREAKS]** `bytes` header values decoded
- [ ] **[DECIDE]** Bedrock constructions without a discoverable region listed, not guessed; invocation-metrics consumers flagged
- [ ] Step 11 verification run and Step 12 report written
# Streaming - Python
## Quick Start
```python
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Write a story"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
```
### Async
```python
async with async_client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Write a story"}]
) as stream:
async for text in stream.text_stream:
print(text, end="", flush=True)
```
### Low-level: `stream=True`
`messages.stream()` (above) is the recommended helper - it accumulates state and exposes `text_stream` / `get_final_message()`. If you only need the raw event iterator and want lower memory use, pass `stream=True` to `messages.create()` instead:
```python
for event in client.messages.create(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Write a story"}],
stream=True,
):
print(event.type)
```
No final-message accumulation is done for you in this form.
---
## Handling Different Content Types
Claude may return text, thinking blocks, or tool use. Handle each appropriately:
> **Fable 5 / Claude Opus 5 / Opus 4.8 / Opus 4.7 / Opus 4.6:** Use `thinking: {type: "adaptive"}`. On Claude Opus 5 adaptive is also what you get by omitting `thinking` entirely. On older models, use `thinking: {type: "enabled", budget_tokens: N}` instead.
```python
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
thinking={"type": "adaptive", "display": "summarized"}, # display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Claude Opus 5 / Opus 4.8 / 4.7
messages=[{"role": "user", "content": "Analyze this problem"}]
) as stream:
for event in stream:
if event.type == "content_block_start":
if event.content_block.type == "thinking":
print("\n[Thinking...]")
elif event.content_block.type == "text":
print("\n[Response:]")
elif event.type == "content_block_delta":
if event.delta.type == "thinking_delta":
print(event.delta.thinking, end="", flush=True)
elif event.delta.type == "text_delta":
print(event.delta.text, end="", flush=True)
```
---
## Streaming with Tool Use
The Python tool runner supports streaming: pass `stream=True` to `client.beta.messages.tool_runner(...)` and each iteration yields a stream you consume event-by-event, with `get_final_message()` for the accumulated message per turn (see `shared/tool-use-concepts.md` -> Tool Runner vs Manual Loop). Use the manual-loop pattern below only when you're not using the tool runner and need per-token streaming with tools:
```python
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
tools=tools,
messages=messages
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
response = stream.get_final_message()
# Continue with tool execution if response.stop_reason == "tool_use"
```
---
## Getting the Final Message
```python
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Hello"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# Get full message after streaming
final_message = stream.get_final_message()
print(f"\n\nTokens used: {final_message.usage.output_tokens}")
```
---
## Streaming with Progress Updates
```python
def stream_with_progress(client, **kwargs):
"""Stream a response with progress updates."""
total_tokens = 0
content_parts = []
with client.messages.stream(**kwargs) as stream:
for event in stream:
if event.type == "content_block_delta":
if event.delta.type == "text_delta":
text = event.delta.text
content_parts.append(text)
print(text, end="", flush=True)
elif event.type == "message_delta":
if event.usage and event.usage.output_tokens is not None:
total_tokens = event.usage.output_tokens
final_message = stream.get_final_message()
print(f"\n\n[Tokens used: {total_tokens}]")
return "".join(content_parts)
```
---
## Error Handling in Streams
```python
try:
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Write a story"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
except anthropic.APIConnectionError:
print("\nConnection lost. Please retry.")
except anthropic.RateLimitError:
print("\nRate limited. Please wait and retry.")
except anthropic.APIStatusError as e:
print(f"\nAPI error: {e.status_code}")
```
---
## Stream Event Types
| Event Type | Description | When it fires |
| --------------------- | --------------------------- | --------------------------------- |
| `message_start` | Contains message metadata | Once at the beginning |
| `content_block_start` | New content block beginning | When a text/tool_use block starts |
| `content_block_delta` | Incremental content update | For each token/chunk |
| `content_block_stop` | Content block complete | When a block finishes |
| `message_delta` | Message-level updates | Contains `stop_reason`, usage |
| `message_stop` | Message complete | Once at the end |
## Best Practices
1. **Always flush output** - Use `flush=True` to show tokens immediately
2. **Handle partial responses** - If the stream is interrupted, you may have incomplete content
3. **Track token usage** - The `message_delta` event contains usage information
4. **Use timeouts** - Set appropriate timeouts for your application
5. **Default to streaming** - Use `.get_final_message()` to get the complete response even when streaming, giving you timeout protection without needing to handle individual events
6. **Large `max_tokens` without streaming raises `ValueError`** - The SDK refuses non-streaming requests it estimates will exceed ~10 minutes (idle connections drop). Pass `stream=True` / use `messages.stream()`, or explicitly override `timeout`, to suppress the guard.
# Tool Use - Python
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Runner (Recommended)
**Beta:** The tool runner is in beta in the Python SDK.
Use the `@beta_tool` decorator to define tools as typed functions, then pass them to `client.beta.messages.tool_runner()`:
```python
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def get_weather(location: str, unit: str = "celsius") -> str:
"""Get current weather for a location.
Args:
location: City and state, e.g., San Francisco, CA.
unit: Temperature unit, either "celsius" or "fahrenheit".
"""
# Your implementation here
return f"72°F and sunny in {location}"
# The tool runner handles the agentic loop automatically
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
tools=[get_weather],
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)
# Each iteration yields a BetaMessage; iteration stops when Claude is done
for message in runner:
print(message)
```
For async usage, use `@beta_async_tool` with `async def` functions.
**Key benefits of the tool runner:**
- No manual loop - the SDK handles calling tools and feeding results back
- Type-safe tool inputs via decorators
- Tool schemas are generated automatically from function signatures
- Iteration stops automatically when Claude has no more tool calls
### Server tools with the tool runner
The runner's `tools` list accepts raw server-tool definitions (`web_search_20260209`, `web_fetch_20260209`, code execution) alongside decorated tools - pass the literal tool dict; server tools run on Anthropic's servers, so there is no function to implement.
**Caution - the runner does not auto-resume `pause_turn` (as of `anthropic` 0.116.0).** A long-running server-tool turn can stop with `stop_reason: "pause_turn"`. The runner only continues after a client tool produces a result, so a paused turn ends the loop and is returned as the final message - no error, no warning, just a silently truncated answer. Unlike the TypeScript runner, the Python runner cannot be resumed mid-loop: it exits unconditionally when no client tool ran, and `runner.append_messages(...)` does not prevent the exit. To handle `pause_turn`, mirror the conversation history as you iterate, then restart the runner with the paused turn appended:
```python
messages = [{"role": "user", "content": user_input}]
max_restarts = 5 # cap pause_turn restarts, mirroring max_continuations advice
restarts = 0
while True:
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
tools=tools, # may mix @beta_tool functions and server-tool definitions
messages=messages,
)
last = None
for message in runner:
last = message
# Mirror the history - the runner keeps its own copy and does not expose it
messages.append({"role": "assistant", "content": message.content})
tool_response = runner.generate_tool_call_response() # cached; tools still run once
if tool_response is not None:
messages.append(tool_response)
if last is None or last.stop_reason != "pause_turn":
break
restarts += 1
if restarts > max_restarts:
raise RuntimeError("giving up: turn still paused after max_restarts")
# Paused mid-turn: `messages` already ends with the paused assistant
# turn, so the next runner resumes it
```
Alternatively, use the manual loop below, which handles `pause_turn` explicitly.
---
## MCP Tool Conversion Helpers
**Beta.** Convert [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) tools, prompts, and resources to Anthropic API types for use with the tool runner. Requires `pip install anthropic[mcp]` (Python 3.10+).
> **Note:** The Claude API also supports an `mcp_servers` parameter that lets Claude connect directly to remote MCP servers. Use these helpers instead when you need local MCP servers, prompts, resources, or more control over the MCP connection.
### MCP Tools with Tool Runner
```python
from anthropic import AsyncAnthropic
from anthropic.lib.tools.mcp import async_mcp_tool
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
client = AsyncAnthropic()
async with stdio_client(StdioServerParameters(command="mcp-server")) as (read, write):
async with ClientSession(read, write) as mcp_client:
await mcp_client.initialize()
tools_result = await mcp_client.list_tools()
# tool_runner is sync - returns the runner, not a coroutine
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Use the available tools"}],
tools=[async_mcp_tool(t, mcp_client) for t in tools_result.tools],
)
async for message in runner:
print(message)
```
For sync usage, use `mcp_tool` instead of `async_mcp_tool`.
### MCP Prompts
```python
from anthropic.lib.tools.mcp import mcp_message
prompt = await mcp_client.get_prompt(name="my-prompt")
response = await client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[mcp_message(m) for m in prompt.messages],
)
```
### MCP Resources as Content
```python
from anthropic.lib.tools.mcp import mcp_resource_to_content
resource = await mcp_client.read_resource(uri="file:///path/to/doc.txt")
response = await client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
mcp_resource_to_content(resource),
{"type": "text", "text": "Summarize this document"},
],
}],
)
```
### Upload MCP Resources as Files
```python
from anthropic.lib.tools.mcp import mcp_resource_to_file
resource = await mcp_client.read_resource(uri="file:///path/to/data.json")
uploaded = await client.beta.files.upload(file=mcp_resource_to_file(resource))
```
Conversion functions raise `UnsupportedMCPValueError` if an MCP value cannot be converted (e.g., unsupported content types like audio, unsupported MIME types).
---
## Manual Agentic Loop
Prefer the tool runner above. Drop to a manual loop only when you need control the runner does not expose (e.g., a custom transport, request shapes the SDK cannot build, or avoiding a beta dependency - the runner is beta). Human-in-the-loop approval does *not* require a manual loop - gate inside the tool function (return a "user declined" result) or inspect pending `tool_use` blocks in the `for message in runner:` body and call `runner.set_messages_params()`.
If you do need a manual loop:
```python
import anthropic
client = anthropic.Anthropic()
tools = [...] # Your tool definitions
messages = [{"role": "user", "content": user_input}]
# Agentic loop: keep going until Claude stops calling tools
while True:
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=tools,
messages=messages
)
# If Claude is done (no more tool calls), break
if response.stop_reason == "end_turn":
break
# Server-side tool hit iteration limit; re-send to continue
if response.stop_reason == "pause_turn":
messages = [
{"role": "user", "content": user_input},
{"role": "assistant", "content": response.content},
]
continue
# Extract tool use blocks from the response
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
# Append assistant's response (including tool_use blocks)
messages.append({"role": "assistant", "content": response.content})
# Execute each tool and collect results
tool_results = []
for tool in tool_use_blocks:
result = execute_tool(tool.name, tool.input) # Your implementation
tool_results.append({
"type": "tool_result",
"tool_use_id": tool.id, # Must match the tool_use block's id
"content": result
})
# Append tool results as a user message
messages.append({"role": "user", "content": tool_results})
# Final response text
final_text = next(b.text for b in response.content if b.type == "text")
```
---
## Handling Tool Results
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
tool_use_id = block.id
result = execute_tool(tool_name, tool_input)
followup = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=tools,
messages=[
{"role": "user", "content": "What's the weather in Paris?"},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": result
}]
}
]
)
```
---
## Multiple Tool Calls
```python
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Send all results back at once
if tool_results:
followup = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=tools,
messages=[
*previous_messages,
{"role": "assistant", "content": response.content},
{"role": "user", "content": tool_results}
]
)
```
---
## Error Handling in Tool Results
```python
tool_result = {
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": "Error: Location 'xyz' not found. Please provide a valid city name.",
"is_error": True
}
```
---
## Tool Choice
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"}, # Force specific tool
messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)
```
---
## Code Execution
### Basic Usage
```python
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": "Calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
}],
tools=[{
"type": "code_execution_20260120",
"name": "code_execution"
}]
)
for block in response.content:
if block.type == "text":
print(block.text)
elif block.type == "bash_code_execution_tool_result":
print(f"stdout: {block.content.stdout}")
```
### Upload Files for Analysis
```python
# 1. Upload a file
uploaded = client.beta.files.upload(file=open("sales_data.csv", "rb"))
# 2. Pass to code execution via container_upload block
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Analyze this sales data. Show trends and create a visualization."},
{"type": "container_upload", "file_id": uploaded.id}
]
}],
tools=[{"type": "code_execution_20260120", "name": "code_execution"}]
)
```
### Retrieve Generated Files
```python
import os
OUTPUT_DIR = "./claude_outputs"
os.makedirs(OUTPUT_DIR, exist_ok=True)
for block in response.content:
if block.type == "bash_code_execution_tool_result":
result = block.content
if result.type == "bash_code_execution_result" and result.content:
for file_ref in result.content:
if file_ref.type == "bash_code_execution_output":
metadata = client.beta.files.retrieve_metadata(file_ref.file_id)
file_content = client.beta.files.download(file_ref.file_id)
# Use basename to prevent path traversal; validate result
safe_name = os.path.basename(metadata.filename)
if not safe_name or safe_name in (".", ".."):
print(f"Skipping invalid filename: {metadata.filename}")
continue
output_path = os.path.join(OUTPUT_DIR, safe_name)
file_content.write_to_file(output_path)
print(f"Saved: {output_path}")
```
### Container Reuse
```python
# First request: set up environment
response1 = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Install tabulate and create data.json with sample data"}],
tools=[{"type": "code_execution_20260120", "name": "code_execution"}]
)
# Get container ID from response
container_id = response1.container.id
# Second request: reuse the same container
response2 = client.messages.create(
container=container_id,
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Read data.json and display as a formatted table"}],
tools=[{"type": "code_execution_20260120", "name": "code_execution"}]
)
```
### Response Structure
```python
for block in response.content:
if block.type == "text":
print(block.text) # Claude's explanation
elif block.type == "server_tool_use":
print(f"Running: {block.name} - {block.input}") # What Claude is doing
elif block.type == "bash_code_execution_tool_result":
result = block.content
if result.type == "bash_code_execution_result":
if result.return_code == 0:
print(f"Output: {result.stdout}")
else:
print(f"Error: {result.stderr}")
else:
print(f"Tool error: {result.error_code}")
elif block.type == "text_editor_code_execution_tool_result":
print(f"File operation: {block.content}")
```
---
## Memory Tool
### Basic Usage
```python
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Remember that my preferred language is Python."}],
tools=[{"type": "memory_20250818", "name": "memory"}],
)
```
### SDK Memory Helper
Subclass `BetaAbstractMemoryTool`:
```python
from anthropic.lib.tools import BetaAbstractMemoryTool
class MyMemoryTool(BetaAbstractMemoryTool):
def view(self, command): ...
def create(self, command): ...
def str_replace(self, command): ...
def insert(self, command): ...
def delete(self, command): ...
def rename(self, command): ...
memory = MyMemoryTool()
# Use with tool runner
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
tools=[memory],
messages=[{"role": "user", "content": "Remember my preferences"}],
)
for message in runner:
print(message)
```
For full implementation examples, use WebFetch:
- `https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/memory/basic.py`
---
## Structured Outputs
### JSON Outputs (Pydantic - Recommended)
```python
from pydantic import BaseModel
from typing import List
import anthropic
class ContactInfo(BaseModel):
name: str
email: str
plan: str
interests: List[str]
demo_requested: bool
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": "Extract: Jane Doe ([email protected]) wants Enterprise, interested in API and SDKs, wants a demo."
}],
output_format=ContactInfo,
)
# response.parsed_output is a validated ContactInfo instance
contact = response.parsed_output
print(contact.name) # "Jane Doe"
print(contact.interests) # ["API", "SDKs"]
```
### Raw Schema
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": "Extract info: John Smith ([email protected]) wants the Enterprise plan."
}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan": {"type": "string"},
"demo_requested": {"type": "boolean"}
},
"required": ["name", "email", "plan", "demo_requested"],
"additionalProperties": False
}
}
}
)
import json
# output_config.format guarantees the first block is text with valid JSON
text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)
```
### Strict Tool Use
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Book a flight to Tokyo for 2 passengers on March 15"}],
tools=[{
"name": "book_flight",
"description": "Book a flight to a destination",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"destination": {"type": "string"},
"date": {"type": "string", "format": "date"},
"passengers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6, 7, 8]}
},
"required": ["destination", "date", "passengers"],
"additionalProperties": False
}
}]
)
```
### Using Both Together
```python
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Plan a trip to Paris next month"}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"next_steps": {"type": "array", "items": {"type": "string"}}
},
"required": ["summary", "next_steps"],
"additionalProperties": False
}
}
},
tools=[{
"name": "search_flights",
"description": "Search for available flights",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"destination": {"type": "string"},
"date": {"type": "string", "format": "date"}
},
"required": ["destination", "date"],
"additionalProperties": False
}
}]
)
```
# Managed Agents - Python
> **Bindings not shown here:** This README covers the most common managed-agents flows for Python. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Python SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK.
> **Agents are persistent - create once, reference by ID.** Store the agent ID returned by `agents.create` and pass it to every subsequent `sessions.create`; do not call `agents.create` in the request path. **Recommended:** define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md` (its live-docs URL is in `shared/live-sources.md`). The CLI owns the control plane (create/update); your code owns the data plane (sessions with the stored ID). The examples below show in-code creation for when you must provision programmatically; in production the create call belongs in setup, not in the request path.
## Installation
```bash
pip install anthropic
```
## Client Initialization
```python
import anthropic
# Default - resolves credentials from the environment:
# ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN, or an `ant auth login` profile.
# Prefer this for local dev; don't hardcode a key.
client = anthropic.Anthropic()
# Explicit API key (only when you must inject a specific key)
client = anthropic.Anthropic(api_key="your-api-key")
```
---
## Create an Environment
```python
environment = client.beta.environments.create(
name="my-dev-env",
config={
"type": "cloud",
"networking": {"type": "unrestricted"},
},
)
print(environment.id) # env_...
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** `model`/`system`/`tools` live on the agent object, not the session. Always start with `agents.create()` - the session only takes `agent={"type": "agent", "id": agent.id}`.
### Minimal
```python
# 1. Create the agent (reusable, versioned)
agent = client.beta.agents.create(
name="Coding Assistant",
model="claude-opus-5",
tools=[{"type": "agent_toolset_20260401", "default_config": {"enabled": True}}],
)
# 2. Start a session
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=environment.id,
)
print(session.id, session.status)
print(f"Trace: https://platform.claude.com/workspaces/default/sessions/{session.id}") # swap 'default' for your workspace ID if the API key is not in the Default workspace
```
### With system prompt and custom tools
```python
import os
agent = client.beta.agents.create(
name="Code Reviewer",
model="claude-opus-5",
system="You are a senior code reviewer.",
tools=[
{"type": "agent_toolset_20260401"},
{
"type": "custom",
"name": "run_tests",
"description": "Run the test suite",
"input_schema": {
"type": "object",
"properties": {
"test_path": {"type": "string", "description": "Path to test file"}
},
"required": ["test_path"],
},
},
],
)
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=environment.id,
title="Code review session",
resources=[
{
"type": "github_repository",
"url": "https://github.com/owner/repo",
"mount_path": "/workspace/repo",
"authorization_token": os.environ["GITHUB_TOKEN"],
"branch": "main",
}
],
)
```
---
## Send a User Message
```python
client.beta.sessions.events.send(
session_id=session.id,
events=[
{
"type": "user.message",
"content": [{"type": "text", "text": "Review the auth module"}],
}
],
)
```
> Tip: **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens - stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns).
---
## Stream Events (SSE)
```python
import json
# Stream-first: open stream, then send while stream is live
with client.beta.sessions.events.stream(
session_id=session.id,
) as stream:
client.beta.sessions.events.send(
session_id=session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": "..."}]}],
)
for event in stream:
... # process events
# Standalone stream iteration:
with client.beta.sessions.events.stream(
session_id=session.id,
) as stream:
for event in stream:
if event.type == "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="", flush=True)
elif event.type == "agent.custom_tool_use":
# Custom tool invocation - session is now idle
print(f"\nCustom tool call: {event.name}")
print(f"Input: {json.dumps(event.input)}")
# Send result back (see below)
elif event.type == "session.status_idle":
print("\n--- Agent idle ---")
elif event.type == "session.status_terminated":
print("\n--- Session terminated ---")
break
```
---
## Provide Custom Tool Result
```python
client.beta.sessions.events.send(
session_id=session.id,
events=[
{
"type": "user.custom_tool_result",
"custom_tool_use_id": "sevt_abc123",
"content": [{"type": "text", "text": "All 42 tests passed."}],
}
],
)
```
---
## Poll Events
```python
events = client.beta.sessions.events.list(
session_id=session.id,
)
for event in events.data:
print(f"{event.type}: {event.id}")
```
> Warning: **Prefer the SDK over raw `requests`/`httpx`.** If you hand-roll a poll loop, don't assume `timeout=(5, 60)` or `httpx.Timeout(120)` caps total call duration - both are **per-chunk** read timeouts (reset on every byte), so a trickling response can block forever. For a hard wall-clock deadline, track `time.monotonic()` at the loop level and bail explicitly, or wrap with `asyncio.wait_for()`. See [Receiving Events](../../shared/managed-agents-events.md#receiving-events).
---
## Full Streaming Loop with Custom Tools
```python
import json
def run_custom_tool(tool_name: str, tool_input: dict) -> str:
"""Execute a custom tool and return the result."""
if tool_name == "run_tests":
# Your tool implementation here
return "All tests passed."
return f"Unknown tool: {tool_name}"
def run_session(client, session_id: str):
"""Stream events and handle custom tool calls."""
while True:
with client.beta.sessions.events.stream(
session_id=session_id,
) as stream:
tool_calls = []
for event in stream:
if event.type == "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="", flush=True)
elif event.type == "agent.custom_tool_use":
tool_calls.append(event)
elif event.type == "session.status_idle":
break
elif event.type == "session.status_terminated":
return
if not tool_calls:
break
# Process custom tool calls
results = []
for call in tool_calls:
result = run_custom_tool(call.name, call.input)
results.append({
"type": "user.custom_tool_result",
"custom_tool_use_id": call.id,
"content": [{"type": "text", "text": result}],
})
client.beta.sessions.events.send(
session_id=session_id,
events=results,
)
```
---
## Upload a File
```python
with open("data.csv", "rb") as f:
file = client.beta.files.upload(
file=f,
)
# Use in a session
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=environment.id,
resources=[{"type": "file", "file_id": file.id, "mount_path": "/workspace/data.csv"}],
)
```
---
## List and Download Session Files
List files the agent wrote to `/mnt/session/outputs/` during a session, then download them.
```python
# List files associated with a session
files = client.beta.files.list(
scope_id=session.id,
betas=["managed-agents-2026-04-01"],
)
for f in files.data:
print(f.filename, f.size_bytes)
# Download each file and save to disk
file_content = client.beta.files.download(f.id)
file_content.write_to_file(f.filename)
```
> Tip: There's a brief indexing lag (~1-3s) between `session.status_idle` and output files appearing in `files.list`. Retry once or twice if the list is empty.
---
## Session Management
```python
# Get session details
session = client.beta.sessions.retrieve(session_id="sesn_011CZxAbc123Def456")
print(session.status, session.usage)
# List sessions
sessions = client.beta.sessions.list()
# Delete a session
client.beta.sessions.delete(session_id="sesn_011CZxAbc123Def456")
# Archive a session
client.beta.sessions.archive(session_id="sesn_011CZxAbc123Def456")
```
---
## MCP Server Integration
```python
# Agent declares MCP server (no auth here - auth goes in a vault)
agent = client.beta.agents.create(
name="MCP Agent",
model="claude-opus-5",
mcp_servers=[
{"type": "url", "name": "my-tools", "url": "https://my-mcp-server.example.com/sse"},
],
tools=[
{"type": "agent_toolset_20260401", "default_config": {"enabled": True}},
{"type": "mcp_toolset", "mcp_server_name": "my-tools"},
],
)
# Session attaches vault(s) containing credentials for those MCP server URLs
session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
vault_ids=[vault.id],
)
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.
# Claude API - Ruby
> **Note:** The Ruby SDK supports the Claude API. A tool runner is available in beta via `client.beta.messages.tool_runner()`. Agent SDK is not yet available for Ruby.
## Installation
```bash
gem install anthropic
```
## Client Initialization
```ruby
require "anthropic"
# Default (uses ANTHROPIC_API_KEY env var)
client = Anthropic::Client.new
# Explicit API key
client = Anthropic::Client.new(api_key: "your-api-key")
```
---
## Basic Message Request
```ruby
message = client.messages.create(
model: :"claude-opus-5",
max_tokens: 16000,
messages: [
{ role: "user", content: "What is the capital of France?" }
]
)
# content is an array of polymorphic block objects (TextBlock, ThinkingBlock,
# ToolUseBlock, ...). .type is a Symbol - compare with :text, not "text".
# .text raises NoMethodError on non-TextBlock entries.
message.content.each do |block|
puts block.text if block.type == :text
end
```
---
## Extended Thinking
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking. `budget_tokens` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `thinking:` runs adaptive (`{ type: "adaptive" }` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `{ type: "disabled" }` is accepted only at effort `high` or lower; pairing it with `xhigh`/`max` returns a 400.
> **Older models:** Use `thinking: { type: "enabled", budget_tokens: N }` (must be < `max_tokens`, min 1024).
```ruby
message = client.messages.create(
model: :"claude-opus-5",
max_tokens: 16000,
thinking: { type: "adaptive" },
messages: [{ role: "user", content: "Solve: 27 * 453" }]
)
message.content.each do |block|
case block.type
when :thinking then puts "Thinking: #{block.thinking}"
when :text then puts "Response: #{block.text}"
end
end
```
---
## Prompt Caching
`system_:` (trailing underscore - avoids shadowing `Kernel#system`) takes an array of text blocks; set `cache_control` on the last block. Plain hashes work via the `OrHash` type alias. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`.
```ruby
message = client.messages.create(
model: :"claude-opus-5",
max_tokens: 16000,
system_: [
{ type: "text", text: long_system_prompt, cache_control: { type: "ephemeral" } }
],
messages: [{ role: "user", content: "Summarize the key points" }]
)
```
For 1-hour TTL: `cache_control: { type: "ephemeral", ttl: "1h" }`. There's also a top-level `cache_control:` on `messages.create` that auto-places on the last cacheable block.
Verify hits via `message.usage.cache_creation_input_tokens` / `message.usage.cache_read_input_tokens`.
---
## Stop Details
When `stop_reason` is `:refusal`, the response includes structured `stop_details`:
```ruby
if message.stop_reason == :refusal && message.stop_details
puts "Category: #{message.stop_details.category}" # e.g. :cyber, :bio, :reasoning_extraction, :frontier_llm, or nil - see docs for the full set
puts "Explanation: #{message.stop_details.explanation}"
end
```
**Refusal fallbacks (Claude Fable 5.1) - opt in by default.** Fallbacks are opt-in: without them a refused request simply stops. New `claude-fable-5-1` code should include the server-side `fallbacks` parameter (beta header `server-side-fallback-2026-06-01`, `fallbacks: [{model: "claude-opus-4-8"}]` on the beta messages call) by default. The exact Ruby binding (and the client-side middleware for providers without server-side support) is not documented here - WebFetch the Ruby SDK repo's `examples/` from `shared/live-sources.md`; full semantics in `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason.
---
## Beta Features
`betas:` is only valid on `client.beta.messages.create`, not the non-beta path.
### Task budgets
```ruby
response = client.beta.messages.create(
model: :"claude-opus-5",
max_tokens: 16000,
output_config: { task_budget: { type: :tokens, total: 64_000 } },
tools: [...],
messages: [...],
betas: ["task-budgets-2026-03-13"]
)
```
---
## Error Type
`APIStatusError` exposes a `.type` field for programmatic error classification:
```ruby
begin
client.messages.create(...)
rescue Anthropic::Errors::APIStatusError => e
puts e.type # :rate_limit_error, :overloaded_error, etc.
end
```
# Streaming - Ruby
## Streaming
```ruby
stream = client.messages.stream(
model: :"claude-opus-5",
max_tokens: 64000,
messages: [{ role: "user", content: "Write a haiku" }]
)
stream.text.each { |text| print(text) }
```
---
# Tool Use - Ruby
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Use
The Ruby SDK supports tool use via raw JSON schema definitions and also provides a beta tool runner for automatic tool execution.
### Tool Runner (Beta)
```ruby
class GetWeatherInput < Anthropic::BaseModel
required :location, String, doc: "City and state, e.g. San Francisco, CA"
end
class GetWeather < Anthropic::BaseTool
doc "Get the current weather for a location"
input_schema GetWeatherInput
def call(input)
"The weather in #{input.location} is sunny and 72°F."
end
end
client.beta.messages.tool_runner(
model: :"claude-opus-5",
max_tokens: 16000,
tools: [GetWeather.new],
messages: [{ role: "user", content: "What's the weather in San Francisco?" }]
).each_message do |message|
puts message.content
end
```
### Manual Loop
See the [shared tool use concepts](../../shared/tool-use-concepts.md) for the tool definition format and agentic loop pattern.
---
# Managed Agents - Ruby
> **Bindings not shown here:** This README covers the most common managed-agents flows for Ruby. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Ruby SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK.
> **Agents are persistent - create once, reference by ID.** Store the agent ID returned by `client.beta.agents.create` and pass it to every subsequent `client.beta.sessions.create`; do not call `agents.create` in the request path. **Recommended:** define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md` (its live-docs URL is in `shared/live-sources.md`). The CLI owns the control plane (create/update); your code owns the data plane (sessions with the stored ID). The examples below show in-code creation for when you must provision programmatically; in production the create call belongs in setup, not in the request path.
## Installation
```bash
gem install anthropic
```
## Client Initialization
```ruby
require "anthropic"
# Default (uses ANTHROPIC_API_KEY env var)
client = Anthropic::Client.new
# Explicit API key
client = Anthropic::Client.new(api_key: "your-api-key")
```
> Warning: **Trailing underscores:** The Ruby SDK uses `system_:` and `send_(` (trailing underscore) to avoid shadowing `Kernel#system` and `Kernel#send`. Use these forms throughout managed-agents code.
---
## Create an Environment
```ruby
environment = client.beta.environments.create(
name: "my-dev-env",
config: {
type: "cloud",
networking: {type: "unrestricted"}
}
)
puts "Environment ID: #{environment.id}" # env_...
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** `model`/`system_`/`tools` live on the agent object, not the session. Always start with `client.beta.agents.create()` - the session takes either `agent: agent.id` or the typed hash form `agent: {type: "agent", id: agent.id, version: agent.version}`.
### Minimal
```ruby
# 1. Create the agent (reusable, versioned)
agent = client.beta.agents.create(
name: "Coding Assistant",
model: :"claude-opus-5",
system_: "You are a helpful coding assistant.",
tools: [{type: "agent_toolset_20260401"}]
)
# 2. Start a session
session = client.beta.sessions.create(
agent: {type: "agent", id: agent.id, version: agent.version},
environment_id: environment.id,
title: "Quickstart session"
)
puts "Session ID: #{session.id}"
puts "Trace: https://platform.claude.com/workspaces/default/sessions/#{session.id}" # swap 'default' for your workspace ID if the API key is not in the Default workspace
```
### Updating an Agent
Updates create new versions; the agent object is immutable per version.
```ruby
updated_agent = client.beta.agents.update(
agent.id,
version: agent.version,
system_: "You are a helpful coding agent. Always write tests."
)
puts "New version: #{updated_agent.version}"
# List all versions
client.beta.agents.versions.list(agent.id).auto_paging_each do |version|
puts "Version #{version.version}: #{version.updated_at.iso8601}"
end
# Archive the agent
archived = client.beta.agents.archive(agent.id)
puts "Archived at: #{archived.archived_at.iso8601}"
```
---
## Send a User Message
```ruby
client.beta.sessions.events.send_(
session.id,
events: [{
type: "user.message",
content: [{type: "text", text: "Review the auth module"}]
}]
)
```
> Tip: **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens - stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns).
---
## Stream Events (SSE)
```ruby
# Open the stream first, then send the user message
stream = client.beta.sessions.events.stream_events(session.id)
client.beta.sessions.events.send_(
session.id,
events: [{
type: "user.message",
content: [{type: "text", text: "Summarize the repo README"}]
}]
)
stream.each do |event|
case event.type
in :"agent.message"
event.content.each { |block| print block.text }
in :"agent.tool_use"
puts "\n[Using tool: #{event.name}]"
in :"session.status_idle"
break
in :"session.error"
puts "\n[Error: #{event.error&.message || "unknown"}]"
break
else
# ignore other event types
end
end
```
> Note: Event `.type` is a Symbol (compare with `:"agent.message"`, not `"agent.message"`).
### Reconnecting and Tailing
When reconnecting mid-session, list past events first to dedupe, then tail live events:
```ruby
require "set"
stream = client.beta.sessions.events.stream_events(session.id)
# Stream is open and buffering. List history before tailing live.
seen_event_ids = Set.new
client.beta.sessions.events.list(session.id).auto_paging_each { |past| seen_event_ids << past.id }
# Tail live events, skipping anything already seen
stream.each do |event|
next if seen_event_ids.include?(event.id)
seen_event_ids << event.id
case event.type
in :"agent.message"
event.content.each { |block| print block.text }
in :"session.status_idle"
break
else
# ignore other event types
end
end
```
---
## Provide Custom Tool Result
> Note: The Ruby managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `anthropic` Ruby gem repository for the corresponding params.
---
## Poll Events
```ruby
client.beta.sessions.events.list(session.id).auto_paging_each do |event|
puts "#{event.type}: #{event.id}"
end
```
---
## Upload a File
```ruby
require "pathname"
file = client.beta.files.upload(file: Pathname("data.csv"))
puts "File ID: #{file.id}"
# Mount in a session
session = client.beta.sessions.create(
agent: agent.id,
environment_id: environment.id,
resources: [
{
type: "file",
file_id: file.id,
mount_path: "/workspace/data.csv"
}
]
)
```
### Add and Manage Resources on an Existing Session
```ruby
# Attach an additional file to an open session
resource = client.beta.sessions.resources.add(
session.id,
type: "file",
file_id: file.id
)
puts resource.id # "sesrsc_01ABC..."
# List resources on the session
listed = client.beta.sessions.resources.list(session.id)
listed.data.each { |entry| puts "#{entry.id} #{entry.type}" }
# Detach a resource
client.beta.sessions.resources.delete(resource.id, session_id: session.id)
```
---
## List and Download Session Files
```ruby
files = client.beta.files.list(scope_id: "sesn_abc123", betas: ["managed-agents-2026-04-01"])
content = client.beta.files.download(files.data[0].id)
File.binwrite("output.txt", content.read)
```
---
## Session Management
```ruby
# List environments
environments = client.beta.environments.list
# Retrieve a specific environment
env = client.beta.environments.retrieve(environment.id)
# Archive an environment (read-only, existing sessions continue)
client.beta.environments.archive(environment.id)
# Delete an environment (only if no sessions reference it)
client.beta.environments.delete(environment.id)
# Delete a session
client.beta.sessions.delete(session.id)
```
---
## MCP Server Integration
```ruby
# Agent declares MCP server (no auth here - auth goes in a vault)
agent = client.beta.agents.create(
name: "GitHub Assistant",
model: :"claude-opus-5",
mcp_servers: [
{
type: "url",
name: "github",
url: "https://api.githubcopilot.com/mcp/"
}
],
tools: [
{type: "agent_toolset_20260401"},
{type: "mcp_toolset", mcp_server_name: "github"}
]
)
# Session attaches vault(s) containing credentials for those MCP server URLs
session = client.beta.sessions.create(
agent: {type: "agent", id: agent.id, version: agent.version},
environment_id: environment.id,
vault_ids: [vault.id]
)
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.
---
## Vaults
```ruby
# Create a vault
vault = client.beta.vaults.create(
display_name: "Alice",
metadata: {external_user_id: "usr_abc123"}
)
puts vault.id # "vlt_01ABC..."
# Add an OAuth credential
credential = client.beta.vaults.credentials.create(
vault.id,
display_name: "Alice's Slack",
auth: {
type: "mcp_oauth",
mcp_server_url: "https://mcp.slack.com/mcp",
access_token: "xoxp-...",
expires_at: "2026-04-15T00:00:00Z",
refresh: {
token_endpoint: "https://slack.com/api/oauth.v2.access",
client_id: "1234567890.0987654321",
scope: "channels:read chat:write",
refresh_token: "xoxe-1-...",
token_endpoint_auth: {
type: "client_secret_post",
client_secret: "abc123..."
}
}
}
)
# Rotate the credential (e.g., after a token refresh)
client.beta.vaults.credentials.update(
credential.id,
vault_id: vault.id,
auth: {
type: "mcp_oauth",
access_token: "xoxp-new-...",
expires_at: "2026-05-15T00:00:00Z",
refresh: {refresh_token: "xoxe-1-new-..."}
}
)
# Archive a vault
client.beta.vaults.archive(vault.id)
```
---
## GitHub Repository Integration
Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential):
```ruby
session = client.beta.sessions.create(
agent: agent.id,
environment_id: environment.id,
vault_ids: [vault.id],
resources: [
{
type: "github_repository",
url: "https://github.com/org/repo",
mount_path: "/workspace/repo",
authorization_token: "ghp_your_github_token"
}
]
)
```
Multiple repositories on the same session:
```ruby
resources = [
{
type: "github_repository",
url: "https://github.com/org/frontend",
mount_path: "/workspace/frontend",
authorization_token: "ghp_your_github_token"
},
{
type: "github_repository",
url: "https://github.com/org/backend",
mount_path: "/workspace/backend",
authorization_token: "ghp_your_github_token"
}
]
```
Rotating a repository's authorization token:
```ruby
listed = client.beta.sessions.resources.list(session.id)
repo_resource_id = listed.data.first.id
client.beta.sessions.resources.update(
repo_resource_id,
session_id: session.id,
authorization_token: "ghp_your_new_github_token"
)
```
# Admin API (Organization Management)
Read this file when the user wants to manage their Anthropic organization programmatically: members and roles, invites, workspaces and workspace members, API keys, rate limit reports, service accounts, workload identity federation (WIF), or customer-managed encryption keys (CMEK).
The Admin API lives under `https://api.anthropic.com/v1/organizations/*`. It manages the organization itself - it does not send messages. As of **August 26, 2026** it is available in all seven SDKs (Python, TypeScript, C#, Go, Java, PHP, Ruby) under `client.beta.organization`, and in the `ant` CLI under `ant beta:organization`. Usage reports, cost reports, and the Claude Enterprise user-management and analytics endpoints are **not** in the SDKs - call those with raw HTTP.
## Authentication
Two credential types, both read automatically by the default SDK client and the CLI:
| Credential | Env var | HTTP header | Covers |
| --- | --- | --- | --- |
| Admin API key (`sk-ant-admin...`) | `ANTHROPIC_API_KEY` | `x-api-key` | Most endpoints |
| `org:admin` OAuth token | `ANTHROPIC_AUTH_TOKEN` | `authorization: Bearer` | Everything, including the OAuth-only endpoints |
- **OAuth-only endpoints:** service accounts, federation issuers, and federation rules reject API keys - they require an `org:admin` OAuth token.
- **Precedence gotcha:** when both env vars are set, some clients prefer the API key. When using a bearer token, leave `ANTHROPIC_API_KEY` unset in that shell.
- Admin API keys are created in the Claude Console by organization admins.
- Regular (non-admin) API keys do not work on any of these endpoints, and admin credentials do not work on the Messages API.
- An `org:admin` token grants access to the whole organization regardless of any workspace binding.
**Interactive OAuth token** - log in with the `ant` CLI under a dedicated profile (keeps routine commands from running with elevated access), then export the token. Tokens are short-lived; on 401, re-run the export. Profile and scope mechanics (why `org:admin` needs an explicit `--scope`, switching profiles): `shared/anthropic-cli.md`.
```bash
ant auth login --profile admin --scope "org:admin"
export ANTHROPIC_AUTH_TOKEN=$(ant auth print-credentials --profile admin --access-token)
# When done: unset ANTHROPIC_AUTH_TOKEN && ant profile activate default
```
**Automated workloads (CI)** - don't log in interactively. Create a federation rule with `oauth_scope: org:admin` targeting a service account whose `organization_role` is `admin` (this one rule must be created by a human in the Claude Console), then point the client at it with the federation env vars and construct it with no arguments - the SDK/CLI performs the token exchange automatically and refreshes before expiry:
```bash
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_... # the org:admin rule
export ANTHROPIC_ORGANIZATION_ID=<org-uuid>
export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_... # the rule's target service account
export ANTHROPIC_IDENTITY_TOKEN_FILE=/path/to/jwt # or ANTHROPIC_IDENTITY_TOKEN
```
**curl** also needs `anthropic-version: 2023-06-01` on every request.
## Endpoint Coverage
SDK accessor shown in Python spelling; see the per-language table below for naming conventions.
| Resource | REST path | SDK accessor (`client.beta.organization` +) | CLI (`ant beta:organization` +) |
| --- | --- | --- | --- |
| Organization info | `GET /v1/organizations/me` | `.retrieve()` | `retrieve` |
| Members | `/v1/organizations/users` | `.users` - `list`, `update`, `remove` | `:users list\|update\|remove` |
| Invites | `/v1/organizations/invites` | `.invites` - `create`, `list`, `delete` | `:invites create\|list\|delete` |
| Workspaces | `/v1/organizations/workspaces` | `.workspaces` - `create`, `retrieve`, `list`, `update`, `archive` | `:workspaces create\|list\|update\|archive` |
| Workspace members | `/v1/organizations/workspaces/{id}/members` | `.workspaces.members` - `add`, `list`, `update`, `remove` | `:workspaces:members add\|list\|update\|remove` |
| API keys | `/v1/organizations/api_keys` | `.api_keys` - `list`, `update` | `:api-keys list\|update` |
| Org rate limits | `GET /v1/organizations/rate_limits` | `.rate_limits.list(model=..., group_type=...)` | `:rate-limits list` |
| Workspace rate limits | `GET /v1/organizations/workspaces/{id}/rate_limits` | `.workspaces.rate_limits.list(workspace_id)` | `:workspaces:rate-limits list` |
| Service accounts (*) | `/v1/organizations/service_accounts` | `.service_accounts` - `create`, `list`, `archive` | `:service-accounts create\|list\|archive` |
| Federation issuers (*) | `/v1/organizations/federation_issuers` | `.federation.issuers` - `create`, `list`, `archive` | `:federation:issuers create\|list\|archive` |
| Federation rules (*) | `/v1/organizations/federation_rules` | `.federation.rules` - `create`, `list`, `archive` | `:federation:rules create\|list\|archive` |
| CMEK external keys | `/v1/organizations/external_keys` | `.external_keys` - `create`, `validate` | - |
(*) OAuth-only: requires an `org:admin` bearer token, not an API key.
Attaching a CMEK external key to a workspace is a workspace update: `client.beta.organization.workspaces.update("<workspace-id>", external_key_id="ekey_...")`.
## Per-Language Naming & Pagination
| Language | Accessor style (list members example) | List behavior |
| --- | --- | --- |
| Python | `client.beta.organization.users.list(limit=10)` | Iterator auto-fetches more pages; `limit` = page size, not total |
| TypeScript | `client.beta.organization.users.list({ limit: 10 })` - camelCase sub-resources: `apiKeys`, `rateLimits`, `serviceAccounts`, `externalKeys` | `for await` auto-pages |
| C# | `client.Beta.Organization.Users.List(new() { Limit = 10 })` | `await foreach (var u in page.Paginate())` auto-pages |
| Go | `client.Beta.Organization.Users.ListAutoPaging(ctx, params)`; org info is `Organization.Get(ctx)` | `.Next()` / `.Current()` auto-pages |
| Java | `client.beta().organization().users().list(params)` with builder params (`UserListParams.builder().limit(10).build()`) | `.autoPager()` auto-pages |
| PHP | `$client->beta->organization->users->list(limit: 10)` | Raw single-page data call - iterate `->getItems()`; the SDK's auto-pagination helpers aren't wired up for these endpoints yet |
| Ruby | `client.beta.organization.users.list(limit: 10)` | Raw single-page data call - iterate `.data`; the SDK's auto-pagination helpers aren't wired up for these endpoints yet |
| CLI | `ant beta:organization:users list --limit 10` | On the member, invite, workspace, workspace-member, and API-key lists, `--limit` caps the results (unlike most `ant` list commands, where `--limit` sets the page size and `--max-items` caps - see `shared/anthropic-cli.md`) |
| curl | `GET /v1/organizations/users?limit=10` | One page per request; cursor pagination per the Admin API reference |
The rate-limit lists (`rate_limits`, `workspaces.rate_limits`) also support pagination as of launch - page them like the other list endpoints rather than assuming a single response.
Go param types follow the pattern `anthropic.BetaOrganizationUserListParams` (with `anthropic.Int(10)` for `Limit`); Java params use builders from `com.anthropic.models.beta.organization.*` (e.g. `UserListParams.builder().limit(10).build()`). The Go and Java pagination loops:
```go
users := client.Beta.Organization.Users.ListAutoPaging(ctx, anthropic.BetaOrganizationUserListParams{Limit: anthropic.Int(10)})
for users.Next() {
user := users.Current() // ...
}
if err := users.Err(); err != nil { /* handle */ }
```
```java
for (var user : client.beta().organization().users().list(params).autoPager()) { /* ... */ }
```
## Examples
Common operations (Python spelling; map to other languages with the table above - every operation follows the same shape in each language):
```python
# Organization info
org = client.beta.organization.retrieve()
# List members (iterator auto-fetches more pages; limit = page size)
for user in client.beta.organization.users.list(limit=10):
print(f"{user.id}: {user.email} ({user.role})")
# Change a member's role / remove a member
client.beta.organization.users.update("user_...", role="developer")
client.beta.organization.users.remove("user_...")
# Invite someone
client.beta.organization.invites.create(email="[email protected]", role="developer")
# Create a workspace and add a member to it
ws = client.beta.organization.workspaces.create(name="Production")
client.beta.organization.workspaces.members.add(
ws.id, user_id="user_...", workspace_role="workspace_developer"
)
# Deactivate / rename an API key
client.beta.organization.api_keys.update("apikey_...", status="inactive", name="New Key Name")
# Rate limit reports (optional filters: model=..., group_type=...)
client.beta.organization.rate_limits.list(model="claude-opus-5")
client.beta.organization.workspaces.rate_limits.list("wrkspc_...")
# Service accounts + WIF (org:admin OAuth token required)
sa = client.beta.organization.service_accounts.create(name="inference-worker", organization_role="developer")
issuer = client.beta.organization.federation.issuers.create(
name="github-actions",
issuer_url="https://token.actions.githubusercontent.com",
jwks={"type": "discovery"},
)
client.beta.organization.federation.rules.create(
name="gha-deploy",
issuer_id=issuer.id,
match={"subject_prefix": "repo:my-org/my-repo:ref:refs/heads/main",
"claims": {"repository_owner": "my-org"}},
target={"type": "service_account", "service_account_id": sa.id},
workspace_id="wrkspc_...",
oauth_scope="workspace:developer",
token_lifetime_seconds=600,
)
# CMEK: register, validate, then attach an external key to a workspace
key = client.beta.organization.external_keys.create(
display_name="prod-key", geo="us",
provider_config={"type": "aws", "kms_arn": "arn:aws:kms:..."},
)
client.beta.organization.external_keys.validate(key.id)
client.beta.organization.workspaces.update("wrkspc_...", external_key_id=key.id)
```
## Organization Roles
| Role | Permissions |
| --- | --- |
| `user` | Playground |
| `claude_code_user` | Playground + Claude Code |
| `developer` | Playground + manage API keys |
| `billing` | Playground + manage billing |
| `admin` | All of the above + manage users |
Owners and primary owners have all admin permissions and can also manage admins. Workspace roles are `workspace_user`, `workspace_developer`, `workspace_admin`, and `workspace_billing`.
## Platform Restrictions
- **Claude Platform on AWS:** only the workspace endpoints work. Members, workspace members, invites, API keys, and usage/cost/rate-limit reports are unavailable. CMEK external-key endpoints are not yet available there - register and attach keys in the Claude Console.
- **Claude Enterprise (claude.ai orgs):** only members and invites from this surface, plus Enterprise-only endpoints (group and custom-role reads, spend limits) that are not in the SDKs.
## Live Docs
| Topic | URL |
| --- | --- |
| Admin API guide | `https://platform.claude.com/docs/en/manage-claude/admin-api.md` |
| Admin API reference | `https://platform.claude.com/docs/en/api/admin.md` |
| Workspaces | `https://platform.claude.com/docs/en/manage-claude/workspaces.md` |
| Rate limits API | `https://platform.claude.com/docs/en/manage-claude/rate-limits-api.md` |
| WIF admin | `https://platform.claude.com/docs/en/manage-claude/wif-admin-api.md` |
| Usage & cost reports (curl-only) | `https://platform.claude.com/docs/en/manage-claude/usage-cost-api.md` |
# Agent Design Patterns
This file covers decision heuristics for building agents on the Claude API: which primitives to reach for, how to design your tool surface, and how to manage context and cost over long runs. For per-tool mechanics and code examples, see `tool-use-concepts.md` and the language-specific folders.
---
## Model Parameters
| Parameter | When to use it | What to expect |
| --- | --- | --- |
| **Adaptive thinking** (`thinking: {type: "adaptive"}`) | When you want Claude to control when and how much to think. | Claude determines thinking depth per request and automatically interleaves thinking between tool calls. No token budget to tune. |
| **Effort** (`output_config: {effort: ...}`) | When adjusting the tradeoff between thoroughness and token efficiency. | Lower effort -> fewer and more-consolidated tool calls, less preamble, terser confirmations. `medium` is often a favorable balance. Use `max` when correctness matters more than cost. |
See `SKILL.md` §Thinking & Effort for model support and parameter details.
---
## Designing Your Tool Surface
### Bash vs. dedicated tools
Claude doesn't know your application's security boundary, approval policy, or UX surface. Claude emits tool calls; your harness handles them. The shape of those tool calls determines what the harness can do.
A **bash tool** gives Claude broad programmatic leverage - it can perform almost any action. But it gives the harness only an opaque command string, the same shape for every action. Promoting an action to a **dedicated tool** gives the harness an action-specific hook with typed arguments it can intercept, gate, render, or audit.
**When to promote an action to a dedicated tool:**
- **Security boundary.** Actions that require gating are natural candidates. Reversibility is a useful criterion: hard-to-reverse actions (external API calls, sending messages, deleting data) can be gated behind user confirmation. A `send_email` tool is easy to gate; `bash -c "curl -X POST ..."` is not.
- **Staleness checks.** A dedicated `edit` tool can reject writes if the file changed since Claude last read it. Bash can't enforce that invariant.
- **Rendering.** Some actions benefit from custom UI. Claude Code promotes question-asking to a tool so it can render as a modal, present options, and block the agent loop until answered.
- **Scheduling.** Read-only tools like `glob` and `grep` can be marked parallel-safe. When the same actions run through bash, the harness can't tell a parallel-safe `grep` from a parallel-unsafe `git push`, so it must serialize.
**Rule of thumb:** Start with bash for breadth. Promote to dedicated tools when you need to gate, render, audit, or parallelize the action.
---
## Anthropic-Provided Tools
| Tool | Side | When to use it | What to expect |
| --- | --- | --- | --- |
| **Bash** | Client | Claude needs to execute shell commands. | Claude emits commands; your harness executes them. Reference implementation provided. |
| **Text editor** | Client | Claude needs to read or edit files. | Claude views, creates, and edits files via your implementation. Reference implementation provided. |
| **Computer use** | Client or Server | Claude needs to interact with GUIs, web apps, or visual interfaces. | Claude takes screenshots and issues mouse/keyboard commands. Can be self-hosted (you run the environment) or Anthropic-hosted. |
| **Code execution** | Server | Claude needs to run code in a sandbox you don't want to manage. | Anthropic-hosted container with built-in file and bash sub-tools. No client-side execution. |
| **Web search / fetch** | Server | Claude needs information past its training cutoff (news, current events, recent docs) or the content of a specific URL. | Claude issues a query or URL; Anthropic executes it and returns results with citations. |
| **Memory** | Client | Claude needs to save context across sessions. | Claude reads/writes a `/memories` directory. You implement the storage backend. |
**Client-side** tools are defined by Anthropic (name, schema, Claude's usage pattern) but executed by your harness. Anthropic provides reference implementations. **Server-side** tools run entirely on Anthropic infrastructure - declare them in `tools` and Claude handles the rest.
---
## Composing Tool Calls: Programmatic Tool Calling
With standard tool use, each tool call is a round trip: Claude calls the tool, the result lands in Claude's context, Claude reasons about it, then calls the next tool. Three sequential actions (read profile -> look up orders -> check inventory) means three round trips. Each adds latency and tokens, and most of the intermediate data is never needed again.
**Programmatic tool calling (PTC)** lets Claude compose those calls into a script instead. The script runs in the code execution container. When the script calls a tool, the container pauses, the call is executed (client-side or server-side), and the result returns to the running code - not to Claude's context. The script processes it with normal control flow (loops, filters, branches). Only the script's final output returns to Claude.
| When to use it | What to expect |
| --- | --- |
| Many sequential tool calls, or large intermediate results you want filtered before they hit the context window. | Claude writes code that invokes tools as functions. Runs in the code execution container. Token cost scales with final output, not intermediate results. |
---
## Scaling the Tool and Instruction Set
| Feature | When to use it | What to expect |
| --- | --- | --- |
| **Tool search** | Many tools available, but only a few relevant per request. Don't want all schemas in context upfront. | Claude searches the tool set and loads only relevant schemas. Tool definitions are appended, not swapped - preserves cache (see Caching below). |
| **Skills** | Task-specific instructions Claude should load only when relevant. | Each skill is a folder with a `SKILL.md`. The skill's description sits in context by default; Claude reads the full file when the task calls for it. |
Both patterns keep the fixed context small and load detail on demand.
---
## Long-Running Agents: Managing Context
| Pattern | When to use it | What to expect |
| --- | --- | --- |
| **Context editing** | Context grows stale over many turns (old tool results, completed thinking). | Tool results and thinking blocks are cleared based on configurable thresholds. Keeps the transcript lean without summarizing. |
| **Compaction** | Conversation likely to reach or exceed the context window limit. | Earlier context is summarized into a compaction block server-side. See `SKILL.md` §Compaction for the critical `response.content` handling. |
| **Memory** | State must persist across sessions (not just within one conversation). | Claude reads/writes files in a memory directory. Survives process restarts. |
**Choosing between them:** Context editing and compaction operate within a session - editing prunes stale turns, compaction summarizes when you're near the limit. Memory is for cross-session persistence. Many long-running agents use all three.
---
## Caching for Agents
**Read `prompt-caching.md` first.** It covers the prefix-match invariant, breakpoint placement, the silent-invalidator audit, and why changing tools or models mid-session breaks the cache. This section covers only the agent-specific workarounds for those constraints.
| Constraint (from `prompt-caching.md`) | Agent-specific workaround |
| --- | --- |
| Editing the system prompt mid-session invalidates the cache. | Append a `{"role": "system", ...}` message to `messages[]` instead (no beta header; on supporting models - see `prompt-caching.md` § Mid-conversation system messages). The cached prefix stays intact, and the model treats it as an operator-authority instruction rather than user text. On models that don't support it, fall back to a `<system-reminder>` text block in the user turn. |
| Switching models mid-session invalidates the cache. | Spawn a **subagent** with the cheaper model for the sub-task; keep the main loop on one model. On Managed Agents that is a `multiagent` roster entry - see `managed-agents-multiagent.md`. |
| Adding/removing tools mid-session invalidates the cache. | Use **tool search** for dynamic discovery - it appends tool schemas rather than swapping them, so the existing prefix is preserved. |
For multi-turn breakpoint placement, use the combination in `prompt-caching.md` § Automatic vs explicit breakpoints: one explicit breakpoint on the static system prefix plus top-level automatic caching for the conversation tail (where automatic caching is available).
---
For live documentation on any of these features, see `live-sources.md`.
# Anthropic CLI (`ant`)
The `ant` CLI exposes every Claude API resource as a shell subcommand. Compared to `curl`: request bodies are built from typed flags or piped YAML instead of hand-written JSON, `@path` inlines file contents into any string field, `--transform` extracts fields with a GJSON path (no `jq`), list endpoints auto-paginate (cap total results with `--max-items N`; `--limit` only sets the server page size), and the `beta:` prefix auto-sets the right `anthropic-beta` header.
## When to use the CLI vs the SDK
**CLI for the control plane, SDK for the data plane.** Agents and environments are relatively static resources you define, configure, and debug with `ant` - check the YAML into your repo, apply from CI, inspect from a terminal. Sessions are dynamic and driven by your application through the SDK - create per task, stream events, react to tool calls, integrate into your product. Both hit the same API; the split is about where the call lives, not what's possible.
| | Control plane -> `ant` | Data plane -> SDK |
|---|---|---|
| Resources | agents, environments, skills, vaults, files | sessions, events |
| Cadence | Once per deploy / ad-hoc | Every task / every turn |
| Lives in | `*.yaml` in your repo + CI + terminal | Application code |
| Typical calls | `create < agent.yaml`, `update --version N`, `list`, `retrieve`, `archive`, `--debug` | `sessions.create()`, `events.stream()`, `events.send()` |
## Install and auth
```sh
# macOS
brew install anthropics/tap/ant
xattr -d com.apple.quarantine "$(brew --prefix)/bin/ant"
# Linux / WSL - pick the release from github.com/anthropics/anthropic-cli/releases
curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${VERSION}/ant_${VERSION}_$(uname -s | tr A-Z a-z)_$(uname -m | sed -e s/x86_64/amd64/ -e s/aarch64/arm64/).tar.gz" \
| sudo tar -xz -C /usr/local/bin ant
# Or from source (Go 1.22+)
go install github.com/anthropics/anthropic-cli/cmd/ant@latest
```
**Auth** - the CLI resolves credentials the same way the SDKs do (first match wins): explicit flags, then `ANTHROPIC_API_KEY`, then `ANTHROPIC_AUTH_TOKEN`, then the `ANTHROPIC_PROFILE`-selected or active profile, then Workload Identity Federation env vars, then the default profile on disk. Override the host with `ANTHROPIC_BASE_URL` or `--base-url`.
- **API key**: set `ANTHROPIC_API_KEY` in the environment.
- **OAuth profile** (no static key to manage): `ant auth login` opens a browser, exchanges for a short-lived token, and stores a profile under `$ANTHROPIC_CONFIG_DIR` (default `~/.config/anthropic/` on Linux/macOS, `%APPDATA%\Anthropic` on Windows - `configs/<profile>.json` for settings, `credentials/<profile>.json` for tokens). Subsequent `ant` (and SDK) calls pick it up automatically - a bare `Anthropic()` client works after login, but scripts that read `ANTHROPIC_API_KEY` directly do not. Claude Code and the Claude Agent SDK honor the same profile resolution. `ant auth status` shows which credential source and profile won (it reports status only - don't script against its exit code as a health check); `ant auth logout` clears the active profile (`--all` for every profile). On a remote host without a browser, `ant auth login --no-browser` prints the authorize URL and accepts the code back in the terminal.
- **Non-interactive workloads** (CI, servers, containers): interactive login is for development on your own machine - use Workload Identity Federation instead (see the authentication docs via `shared/live-sources.md`).
> **The #1 auth trap:** profiles are only consulted when no API key is set. A stale exported `ANTHROPIC_API_KEY` silently overrides every profile - requests hit whatever org/workspace that key is scoped to. `ant auth status` shows which source won; unset the key (or per-command: `env -u ANTHROPIC_API_KEY ant ...`) before relying on a profile. Truly **unset** it - an empty `ANTHROPIC_API_KEY=""` still wins its precedence slot and authenticates with an empty key. The same shadowing applies in reverse to Claude Code: after `ant auth login`, Claude Code may warn about an auth conflict between the profile and its own `/login` credential - keep one (use the profile and `/logout` in Claude Code, or `ant auth logout` to keep Claude Code's own login).
**Named profiles** - an interactive-login token is bound to a single org+workspace, and the API only shows resources belonging to that workspace. If an agent, session, or file you created "disappears", the usual cause is a token scoped to a different workspace than the one that created it (`ant auth status` shows the active workspace). Multi-workspace work means one profile per workspace:
```sh
ant auth login --profile <name> # creates the profile if it doesn't exist; org/workspace picker in browser
ant auth login --profile <name> --workspace-id wrkspc_01... # bind directly, skip the picker
ant profile activate <name> # switch the default profile
ant --profile <name> models list # one-off; equivalent: ANTHROPIC_PROFILE=<name> ant models list
ant profile list # inspect
ant profile set workspace_id wrkspc_01... --profile <name> # edit config keys (workspace_id, base_url, organization_id, ...)
```
`ant profile set` edits an existing profile's config - it never creates one, and it does **not** rebind already-issued credentials; run `ant auth login` again under that profile to mint a token for the new target. Pointing `ANTHROPIC_PROFILE` at a profile that doesn't exist is an error, not a fall-through. Refresh tokens eventually hard-expire (they don't slide with use) - when a previously working profile starts failing auth, re-run `ant auth login` before debugging anything else.
**Scopes** - a profile's OAuth scope set is requested at login (`--scope`) and persists on the profile (`scope` is also a `profile set` config key; like other config edits, changing it requires a fresh `ant auth login` to take effect). Privileged scopes - e.g. `org:admin` for organization-administration endpoints - are **not** in the default scope set: pass the full set you want explicitly (`ant auth login --profile admin --scope "... org:admin"`), and the server grants a privileged scope only if your role actually has it. Because the scope set rides on every token the profile mints, keep privileged work on a dedicated profile (`admin` vs `default`) and do day-to-day inference on the unprivileged one, switching with `--profile`/`ANTHROPIC_PROFILE`. Check `ant auth login --help` for the current scope list, and `ant auth status` to see what the active token carries.
To hand the active credential to a subprocess or raw-HTTP script:
```sh
# Bare access token - for curl's Authorization header
curl https://api.anthropic.com/v1/messages \
-H "Authorization: Bearer $(ant auth print-credentials --access-token)" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "content-type: application/json" \
-d '{"model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}'
# .env format - sets ANTHROPIC_AUTH_TOKEN (and ANTHROPIC_BASE_URL if the profile has one).
# Output is bare KEY=value (no `export`), so use `set -a` to auto-export for child processes:
set -a; eval "$(ant auth print-credentials --env)"; set +a
python my_script.py # SDK picks up ANTHROPIC_AUTH_TOKEN
```
OAuth tokens go on `Authorization: Bearer` (not `x-api-key:`) **plus the `anthropic-beta: oauth-2025-04-20` header** - converting a raw curl/httpx script from an API key is a header change, not a key swap. The beta header requirement is endpoint-dependent (some endpoints happen to work without it; `/v1/messages` does not) - always send it so requests don't break when you switch endpoints. The token is short-lived and not auto-refreshed when passed via env var, so re-run `print-credentials` before it expires for long-running scripts (`print-credentials` itself refreshes the token if needed). If both `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` are set, the SDKs send both and the API rejects the request - unset `ANTHROPIC_API_KEY` before `eval`ing the `--env` output.
**Foot-gun:** `ant auth print-credentials` with **no flags** prints the entire credentials JSON, not the bare token - putting that in an `Authorization` header yields an empty response or HTTP/2 protocol error. Always use `--access-token` for headers (it always reads the named/active profile; a set `ANTHROPIC_API_KEY` doesn't override credential printing).
## Command structure
```
ant <resource>[:<subresource>] <action> [flags]
```
Beta resources (agents, sessions, environments, deployments, skills, vaults, memory stores) live under `beta:` - the CLI auto-sends the right `anthropic-beta` header, so don't pass it yourself unless overriding with `--beta <header>`. For self-hosted environments, `ant beta:worker poll/run` and `ant beta:environments:work stats/stop` drive and monitor the work queue - see `shared/managed-agents-self-hosted-sandboxes.md`.
```sh
ant models list
ant messages create --model claude-opus-5 --max-tokens 1024 --message '{role: user, content: "Hello"}'
ant beta:agents retrieve --agent-id agent_01...
ant beta:sessions:events list --session-id session_01...
```
`ant --help` lists resources; append `--help` to any subcommand for its flags.
## Global flags
| Flag | Purpose |
| --- | --- |
| `--format` | `auto` (default: pretty if TTY, compact if piped), `json`, `jsonl`, `yaml`, `pretty`, `raw`, `explore` (interactive TUI) |
| `--transform` | GJSON path applied to the response (per-item on list endpoints). Not applied when `--format raw`. |
| `-r`, `--raw-output` | If the transformed result is a string, print it without quotes (jq semantics). Pair with `--transform` for scalar capture. |
| `--max-items` | Cap total results returned from auto-paginating list endpoints (distinct from `--limit`, which is the server page size). |
| `--format-error` / `--transform-error` | Same as `--format`/`--transform`, applied to error responses. `-r` does not apply to the error path - use `--format-error yaml` for unquoted error scalars. |
| `--base-url` | Override API host |
| `--debug` | Print full HTTP request + response to stderr (API key redacted) |
## Output - `--transform` + `--format`
`--transform` takes a [GJSON path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md). On list endpoints it runs **per item**, not on the envelope.
```sh
ant beta:agents list --transform '{id,name,model}' --format jsonl
```
**Extract a scalar for shell use:** pair `--transform` with `-r` (`--raw-output` - prints strings unquoted, jq-style):
```sh
AGENT_ID=$(ant beta:agents create --name "My Agent" --model '{id: claude-sonnet-5}' \
--transform id -r)
```
## Input - flags, stdin, `@file`
**Flags** - scalar fields map directly. Structured fields accept relaxed-YAML syntax (unquoted keys) or strict JSON. Repeatable flags build arrays (each `--tool`, `--event`, `--message` appends one element):
```sh
ant beta:agents create \
--name "Research Agent" \
--model '{id: claude-opus-5}' \
--tool '{type: agent_toolset_20260401}' \
--tool '{type: custom, name: search_docs, input_schema: {type: object, properties: {query: {type: string}}}}'
```
**Stdin** - pipe a full JSON or YAML body. Merged with flags; flags win on conflict (for array fields, any flag **replaces** the stdin array entirely - it does not append). Quote the heredoc delimiter (`<<'YAML'`) to disable shell expansion inside the body:
```sh
ant beta:agents create <<'YAML'
name: Research Agent
model: claude-opus-5
system: |
You are a research assistant. Cite sources for every claim.
tools:
- type: agent_toolset_20260401
YAML
```
**`@file` references** - inline a file's contents into any string-valued field. Inside structured flag values, quote the path. Binary files are auto-base64'd; force with `@file://` (text) or `@data://` (base64). Escape a literal leading `@` as `\@`.
```sh
ant beta:agents create --name "Researcher" --model '{id: claude-sonnet-5}' --system @./prompts/researcher.txt
ant messages create --model claude-opus-5 --max-tokens 1024 \
--message '{role: user, content: [
{type: document, source: {type: base64, media_type: application/pdf, data: "@./scan.pdf"}},
{type: text, text: "Extract the text from this scanned document."}
]}' \
--transform 'content.0.text' -r
```
Flags that natively take a file path (e.g. `--file` on `beta:files upload`) accept a bare path without `@`.
## Version-controlled Managed Agents resources
This is the recommended flow for defining agents and environments - check the YAML into your repo and sync via `create` (first time) / `update` (thereafter). See `shared/managed-agents-core.md` for the field reference.
```yaml
# summarizer.agent.yaml
name: Summarizer
model: claude-sonnet-5
system: |
You are a helpful assistant that writes concise summaries.
tools:
- type: agent_toolset_20260401
```
```sh
# Create (once) - capture the ID
AGENT_ID=$(ant beta:agents create < summarizer.agent.yaml --transform id -r)
# Update (CI) - needs ID + current version (optimistic lock)
ant beta:agents update --agent-id "$AGENT_ID" --version 1 < summarizer.agent.yaml
```
Same pattern for environments (`ant beta:environments create|update < env.yaml`), then start a session with both IDs:
```sh
ant beta:sessions create --agent "$AGENT_ID" --environment-id "$ENV_ID" --title "Task"
ant beta:sessions:events send --session-id "$SID" \
--event '{type: user.message, content: [{type: text, text: "Summarize X"}]}'
ant beta:sessions:events list --session-id "$SID" --transform 'content.0.text' -r
ant beta:sessions:events stream --session-id "$SID" # live event stream
```
### Interactive session loop (stream-before-send)
`ant beta:sessions:events stream` only delivers events emitted *after* the stream opens - so open it **before** sending the kickoff to avoid missing early events. Use process substitution to hold the stream on a file descriptor, send, then read:
```sh
exec {stream}< <(ant beta:sessions:events stream --session-id "$SID" \
--transform '{type,text:content.#(type=="text").text,err:error.message}' --format yaml)
ant beta:sessions:events send --session-id "$SID" > /dev/null <<'YAML'
events:
- type: user.message
content:
- type: text
text: Summarize the repo README
YAML
type=
while IFS= read -r -u "$stream" line; do
case "$line" in
type:\ session.status_idle) break ;;
type:\ session.error)
IFS= read -r -u "$stream" next || next=
case "$next" in err:\ *) msg=${next#err: } ;; *) msg=unknown ;; esac
printf '\n[Error: %s]\n' "$msg"; break ;;
type:\ *) type=${line#type: } ;;
text:*)
[[ $type == agent.message ]] || continue
val=${line#text: }
case "$val" in '|-'|'|') ;; *) printf '%s' "$val" ;; esac ;;
\ \ *)
if [[ $type == agent.message ]]; then printf '%s\n' "${line# }"; fi ;;
esac
done
exec {stream}<&-
```
This works for interactive exploration and demos. For application code that needs to react to `agent.tool_use` / `agent.custom_tool_use` events, reconnect after drops, or dedup against `events.list`, use the SDK - see `shared/managed-agents-client-patterns.md`.
## Scripting patterns
`--transform id -r` on a list endpoint emits one bare ID per line - compose with `xargs`, or use `--max-items N` to bound the result set without piping through `head`:
```sh
FIRST=$(ant beta:agents list --transform id -r --max-items 1)
ant beta:agents:versions list --agent-id "$FIRST" --transform '{version,created_at}' --format jsonl
```
Error shaping mirrors the success path (note: `-r` does not apply to error output - use `--format-error yaml` for an unquoted scalar here):
```sh
ant beta:agents retrieve --agent-id bogus --transform-error error.message --format-error yaml 2>&1
```
Shell completion: `ant @completion {zsh|bash|fish|powershell}`.
For the full, always-current reference (including per-endpoint flags), WebFetch the **Anthropic CLI** URL in `shared/live-sources.md`.
# Claude Platform on AWS
**Anthropic-operated** access to the Claude Developer Platform through AWS infrastructure - SigV4 authentication, AWS IAM access control, and AWS Marketplace billing. Because Anthropic operates it, **the API surface matches first-party with same-day parity** - for per-feature exceptions, see `shared/platform-availability.md` (the single source of truth; do not rely on an inline exception list here). Model IDs are the bare first-party strings (`claude-opus-5`, `claude-sonnet-5`) - **no provider prefix**.
> **Not the same as Amazon Bedrock.** Bedrock is partner-operated (AWS runs the service; release schedules vary, feature subset, `anthropic.`-prefixed model IDs). Claude Platform on AWS and Bedrock coexist; pick by whether you need AWS-native IAM/billing with full Anthropic API parity (this page) vs. Bedrock's own ecosystem.
---
## Client & install
| Language | Install | Client |
|---|---|---|
| Python | `pip install -U "anthropic[aws]"` | `from anthropic import AnthropicAWS` -> `AnthropicAWS()` |
| TypeScript | `npm install @anthropic-ai/aws-sdk` | `import AnthropicAws from "@anthropic-ai/aws-sdk"` -> `new AnthropicAws()` |
| Go | `go get github.com/anthropics/anthropic-sdk-go` | `import anthropicaws "github.com/anthropics/anthropic-sdk-go/aws"` -> `anthropicaws.NewClient(ctx, anthropicaws.ClientConfig{})` |
| C# | `dotnet add package Anthropic.Aws` | `new AnthropicAwsClient()` |
| Java | See SDK repo in `shared/live-sources.md` | See SDK repo in `shared/live-sources.md` |
| Ruby | `gem install anthropic aws-sdk-core` | See SDK repo in `shared/live-sources.md` |
| PHP | `composer require anthropic-ai/sdk aws/aws-sdk-php` | See SDK repo in `shared/live-sources.md` |
After construction, **use the client exactly as you would `Anthropic()`** - `client.messages.create(...)`, `client.beta.sessions.*`, etc., with bare model IDs.
```python
from anthropic import AnthropicAWS
client = AnthropicAWS() # region + workspace_id from env; see below
client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
```
---
## Required configuration
Two values must be available (constructor args or environment) - **there is no default fallback** for either:
| Value | Env var | Notes |
|---|---|---|
| AWS region | `AWS_REGION` | Required. Unlike `AnthropicBedrock`, there is no `us-east-1` fallback. |
| Workspace ID | `ANTHROPIC_AWS_WORKSPACE_ID` | Required. Routes requests to your Claude workspace. |
Endpoint pattern: `https://aws-external-anthropic.{region}.api.aws/v1/...`. Requests are SigV4-signed with service name `aws-external-anthropic`.
## Authentication
The client resolves AWS credentials via the standard precedence chain: explicit constructor args -> environment (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`) -> shared profile -> assumed role / instance metadata.
**Short-term API keys** are also supported for cases where SigV4 isn't practical (e.g., browser, simple scripts). Mint one with the per-language token-generator package; pass it as `api_key` on the client. Lifetime is the **lesser of** the requested duration, the underlying credential's expiry, and **12 hours**. For package names and IAM details, WebFetch the Claude Platform on AWS page in `shared/live-sources.md`.
---
## What to tell users
- Treat it as first-party: every section of this skill applies unchanged. Do **not** apply Bedrock's feature-availability mask. Three Managed Agents differences only: (1) a session can run autonomously (no user events) for at most **6 hours** before it needs reauthentication - send any user-role event to continue; (2) sessions on **self-hosted** environments **cannot attach memory stores** (rejected at session create) - cloud environments attach them as usual; (3) self-hosted workers authenticate with IAM/SigV4 or an AWS-Console API key plus the `AnthropicSelfHostedEnvironmentAccess` managed policy - Console-generated environment keys don't work against the AWS endpoint.
- Model IDs are bare (`claude-opus-5`). Do **not** add an `anthropic.` prefix.
- A missing region or `workspace_id` throws at client-construction time (no request is sent). A **403** means the request reached the server - check for a **wrong** `workspace_id` or a missing IAM action on the principal. See the IAM actions reference in `shared/live-sources.md`.
# Cost Optimization - Cutting Spend per Completed Task > **If you arrived via `/claude-api cost-optimize`:** this is the right file. Execute the steps below in order rather than summarizing the guide back to the user - presenting the profile, the ranked plan, and the findings IS part of the execution. Start with Step 0 (establish scope, quality bar, and baseline), and finish with Step 4's two deliverables: the cost profile and the changes. API spend is optimized in units of **cost per completed task, not cost per token**. A model with a higher sticker price can be the cheaper option if it finishes the job in fewer turns, and a cheaper model that fails still bills its tokens, then the retry, then whatever the failure costs downstream. Every judgment below reads cost and quality together. The levers divide into two kinds, and the order of the steps is load-bearing: - **Free wins** - prompt caching, input-token hygiene (including a prompt audit), loop hygiene, output-token hygiene, batch processing - lower what you pay without lowering output quality. They go first, and caching stays on permanently. - **Tradeoffs** - budgets, effort, model choice, multi-model architectures - exchange cost for intelligence. They go last, because each one changes what the model can do, and overshooting costs quality that the free wins never touch. **Where this workflow sits**: the `prompt-audit` subcommand (`shared/prompt-audit.md`) audits the prompt surface (prompts, skills, tool descriptions) alone; this workflow is the holistic cost pass - request shape, caching, loop structure, output, batching, effort, model - and runs that audit as one sub-lever of input hygiene (§ 2.2) rather than restating its patterns; and once the project has an eval, the levers become a hillclimb - one change at a time against the eval, keep or revert (Step 3). Measured expectations quoted below are snapshots of Anthropic's published runs (sources at the end). They are directional, not guarantees - the validation loop in Step 3 is what makes a number true for this project - and both sources are fetched live - the platform guide through `shared/live-sources.md`, the cookbook at its URL in the Sources section below: wherever a fetched page differs from this snapshot, the page wins. --- ## Step 0: Establish scope, quality bar, and baseline **First, establish three things - from the request and the repository where they answer it, and from the user where they don't.** Unlike the prompt audit, this workflow is interactive by design: when context for a lever is missing, or a step would spend real money, work through it with the user rather than assuming. It is not expected to one-shot the audit. State all three at the top of the report (the baseline value itself may read "pending Step 1" at first). 1. **Scope.** If the request names files or directories, that is the scope. Otherwise it is every place the project calls the Claude API - request builders, agent loops, batch jobs. Note distinct traffic classes (an interactive path and a nightly job are different workloads even on one key): the profile, the ranking, and every validation later run per class, and "cost per task" means nothing blended across classes. **Also establish which platform** the code targets (first-party Anthropic API, Claude Platform on AWS, Bedrock, Vertex, or Foundry) - feature availability varies, and it filters which levers are even on the table. 2. **Quality bar.** Find the project's eval, test suite, or outcome checks for its LLM calls. If none exists, say so prominently in the report: without one, savings cannot be told apart from regressions. Do not stop - free wins are safe to propose regardless - but mark every tradeoff lever "needs an eval before applying", and ask the user what outcome check they can provide. An eval only validates the traffic class it covers: mark levers on uncovered paths the same way. If the only check is the user's own manual review, it gates free wins - it never clears a tradeoff. The full no-eval endgame - including a minimal eval recipe that unblocks tradeoffs - is in Step 3. 3. **Baseline cost per task.** The baseline is whatever honest number is cheapest to obtain, in this order: - **From history, free**: with Admin API access, pull Step 1's usage and cost reports forward and compute the baseline from them - the reports supply the dollars, but the per-task denominator must come from the user or the application's own logs; or roll up the application's own logged `usage` objects per task, not per request - four token counts, each at its own rate: regular input, cache writes (1.25x input for the 5-minute duration, 2x for 1-hour), cache reads (0.1x input), and output - multiplier structure as published on the pricing page; confirm it when you fetch the rates. - **From a baseline run, paid**: run the project's eval (or, with no eval, replay a representative sample of real requests) and roll up the same way. This spends real API money: state the expected cost - from Step 1's token estimates and live pricing, and "estimated - pending Step 1" is an acceptable first answer - **and get the user's approval before running it.** If the user declines the spend, estimate the baseline from the code and any bill figure they can read off the Console, label it an estimate, and continue. For current per-model rates, WebFetch the **Pricing** URL from `shared/live-sources.md` - prices change; do not quote remembered ones (if the pricing fetch fails, effective realized rates come from dividing cost-report amounts by the usage report's matching token counts - same model, same token type). For counting tokens in prompts and files, see `shared/token-counting.md` (`count_tokens` returns the count without running inference). Sanity-check an estimated baseline against any known monthly bill: divergence usually means multi-turn history growth the single-turn estimate missed. ## Step 1: Profile where the tokens go The profile can be measured or estimated. Measure when the organization's access allows it; fall back to reading the code. Either way, the levers that pay are decided by the workload's shape, not by the list of what exists. ### Measure it - the Usage and Cost Admin API (preferred) If the user has an **Admin API key** (`sk-ant-admin01-...` - a different key type from the standard API key; not available for individual accounts - creation and scopes are covered in the Admin API docs, reachable from the **Usage and Cost Admin API** URL in `shared/live-sources.md`), pull the real numbers instead of estimating. These are report reads, not model calls - they consume no tokens. Full parameters and response schemas: the **Usage and Cost Admin API** URL in `shared/live-sources.md`. - **Token profile**: `GET /v1/organizations/usage_report/messages` with `group_by[]=model` and `bucket_width=1d` (the default page is 7 daily buckets - raise `limit`, up to 31; the `group_by` dimensions also include `api_key_id`, `workspace_id`, `service_tier`, and `context_window`, among others). Each result splits into exactly the quantities the levers below act on: `uncached_input_tokens`, `cache_read_input_tokens`, `cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`, and `output_tokens`. - **Dollar profile**: `GET /v1/organizations/cost_report` (daily granularity, USD as decimal strings in cents) with `group_by[]=description`; description-grouped results carry structured `model`, `cost_type`, `token_type`, and `service_tier` fields - `token_type` makes the cache split readable directly in dollars. Code execution appears under a `Code Execution Usage` description; Priority Tier costs are not included in this endpoint - track those through the usage endpoint's `service_tier` dimension. - Data appears within about 5 minutes of a request completing; poll at most once per minute for sustained use. - Caveats by platform: Claude Enterprise (claude.ai) organizations use the Analytics API instead, and the endpoints are not currently available on Claude Platform on AWS - there, ask the user to read the totals off the Console's Usage and Cost pages and relay them. The measured profile answers directly: the real cache hit rate (`cache_read_input_tokens` against uncached input), how much traffic already rides the batch tier, the input/output balance, and where spend concentrates by model, key, and workspace. **Check that the measured footprint plausibly matches the audited code** (same models, a believable order of magnitude): the report covers the whole organization, and a key shared across projects blends their traffic - making per-project reads, including Step 3's post-cutover confirmation, unattributable. On a mismatch, reconcile against the code estimate, scope usage-report queries by `api_key_ids[]` / `workspace_ids[]` where the separation exists (the cost report takes neither filter - it segments only by workspace, via `group_by`), and recommend per-project keys or workspaces as a measurement prerequisite where it doesn't. Optimization effort follows the audited scope's spend, not the org blend. ### Estimate it from the code Without Admin API access (no Admin key, a Claude Enterprise organization, or Claude Platform on AWS - whose feature availability `shared/claude-platform-on-aws.md` covers) - and even with it, for the structural facts no usage report can show - read the request-building code: > **Per-model defaults, parameter support, and per-platform feature availability change across releases.** For any "what happens when `thinking`/`effort` is omitted", "does this model accept `effort`", "what levels does it support", or "is this feature available on Bedrock/Vertex/Foundry" question, read the answer from SKILL.md -> Thinking & Effort, `shared/models.md`, or `shared/platform-availability.md` (or the live Models API) - never assume, and never encode the answer in this guide. - **Prefix**: how large are the system prompt and tool schemas, and is anything dynamic (timestamps, request IDs) interpolated into them? - **Reference material**: is documentation or a manual inlined into every request? - **Tools**: how many schema tokens, and does every request need every tool? - **Loop**: how many turns deep, and do bulky tool results accumulate across them? - **Media**: are images, PDFs, or large files entering the context at full size? - **Output**: how long are visible responses, and what is `max_tokens` set to? - **Model and effort**: which model, which effort, and was either ever swept against an eval? Look up what the model does when both are omitted (SKILL.md -> Thinking & Effort) - an unset default that runs thinking is a hidden output-token line item. - **Caching**: are there `cache_control` breakpoints already, and what do `cache_read_input_tokens` / `cache_creation_input_tokens` show in practice? - **Latency tolerance**: is a user waiting on every response, or can some work batch? ### Ask for the app's own usage logs first Before ranking on estimates, **ask the user whether the application already logs `response.usage` per request** - and if so, to paste a representative day's worth. That turns cache hit rate, the input/output split, and thinking-token spend from guesses into measurements at zero API cost, and it decides which tier of the ranking table below applies. If the app doesn't log usage yet, note that adding it is itself a free-win diff (Step 3) and proceed on the code estimate. **Estimating cache hit rate without usage data.** If the app logs request timestamps, simulate the TTL walk: sort timestamps, count a hit whenever the gap to the previous request is <= TTL (reads refresh the entry), and run it for each cache TTL the platform offers (see `shared/prompt-caching.md`) - the difference between durations is the longer-TTL lever's ceiling on the user's real traffic. If only aggregate volume is known, approximate with Poisson arrivals: hit rate ~ `1 - e^(-lambda·TTL)` where lambda is requests per second. Either beats comparing average gap to TTL, which ignores burstiness. ### Rank the levers Before touching code, size each lever the profile makes applicable so the shortlist can be ordered. **How you quote the size depends on what data you have** - an estimate and a measurement must not look the same in the report: | Data available | Quote each ceiling as | |---|---| | Admin API usage/cost report | **Dollar range**, labeled `measured` | | App-side `usage` logs, or a user-reported bill total only | **% of current bill**, with dollars only as a parenthetical "(~ $Y at your reported $X/mo)" - the % is the claim; the $ is the user's own arithmetic | | Neither (pure code read) | **Relative buckets** - "largest / medium / small", or an order-of-magnitude band - no specific figures | **Before sizing, drop any lever the target platform doesn't support** (`shared/platform-availability.md` is the single source of truth - do not assume 1P availability carries to Bedrock, Vertex, Foundry, or Claude Platform on AWS). A lever that can't ship on the user's platform isn't worth ranking; list it under "skipped" with the availability reason instead. Within whichever unit applies, size each lever from the measured (or estimated) spend components and the measured expectations quoted in Step 2 - for example: - **Caching ceiling**: the spend on input that is shared and byte-stable across requests - the would-be prefix - re-billed at 0.1x. (0.025x on Claude Fable 5.1 - whether Claude Mythos 5.1 shares that rate is open at launch - so its cost per task sits at or under the Claude Fable 5 figures quoted below.) Blend the measured `uncached_input_tokens` with the code profile here: unique per-request payload can never cache, so on a workload that is mostly payload (or already well cached) this ceiling is honestly small. Sanity-bound the result against the published agent-loop range (a factor of 2.5 to 3.7 off at 81% to 90% hit rates). - **Batch ceiling**: 50% of the spend on standard-tier traffic that no one is waiting on. The model-grouped profile cannot see that split - segment first: group by `service_tier` to find what already batches, use a finer `bucket_width` to spot scheduled spikes, and ask the user which traffic can wait. - **Input-hygiene ceiling**: the share of input spend going to reference material, tool schemas, or oversized media that the § 2.2 levers would remove or defer. - **Effort/model ceiling**: the published tradeoff curves applied to the biggest spend concentrations - carried as a range, since the quality cost is unknown until the eval runs. Ceilings that claim the same tokens (caching an inlined document versus deleting it) are mutually exclusive: compute each ceiling unconditionally, rank, then deflate each for its overlap with the levers above it, so the shortlist can never sum past the bill. Present the ranked shortlist with the profile evidence behind each number - labeled as ranked by savings ceiling, not application order (Step 2's § 2.x numbering decides the sequence) - and say where the list stops: a lever whose ceiling is a small fraction of the bill - or would not repay the approved runs and effort needed to validate it - does not earn an eval cycle, and most levers will not earn a place on any given workload (the "Workload shape -> lever" table near the end of this file is the map for matching profile to levers). On a small bill the honest shortlist may be empty: "nothing here is worth changing" is a successful finding, not a failure - report it plainly. Expected savings are planning numbers, not results - Step 3's measurements are the results. ## Step 2: Work the levers in order Free wins may be applied directly when the request asked for edits (a bare subcommand invocation has not asked - propose). Tradeoff levers (2.6 onward) are always presented with their measured quality cost and applied only on the user's explicit acceptance - never trade accuracy for cost silently. And every run that exercises the model - the baseline, each lever's validation pass - spends real API money: get explicit approval before each one, with the expected cost, or once as a Step 3 measurement budget that covers them. Pricing multipliers quoted below (cache read/write rates, batch discount) are current as of writing - confirm against the Pricing URL in `shared/live-sources.md` before computing any ceiling. ### 2.1 Prompt caching - first, and it stays on Every turn of an agentic task resends the entire growing conversation - system prompt, tool definitions, every prior turn - so a 40-turn task sends its first turn 40 times and task cost grows with roughly the square of turn count. Caching does not stop the resending; it reprices it to 0.1x for everything already cached. For design and placement - the prefix-match invariant, classifying inputs by stability, breakpoint patterns, the anti-pattern table - **read `shared/prompt-caching.md` and follow its workflow**; do not improvise `cache_control` markers. Points that matter specifically for cost: - **Measured expectation**: the largest single lever on every model and benchmark Anthropic measured - it cut agent-loop cost by a factor of 2.5 to 3.7, at 81% to 90% hit rates; a small issue-triage agent's bill fell 83% from caching alone. - **Explicit breakpoints when many independent conversations share a static prefix** (or prefix layers change at different rates). Automatic caching only amortizes within one conversation; in the cookbook's worked example, one explicit breakpoint on the static system prefix roughly halved cost per task across a queue of independent tasks. The robust shape for agent loops - one explicit breakpoint on the static prefix plus top-level automatic caching for the tail - and the cases where automatic alone is a pure surcharge are in `shared/prompt-caching.md` § Automatic vs explicit breakpoints. - **Use the 1-hour cache duration when the loop waits on humans between turns.** It writes at 2x instead of 1.25x and pays for itself on the first prevented miss - a miss resends the whole prefix at full price and writes it again. Decide from the start-to-start gap between requests (generation time counts against the TTL) - the table in `shared/prompt-caching.md` § Choosing the TTL. - **Audit for mid-task cache-breakers**: dynamic content above a breakpoint; changing `thinking` or `effort` between requests (always invalidates the messages cache, and on some models the tools+system cache too - `shared/prompt-caching.md` § Invalidation hierarchy); changing a task budget mid-task; every context-editing pass; switching models mid-conversation (caches are per-model). - **Verify from usage, not from code review - and re-verify after every prompt-assembly change**: on a warmed-up loop, `cache_read_input_tokens` should dominate regular `input_tokens`, and `cache_creation_input_tokens` should be roughly one turn's worth, not the whole conversation. If it isn't, hunt for a cache-breaker with the healthy-loop signature and payload-diff method in `shared/prompt-caching.md` § Verifying cache hits - unless the workload's input is mostly unique per-request payload (which can never cache), or the misses are concurrent-batch artifacts (§ 2.5); neither is a breaker, and neither has a fix. - **The cache probe, when there is no usage history to read**: a scratch script for the project's own stack that sends one representative request twice, byte-identical; prints all four usage meters (`input_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens`, `output_tokens`) for both; and exits non-zero if the second request's `cache_read_input_tokens` is zero. Ship it alongside the caching diff so the user can run the before/after themselves. It spends real tokens and may execute the project's tools - run it only under the standing approval rule, and point it at a scratch environment if the request's tools mutate state. ### 2.2 Input tokens - progressive disclosure Send the model what the task needs, let it fetch the rest. Each sub-lever has a skip-when; the caveat at the end of this section governs all of them. - **Large reference document in every prompt** -> move it behind a tool or skill so the model retrieves sections on demand. Skip when most calls consult most of it anyway - a document in the cached prefix is cheap - or when the eval shows misses on cases that hinge on rules the model now has to go looking for. - **Tool recaps in the system prompt** -> delete them. Tool schemas already render into the request; prose restating them only inflates the prefix. - **Many or heavy tool schemas** -> tool search with `defer_loading` on rarely-used tools, so definitions load only when needed. Pays once schemas run past roughly 10K tokens (MCP servers reach that fast); below that the search step is overhead. Measurement gotcha: the token-counting endpoint rejects server tools - read billed input off a `max_tokens: 1` request instead (a paid, if tiny, model call: it sits under the standing approval rule). - **Images and PDFs at full resolution** -> pre-downscale to what the task needs. Vision inputs are tokenized by pixel area at roughly one token per 28×28 patch, so cost scales with resolution, not information content; 1280×720 is a safe default that caps an image near 1,200 tokens (current formula - verify via the Vision docs in `shared/live-sources.md`). - **Large tables and artifacts inlined** -> Files API plus code execution: mount the file, let the model compute in the sandbox, and only the answer enters context. Skip when there is nothing to extract or compute - the sandbox round-trip only adds tokens (and sandbox container time bills hourly beyond a free allowance). - **Fetched web pages** -> dynamic filtering in the web fetch tool keeps boilerplate out of the context. - **Chained tool calls whose intermediates don't matter** -> programmatic tool calling runs the calls from code so only the filtered result enters context; its documentation reports 24% fewer input tokens on agentic search benchmarks, with a higher score. - **Broad data-dump tools** -> prefer narrow accessors (`get_policy(claim_id)` over `get_all_policies()`), and give list tools `limit`/`fields`/`date_range` parameters. - **Unbounded user-supplied input** -> the token-counting endpoint as an ingestion gate (`shared/token-counting.md`): count first, then truncate, summarize, or route oversize payloads to the Files API. - **The prompt text itself** -> run the `prompt-audit` subcommand (`shared/prompt-audit.md`) as part of this step; its pattern tables are the reference for dated prompt text (this guide deliberately does not restate them), and its report and proposed diff fold into this workflow's deliverables. Skip when the prompt surface is small and recently audited. Prompts written for an older model make the current one over-work: on a support-desk evaluation, prompts written for Claude Opus 4.8 cost 36% more per ticket on Claude Opus 5 for no change in accuracy; audited, the same prompts were 14% cheaper than unaudited and more accurate (97% of tickets, up from 92%). On the Claude Sonnet 4.6 to Claude Sonnet 5 migration the audit took 14% off at the same accuracy. **Caveat for the whole section**: a smaller prefix is not automatically a cheaper task. Deferring context means the model may spend discovery turns fetching what it previously read inline. Validate against the eval - on the cookbook's workload, wrapping the manual in a tool matched the explicit-breakpoint config on cost and gave back accuracy. ### 2.3 Agent-loop hygiene - keep long loops from compounding Only relevant when the profile shows deep loops with bulky accumulating results; short loops never trigger these and the added machinery is pure overhead. - **Context editing** (clearing old tool uses or thinking) **is a context-window tool, not a savings lever.** Every clearing pass rewrites the cached conversation, which works against prompt caching - in the run measured for the platform docs, context editing cost more than it saved. Use it to make room in the window; set the trigger high enough that clears stay infrequent, and clear in a few large batches rather than every turn. - **Compaction** (the server-side summarize-and-continue edit) needs sessions long enough to reach its trigger; where it fired once on a long triage run it cut the bill a further 38%. Steer it with its `instructions` string so task-critical state survives the summary. - **Client-side pruning at natural boundaries**: collapse bulky tool results to one-line extracts when a work phase completes, keeping the message array byte-identical between prunes so each prune is one cold cache miss rather than a new miss every turn. - **Subagents for self-contained bulky steps**: a nested loop absorbs its own heavy tool results and hands back one line, optionally on a cheaper model. Skip when the deciding model needs the intermediate context to judge well - and note the subagent starts a fresh prefix with no cache shared with the parent. ### 2.4 Output tokens - **`max_tokens` is a backstop, not a tuning knob.** The model never sees it; hitting it cuts the response off mid-thought with `stop_reason: "max_tokens"`. In Anthropic's coding runs a 16,384-token cap ended 15% of Claude Opus 5's attempts and a third of Claude Fable 5's, none of them solved - capped runs spent less per attempt and bought proportionally fewer solves, so cost per solved task didn't improve. Set it to 64,000 for agentic work (128,000 at `xhigh` or `max` effort), stream responses that large, and treat `stop_reason: max_tokens` as a failed attempt rather than retrying at the same cap. - **To shorten visible responses**, specify the exact output shape in the prompt, ideally with an example. To shorten reasoning, that is the effort parameter (§ 2.6) - not `max_tokens`. - **Stop sequences as content-aware early exits**: register a sentinel the model emits when it cannot proceed (for example `<CANNOT_REVIEW>`), so it stops instead of spending tokens explaining. ### 2.5 Batch processing 50% off **every token in the request, including cache reads and writes** - the discounts stack. The second-largest free lever after caching for unattended agent work - evaluation runs, backfills, scheduled jobs. - Results arrive asynchronously within 24 hours; that window is an expiry, not an SLA. Keep user-facing work synchronous. - Batch requests are single-shot - no mid-batch tool loop. A tool loop can sometimes be flattened into one batchable request by pre-fetching its inputs up front; in the cookbook's worked example that ran at roughly half the interactive config's cost, but it is an architecture decision, not a parameter - it changes how the model reasons (the flattened run held its pass rate less firmly), and cache hits inside a concurrent batch are best-effort. - Not available for Managed Agents sessions (current mechanics and availability: the **Batch Processing** URL in `shared/live-sources.md`). ### 2.6 Effort and budgets - the first tradeoffs From here down, every lever trades capability for cost. Sweep on the eval, one change at a time. - **Sweep effort before touching the model** (on models that expose an effort parameter - check `shared/models.md` or the **Effort Parameter** URL in `shared/live-sources.md`). Effort scales thinking and tool-call depth without changing the model. Test each level in a separate session - changing effort mid-session invalidates the cache and distorts the comparison. Sweep mechanics that keep the comparison honest: - Cells are byte-identical except `output_config.effort`; same model throughout. Complete every sample request at one setting before starting the next, in a stable order, so cache reads are comparable across settings - and if the cache meters still differ materially between settings, say so and weight the read toward output-side cost. - Include a hard case the user knows about: curves are flattest on easy tasks, and the hard tail is where higher effort earns its cost. - **Side-effect gate**: if replaying a sample request executes tools that mutate real state, point the replay at a scratch environment or stub those tools first; a sweep is never worth a production mutation. If that isn't possible, sweep only the requests that are safe to replay and say so. - Read the curve as flat (the lower setting does this workload's work), steep (the higher setting is earning its cost - now a measured number rather than a fear), or mixed (name which tasks flipped - those are the candidates for the re-run-failures policy below). Differences of a task or two of pass rate, or cents of mean cost, are within noise on single runs; the remedy is repeat trials at the settings in contention, offered with their cost. - The curve is per-workload *and* per-model. Keep the sample and the outcome check where the report says they live, and re-sweep after a model migration, a major prompt change, or a workload shift. What to expect by workload shape: - Research and knowledge work: nearly flat curves - in Anthropic's runs (all with Claude Fable 5), `low` gave up 1 to 3 points for a third to a half off cost per task; `medium` matched the default's accuracy at 70% to 85% of its cost; the default bought nothing measurable over `medium` on any of the four benchmarks measured. Lower effort is also faster (4.5 versus 7.9 minutes per problem on one research benchmark). - Long-horizon coding: a real tradeoff - Claude Opus 5 gave up about 2 points at `medium` for half the cost, and about 8 points at `low` for a quarter of it. - Reasoning-ceiling work (deep multi-subtopic research): every effort step bought about 2.4 rubric points - no free cut on that curve. - **Re-run failures at higher effort** - when the workload has a usable failure signal (tests, a checker, a validator). Run everything at `low` and re-run failures at the default: in Anthropic's coding runs, about 93% passed for about $0.70 per task, against 91.7% for $1.39 running everything at the default - the same pass rate for half the cost, counting the failed cheap attempts. Starting at `medium` solved about 94% for about $0.95. Use this for the saving, not the lift, and price in the checker and the doubled wall-clock on failures. - **Task budgets** (the model sees the budget and paces itself - this is the budget control that saves money): set from the loop's 90th-percentile token usage, then tighten. The budget is advisory - it steers the model rather than stopping it - so verify adherence on the workload. Measured on coding: a generous budget gave up about 2.7 points of pass rate for an 18% saving; the tightest allowed budget gave up 4.4 points for 47%. Budgets below the 20,000-token floor are rejected; very tight budgets can produce refusal-like behavior; set the budget once on the first request - a mid-task change invalidates the cache. Check model availability before wiring it in (beta, and not available on every current model) - parameter shape, the streaming requirement, and supported models are in this skill's SKILL.md -> Task Budgets (Quick Reference) and `shared/model-migration.md` -> Task Budgets. - **Backstops that don't save per-task money but cap the damage**: a Managed Agents session budget is a hard dollar stop; a workspace spend limit is the final backstop on the whole workspace. ### 2.7 Model selection - last, deliberately Model choice constrains the intelligence ceiling, which is why it comes after every lever that doesn't. - **Price candidates in cost per completed task on your own traffic**, including the larger model at reduced effort - per-token price lists do not predict the ranking. In Anthropic's runs, Claude Fable 5 at `low` effort beat Claude Sonnet 5 on a deep-research benchmark while costing about 10% less per task; on a coding subset both models largely saturate, Claude Opus 5 matched Claude Fable 5 (91.7% versus 91.3%) at about 60% of its cost. For most agent workloads, start with Claude Opus 5. At the other end, Claude Haiku 4.5 answered knowledge questions at about a tenth of Claude Opus 5's cost per question at 63% accuracy versus 92% - it fits high-volume work with checkable outputs, not long agentic loops. - **Price the tail, not the median.** Compare models on the hardest tenth of the workload: on the typical task every model looks similar and the cheapest looks best, but the bill is decided by the tasks the cheap model fails - and the tail is where the money goes even when nothing fails (on one 20-problem research run, two problems carried 43% of the spend). - **The stepping-down method**: sweep effort on the current model first; if `low` passes the eval, drop one model tier, **confirm which parameters and effort levels the target tier supports** (SKILL.md -> Thinking & Effort), reset effort to that tier's default - not a hardcoded level; the default and the supported range vary by model - and re-sweep down from there (on a tier without `effort` support, evaluate at its single default only). One notch at a time, against the eval - and when there is no cheaper tier, the lever is exhausted; say so rather than inventing a step. Current model lineup and discovery: `shared/models.md`; for model-swap mechanics and per-target breaking changes, the `migrate` subcommand (`shared/model-migration.md`). - **Two models can beat one, in exactly two measured shapes** - both are architecture changes; validate like one: - **Advisor** (a cheaper executor runs the loop and consults a frontier model on hard decisions): pays when the capability gap between the two models is wide and the executor actually consults. The consult rate is the fragile variable - lowering effort can drop a pairing from consulting on most tasks to almost none, and then it scores below the executor alone - and gating the consult well requires a cheap signal; asking the executor to recognize the hard cases itself demands the very judgment it's missing. Benchmark first: on Anthropic's coding benchmark the flagship pairing was the most accurate configuration measured but sat within noise of the frontier model alone at `medium` effort, at about the same cost - sweep effort and price the stronger model alone before adding the advisor. - **Orchestrator** (a frontier model plans and delegates bulk work to cheaper workers): buys something only when there is bulk to hand off - many independent pieces, ideally too many for one context window. On work larger than any context window it cost 55% less than the frontier model solo at every effort setting (3 to 7 points below its best score); on routine search work it paid as tail insurance (about half the average cost, a third at the 90th percentile) but reversed on the harder full set. When the work is one dependent chain, or fits in a single context, the orchestrator pays for a plan, a handoff, and a merge that a single model gets for free - in every such case measured, the coordinator's model alone at lower effort came out ahead. ## Step 3: Apply, measure, keep or revert - one lever at a time - Work down the ranked shortlist to decide which levers earn a diff - but **apply shortlisted levers in the § 2 order** (free wins -> effort/budgets -> model), not in savings-rank order: the ranking decides inclusion and where the eval budget goes; the § 2.x numbering decides sequence. Each lever that earns a place becomes **its own diff** (one lever per diff, so a revert is clean and effects attribute), applied and then measured: re-run the eval covering that lever's traffic class, and read pass rate and cost per task together against the previous kept configuration (the baseline for the first lever only). A lever that saves money and gives back accuracy is not an optimization - revert it and record why. A lever touching a path no eval covers cannot be validated by the eval you have: a free win there is measured on cost only, and said so; a tradeoff there stays an unapplied proposal (Step 0.2's marking rule). - **Ask for the measurement budget once, not per run.** Present the validation plan with its total expected runs and cost - an effort sweep is several configurations at several trials each - and get it approved as a budget; within an approved budget, individual runs need no fresh approval. A shadow-run on live traffic roughly doubles production spend while it runs: it is its own approval. - **Never keep or revert on a one-case swing.** Repeat trials within the approved budget until the decision clears the noise. The published bar - around fifty cases and at least five trials per configuration - is the standard for the production cutover; a smaller project eval is acceptable for per-lever decisions when trials are repeated. And validating a caching diff needs a warm cache: run the sample sequentially and measure from the second request on, or the 1.25x writes dominate and the free win reads as a regression. - **When the user can provide no outcome check at all**: free wins become cost-only-measured diffs (or proposals, if no spend is approved), tradeoffs stay unapplied proposals carrying the published expectations, and offer a manual before/after spot-check of a handful of real answers - the user's review gates free wins, never a tradeoff. For an effort sweep specifically, a cost-only run is still worth offering: the same matrix with no pass-rate column, reporting per task the outputs at each setting laid side by side - exactly what the user needs in front of them to judge quality themselves. State plainly in the report which mode ran, and do not invent a grader to fill the gap. If the application doesn't log usage, adding `response.usage` logging is itself a free-win diff, and it is the measurement channel for everything after it when there is no Admin API key. - **Minimal eval recipe** - the cheapest thing that clears a tradeoff lever, so "needs an eval" is a next step rather than a dead end. Offer to build it with the user: - **Inputs**: a fixed set of ~20-30 real requests pulled from production logs or written by the user - enough for per-lever keep/revert decisions (the ~50-case bar above is for the final production cutover). Freeze them; every config runs the identical set. - **Judgment per output**: whichever is cheapest for the workload - golden answers to diff against, a short rubric the user scores each output on, or an automated checker (tests pass, JSON validates, required fields present). A model-graded judge is acceptable when nothing cheaper exists, but it is itself an approved API spend. - **Runner**: a script that runs the frozen inputs through one config, records each output plus `response.usage`, and reports pass rate and cost per task. Each config is one invocation; the sweep is a loop over configs. - **Cost and approval**: estimate it (inputs × configs × baseline cost per task) and get the user's go-ahead before running - this is real API spend under the standing approval rule. - Keep-or-revert is decided locally, on the eval evidence. Shadow-run the winning configuration on live traffic before cutover, keep the eval running after it, and confirm the savings in the usage and cost reports **after** cutover - only where the traffic is attributable (Step 1's shared-key caveat applies to the confirmation read too). - Expect most levers not to fit any given workload. On the cookbook's worked example, most didn't earn a place - tool schemas too small for tool search, loops too short for editing or compaction, no numeric work for code execution - and the levers that came closest on cost each gave back a correct answer. The profile from Step 1 exists so optimization isn't blind. - Plot configurations as score versus cost per task and take the Pareto frontier - that is what the cutover decision reads from. ## Workload shape -> lever Adapted from the cookbook's takeaways table, for mapping a profile to levers (row 1's watch-out is extended): | Where the cost is | Reach for | Skip it or watch out when | |---|---|---| | Same system prompt and tools re-billed on every call | Prompt caching with auto first, then an explicit breakpoint on the static prefix when many independent conversations share it or prefix layers change at different rates, and 1-hour TTL if calls are more than five minutes apart | Anything dynamic sits above the breakpoint - move that content into the user turn. And a cache that already reads well needs nothing: concurrent-batch misses (§ 2.5) aren't breakers, and a 1-hour TTL doesn't reach calls that are hours apart | | Large reference document in every prompt | Move it behind a tool or skill | Each call needs most of the document rather than a section, or the eval shows misses on cases that hinge on rules the model has to go looking for | | Many or heavy tool schemas | Tool search with `defer_loading` | Under roughly 10K schema tokens, where the search step is overhead | | Images, PDFs, or large files in context | Downscale images to what the task needs, and use the Files API plus code execution for tables and PDFs | There is nothing to extract or compute so the sandbox only adds tokens | | Unbounded user-supplied input | Token counting as an ingestion gate | | | Bulky results piling up across a long loop | Context editing or compaction server-side, or a client-side prune at natural boundaries | Loops are short or the cleared content is still needed, and note that every edit breaks the cache from that point | | One self-contained step with bulky intermediates | Subagent, optionally on a cheaper model | The deciding model needs that intermediate context to judge well | | Long visible responses | Specify the output shape with an example, with `max_tokens` as a backstop and a stop-sequence sentinel for early exits | | | Thinking and tool calls dominate, and the eval has headroom | Lower `effort` first, then drop a model tier and re-sweep effort | Always a direct capability trade, so step down one notch at a time against the eval | | Mostly routine cases with a few hard ones | Advisor tool on a cheaper driver | There is no cheap signal to gate the consult, leaving the driver to spot hard cases itself | | No one is waiting on the response | Batch API, flattening a tool loop into one request by pre-fetching its inputs if you have to | A user is waiting, or when flattening changes how the model reasons | ## Step 4: Deliverables 1. **The cost profile and plan**: the Step 0 assumptions (scope, quality bar, baseline), the Step 1 token profile, and the levers chosen with the measured expectation each one carries - plus the levers deliberately skipped and why, so the next person doesn't re-litigate them. Label the shortlist table as ranked by savings ceiling, not application order, so it can't be misread as the diff sequence. 2. **The changes**: one diff per lever so effects attribute - applied and measured (expected versus measured cost per task, pass rate held or not) where the user approved the runs; left as proposals carrying their expected savings and published quality cost where they didn't, or where a tradeoff lever still needs an eval. When nothing cleared the ranking floor, this deliverable is "no changes recommended" - a successful outcome; say it plainly rather than manufacturing a lever. **Report skeleton** (section order and required columns - keep the rest flexible): - **Scope / quality bar / baseline / platform** (Step 0 assumptions) - **Token profile** (Step 1) - **Ranked shortlist** - table columns: `Lever | Type (free win / tradeoff) | Savings ceiling | Data source (measured / usage logs / code estimate)`. Ceiling is in the unit tier the data supports (Step 1 -> Rank the levers). Caption the table "ranked by savings ceiling, not application order." - **Proposed changes** - one diff per lever, numbered in § 2 application order (free wins -> effort/budgets -> model), each tagged *applied and measured* / *proposed* / *needs an eval* - **Levers skipped** and why (including any dropped for platform availability) - **Next step / approvals needed** - measurement budget ask, eval prerequisite, or "no changes recommended" ## Sources and live references The measured results above come from two published Anthropic sources (and the Admin API facts in Step 1 from a third); fetch them when the user needs the full write-ups, charts, or current numbers: - The platform guide **Optimizing for cost and intelligence** - WebFetch the Cost Optimization URL in `shared/live-sources.md`. - The cookbook **Cost optimization on the Claude API** (`https://platform.claude.com/cookbook/cost-optimization-cost-optimization`) - a runnable end-to-end worked example of this workflow. - The **Usage and Cost Admin API** docs - the URL in `shared/live-sources.md`; the endpoint reference pages linked from that page carry the full parameter and response schemas. - Per-model prices: always the **Pricing** URL in `shared/live-sources.md`, never remembered rates.
# HTTP Error Codes Reference
This file documents HTTP error codes returned by the Claude API, their common causes, and how to handle them. For language-specific error handling examples, see the `python/` or `typescript/` folders.
## Error Code Summary
| Code | Error Type | Retryable | Common Cause |
| ---- | ----------------------- | --------- | ------------------------------------ |
| 400 | `invalid_request_error` | No | Invalid request format or parameters |
| 401 | `authentication_error` | No | Invalid or missing API key |
| 403 | `permission_error` | No | API key lacks permission |
| 404 | `not_found_error` | No | Invalid endpoint or model ID |
| 413 | `request_too_large` | No | Request exceeds size limits |
| 429 | `rate_limit_error` | Yes | Too many requests |
| 500 | `api_error` | Yes | Anthropic service issue |
| 529 | `overloaded_error` | Yes | API is temporarily overloaded |
## Detailed Error Information
### 400 Bad Request
**Causes:**
- Malformed JSON in request body
- Missing required parameters (`model`, `max_tokens`, `messages`)
- Invalid parameter types (e.g., string where integer expected)
- Empty messages array
- Messages not alternating user/assistant
**Example error:**
```json
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "messages: roles must alternate between \"user\" and \"assistant\""
},
"request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"
}
```
**Fix:** Validate request structure before sending. Check that:
- `model` is a valid model ID
- `max_tokens` is a positive integer
- `messages` array is non-empty and alternates correctly
---
### 401 Unauthorized
**Causes:**
- Missing `x-api-key` header or `Authorization` header
- Invalid API key format
- Revoked or deleted API key
- OAuth bearer token sent via `x-api-key` instead of `Authorization: Bearer`
- Both `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` set - the SDK sends both headers and the API rejects the request
**Fix:** Set `ANTHROPIC_API_KEY`, or run `ant auth login` and leave the client constructor empty. For raw HTTP with an OAuth token, use `Authorization: Bearer <token>` (not `x-api-key:`).
---
### 403 Forbidden
**Causes:**
- API key doesn't have access to the requested model
- Organization-level restrictions
- Attempting to access beta features without beta access
**Fix:** Check your API key permissions in the Console. You may need a different API key or to request access to specific features.
---
### 404 Not Found
**Causes:**
- Typo in model ID (e.g., `claude-sonnet-4.6` instead of `claude-sonnet-4-6`)
- Using deprecated model ID
- Invalid API endpoint
**Fix:** Use exact model IDs from the models documentation. You can use aliases (e.g., `claude-opus-5`).
---
### 413 Request Too Large
**Causes:**
- Request body exceeds maximum size
- Too many tokens in input
- Image data too large
**Fix:** Reduce input size - truncate conversation history, compress/resize images, or split large documents into chunks.
---
### 400 Validation Errors
Some 400 errors are specifically related to parameter validation:
- `max_tokens` exceeds model's limit
- Invalid `temperature` value (must be 0.0-1.0)
- `budget_tokens` >= `max_tokens` in extended thinking
- Invalid tool definition schema
**Model-specific 400s on Claude Opus 5 / Fable 5/5.1 / Opus 4.8 / 4.7:**
- `temperature`, `top_p`, `top_k` are removed - sending any of them returns 400. Delete the parameter; see `shared/model-migration.md` -> Per-SDK Syntax Reference.
- `thinking: {type: "enabled", budget_tokens: N}` is removed - sending it returns 400. Use `thinking: {type: "adaptive"}` instead.
- **Claude Opus 5:** `thinking: {type: "disabled"}` returns 400 when `effort` is `xhigh` or `max` - it is accepted at `high` or below. Thinking is on by default, so omitting the param runs adaptive rather than disabling it.
- **Fable 5/5.1 only:** an explicit `thinking: {type: "disabled"}` returns 400 at any effort (it is accepted on Opus 4.8/4.7). Omit the `thinking` param entirely instead.
- **Fable 5/5.1, Mythos 5/5.1:** if the organization or workspace is set to zero data retention (ZDR) - or any retention below the required 30 days - then **all** requests to these models return `400 invalid_request_error` ("In order to access this model, your organization or workspace must have data retention enabled."), even with a perfectly valid payload; ZDR only if expressly authorized by Anthropic. Check the retention configuration before debugging the request body.
- **Claude Fable 5.1 / Claude Mythos 5.1 (and Mythos Preview):** `tool_choice: {type: "any"}` or `{type: "tool", name: ...}` returns 400 `tool_choice: type "tool" and "any" are not supported for this model.` - also on `count_tokens` and Batches. Use `{type: "auto"}` plus a prompt instruction (`strict: true` for schema-valid arguments), or structured outputs.
- **Claude Fable 5.1 / Claude Mythos 5.1 - preserved thinking / history-editing check (new accounts created on/after 2026-08-31, or any request that sets `prefix_mismatch_behavior`):** ``messages.N.content.M: Invalid `signature` in `thinking` block. The block is bound to a different conversation. Remove the block, or set `thinking.block_binding.prefix_mismatch_behavior` to "drop_block".`` (plus a sentence naming the beta header when it wasn't sent, and optionally one naming the first message that changed) means the system prompt, tool list, or an earlier message changed since that thinking block was produced. Retrying the same body never clears it; `count_tokens` returns the same 400. (In the Message Batches API the *unset* default drops the failing blocks instead of failing the item - a Batches item fails as `errored` only with `prefix_mismatch_behavior: "error"` set.) Strip the named block and every thinking block after it and retry once, or resend with `thinking.block_binding.prefix_mismatch_behavior: "drop_block"` under beta `thinking-binding-controls-2026-08-01` (where the controls beta is offered - Claude API / Claude Platform on AWS at launch, per model on Bedrock and Google Cloud, not on Foundry: `shared/platform-availability.md`; elsewhere use the strip-and-retry path; without the header that field is a 400 ending `block_binding: Extra inputs are not permitted`); then fix the harness so it stops editing history (see `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5). The same leading clause with *no* "bound to a different conversation" sentence is a tampered signature - always a 400, regardless of the setting.
**Common mistake with extended thinking on older models (Opus 4.6 and earlier):**
```
# Wrong: budget_tokens must be < max_tokens
thinking: budget_tokens=10000, max_tokens=1000 -> Error!
# Correct
thinking: budget_tokens=10000, max_tokens=16000
```
---
### 429 Rate Limited
**Causes:**
- Exceeded requests per minute (RPM)
- Exceeded tokens per minute (TPM)
- Exceeded tokens per day (TPD)
**Headers to check:**
- `retry-after`: Seconds to wait before retrying
- `x-ratelimit-limit-*`: Your limits
- `x-ratelimit-remaining-*`: Remaining quota
**Fix:** The Anthropic SDKs automatically retry 429 and 5xx errors with exponential backoff (default: `max_retries=2`). For custom retry behavior, see the language-specific error handling examples.
---
### 500 Internal Server Error
**Causes:**
- Temporary Anthropic service issue
- Bug in API processing
**Fix:** Retry with exponential backoff. If persistent, check [status.anthropic.com](https://status.anthropic.com).
---
### 529 Overloaded
**Causes:**
- High API demand
- Service capacity reached
**Fix:** Retry with exponential backoff. Consider using a different model (Haiku is often less loaded), spreading requests over time, or implementing request queuing.
---
## Common Mistakes and Fixes
| Mistake | Error | Fix |
| ------------------------------- | ---------------- | ------------------------------------------------------- |
| `temperature`/`top_p`/`top_k` on Claude Opus 5 / Fable 5/5.1 / Opus 4.8 / 4.7 | 400 | Remove the parameter (see `shared/model-migration.md`) |
| `budget_tokens` on Claude Opus 5 / Fable 5/5.1 / Opus 4.8 / 4.7 | 400 | Use `thinking: {type: "adaptive"}` |
| `thinking: {type: "disabled"}` on Fable 5/5.1 | 400 | Omit the `thinking` param entirely (accepted on Opus 4.8/4.7) |
| Org set to ZDR / retention below 30 days (Fable 5/5.1, Mythos 5/5.1) | 400 on every request | Fix the org's data-retention configuration - the payload isn't the problem |
| `tool_choice` `any` / `tool` on Claude Fable 5.1 / Claude Mythos 5.1 / Mythos Preview | 400 | `{type: "auto"}` + name the tool in the prompt (`strict: true` for schema-valid args), or structured outputs |
| Edited history replayed with thinking blocks (Claude Fable 5.1 / Claude Mythos 5.1, preserved thinking) | 400 `Invalid signature in thinking block ... bound to a different conversation` | Stop editing history - keep the transcript append-only, using mid-conversation `role: "system"` / tool-change messages, turn-scoped `clear_at` reminders that are never deleted, server-side context editing, and summary-only compaction instead of edits; recover once by stripping the named block and every thinking block after it (text and tool calls stay), or `prefix_mismatch_behavior: "drop_block"` |
| `thinking.block_binding` without `thinking-binding-controls-2026-08-01` | 400 `block_binding: Extra inputs are not permitted` | Send the beta header where the controls beta is offered (`shared/platform-availability.md`); elsewhere remove `block_binding` and use strip-and-retry |
| `budget_tokens` >= `max_tokens` (older models) | 400 | Ensure `budget_tokens` < `max_tokens` |
| Typo in model ID | 404 | Use valid model ID like `claude-opus-5` |
| First message is `assistant` | 400 | First message must be `user` |
| Consecutive same-role messages | 400 | Alternate `user` and `assistant` |
| API key in code | 401 (leaked key) | Use environment variable |
| Custom retry needs | 429/5xx | SDK retries automatically; customize with `max_retries` |
## Typed Exceptions in SDKs
**Always use the SDK's typed exception classes** instead of checking error messages with string matching. Each HTTP status code maps to a specific exception class per SDK.
### Exception class names by language
| HTTP | Python (`anthropic.*`) / TypeScript (`Anthropic.*`) | Ruby (`Anthropic::Errors::*`) | Java (`com.anthropic.errors.*`) | C# | PHP (`Anthropic\Core\Exceptions\*`) |
|---|---|---|---|---|---|
| 400 | `BadRequestError` | `BadRequestError` | `BadRequestException` | `AnthropicBadRequestException` | `BadRequestException` |
| 401 | `AuthenticationError` | `AuthenticationError` | `UnauthorizedException` | `AnthropicUnauthorizedException` | `AuthenticationException` |
| 403 | `PermissionDeniedError` | `PermissionDeniedError` | `PermissionDeniedException` | `AnthropicForbiddenException` | `PermissionDeniedException` |
| 404 | `NotFoundError` | `NotFoundError` | `NotFoundException` | `AnthropicNotFoundException` | `NotFoundException` |
| 422 | `UnprocessableEntityError` | `UnprocessableEntityError` | `UnprocessableEntityException` | `AnthropicUnprocessableEntityException` | `UnprocessableEntityException` |
| 429 | `RateLimitError` | `RateLimitError` | `RateLimitException` | `AnthropicRateLimitException` | `RateLimitException` |
| >=500 | `InternalServerError` | `InternalServerError` | `InternalServerException` | `Anthropic5xxException` | `InternalServerException` |
| net | `APIConnectionError` | `APIConnectionError` | `AnthropicIoException` | `AnthropicIOException` | `APIConnectionException` |
| base | `APIError` (both); `APIStatusError` (Python only) | `APIStatusError` / `APIError` | `AnthropicServiceException` | `AnthropicApiException` | `APIStatusException` / `APIException` |
The Ruby and PHP classes live in a dedicated errors namespace - write `Anthropic::Errors::RateLimitError` and `Anthropic\Core\Exceptions\RateLimitException` (not bare `Anthropic::RateLimitError`). All 4xx C# exceptions also inherit from `Anthropic4xxException`.
### Catch most-specific first, in a chain
Order `catch`/`except`/`rescue` clauses from the most specific subclass to the base class, with a separate clause for each category you handle differently - retryable (429, >=500, network) vs. non-retryable (4xx). The SDK defines a distinct class per status for exactly this reason; a single broad catch-all discards that information.
```python
try:
msg = client.messages.create(...)
except anthropic.NotFoundError as e: # 404 - e.g. bad model ID
...
except anthropic.RateLimitError as e: # 429 - back off and retry
...
except anthropic.APIStatusError as e: # any other non-2xx HTTP response
print(e.status_code, e.message)
except anthropic.APIConnectionError as e: # network failure before a response
...
```
The same chain shape applies in every SDK: TypeScript `instanceof Anthropic.NotFoundError` -> `RateLimitError` -> `APIConnectionError` -> `APIError` (check `APIConnectionError` before `APIError` - in the TypeScript SDK it's a subclass of `APIError`, unlike Python where it's a sibling); Ruby `rescue Anthropic::Errors::NotFoundError` -> `...::RateLimitError` -> `...::APIStatusError`; Java `catch (NotFoundException) ... catch (RateLimitException) ... catch (AnthropicServiceException)`; C# `catch (AnthropicNotFoundException) ... catch (AnthropicRateLimitException) ... catch (AnthropicApiException)`; PHP `catch (NotFoundException) ... catch (RateLimitException) ... catch (APIStatusException)`.
### Go - `errors.As` then branch on status
The Go SDK returns a single `*anthropic.Error` for all non-2xx responses. Unwrap it with `errors.As`, then branch on `StatusCode`:
```go
_, err := client.Messages.New(ctx, params)
if err != nil {
var apierr *anthropic.Error
if errors.As(err, &apierr) {
switch apierr.StatusCode {
case 404:
// bad model ID / resource
case 429:
// back off and retry
default:
// other API error - apierr.StatusCode, apierr.RequestID
}
} else {
// transport-level error (*url.Error wrapping *net.OpError, etc.)
}
}
```
### Error `.type` Field
All `APIStatusError` subclasses now expose a `.type` property (Python: `.type`, TypeScript: `.type`, Java: `.errorType()`, Go: `.Type()`, Ruby: `.type`, PHP: `.type`) that returns the API error type string (e.g., `"invalid_request_error"`, `"authentication_error"`, `"rate_limit_error"`, `"overloaded_error"`). Use this for programmatic error classification when you need finer granularity than the HTTP status code - for example, distinguishing `"billing_error"` from `"permission_error"` (both map to 403).
```python
except anthropic.APIStatusError as e:
if e.type == "rate_limit_error":
# handle rate limiting
elif e.type == "overloaded_error":
# handle overload
```
# Live Documentation Sources
This file contains WebFetch URLs for fetching current information from platform.claude.com and Agent SDK repositories. Use these when users need the latest data that may have changed since the cached content was last updated.
## When to Use WebFetch
- User explicitly asks for "latest" or "current" information
- Cached data seems incorrect
- User asks about features not covered in cached content
- User needs specific API details or examples
## Claude API Documentation URLs
### Models & Pricing
| Topic | URL | Extraction Prompt |
| --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Models Overview | `https://platform.claude.com/docs/en/about-claude/models/overview.md` | "Extract current model IDs, context windows, and pricing for all Claude models" |
| Migration Guide | `https://platform.claude.com/docs/en/about-claude/models/migration-guide.md` | "Extract breaking changes, deprecated parameters, and per-model migration steps when moving to a newer Claude model" |
| Introducing Claude Fable 5 | `https://platform.claude.com/docs/en/about-claude/models/introducing-claude-fable-5.md` | "Extract capabilities, API changes, and availability stages for Claude Fable 5 and Claude Mythos 5" |
| Pricing | `https://platform.claude.com/docs/en/pricing.md` | "Extract current pricing per million tokens for input and output" |
| Cost Optimization | `https://platform.claude.com/docs/en/about-claude/models/optimizing-for-cost-and-intelligence.md` | "Extract measured cost levers, cache and batch savings, effort and model cost-per-task comparisons, budget controls, and multi-model guidance" |
### Core Features
| Topic | URL | Extraction Prompt |
| ----------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Extended Thinking | `https://platform.claude.com/docs/en/build-with-claude/extended-thinking.md` | "Extract extended thinking parameters, budget_tokens requirements, and usage examples" |
| Adaptive Thinking | `https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking.md` | "Extract adaptive thinking setup, effort levels, and Claude Opus 5 usage examples" |
| Effort Parameter | `https://platform.claude.com/docs/en/build-with-claude/effort.md` | "Extract effort levels, cost-quality tradeoffs, and interaction with thinking" |
| Tool Use | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview.md` | "Extract tool definition schema, tool_choice options, and handling tool results" |
| Streaming | `https://platform.claude.com/docs/en/build-with-claude/streaming.md` | "Extract streaming event types, SDK examples, and best practices" |
| Prompt Caching | `https://platform.claude.com/docs/en/build-with-claude/prompt-caching.md` | "Extract cache_control usage, pricing benefits, and implementation examples" |
### Media & Files
| Topic | URL | Extraction Prompt |
| ----------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Vision | `https://platform.claude.com/docs/en/build-with-claude/vision.md` | "Extract supported image formats, size limits, and code examples" |
| PDF Support | `https://platform.claude.com/docs/en/build-with-claude/pdf-support.md` | "Extract PDF handling capabilities, limits, and examples" |
### API Operations
| Topic | URL | Extraction Prompt |
| ---------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Batch Processing | `https://platform.claude.com/docs/en/build-with-claude/batch-processing.md` | "Extract batch API endpoints, request format, and polling for results" |
| Files API | `https://platform.claude.com/docs/en/build-with-claude/files.md` | "Extract file upload, download, referencing in messages, supported types, and the migration steps from files-api-2025-04-14" |
| Token Counting | `https://platform.claude.com/docs/en/build-with-claude/token-counting.md` | "Extract token counting API usage and examples" |
| Rate Limits | `https://platform.claude.com/docs/en/api/rate-limits.md` | "Extract current rate limits by tier and model" |
| Usage and Cost Admin API | `https://platform.claude.com/docs/en/manage-claude/usage-cost-api.md` | "Extract the usage_report and cost_report endpoints, Admin API key requirements, filter and group_by dimensions, token fields, and granularity limits" |
| Errors | `https://platform.claude.com/docs/en/api/errors.md` | "Extract HTTP error codes, meanings, and retry guidance" |
| Amazon Bedrock | `https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock.md` | "Extract the AnthropicBedrockMantle client per language, `anthropic.`-prefixed model IDs, auth paths, feature availability, and regions" |
| Claude Platform on AWS | `https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws.md` | "Extract the AnthropicAWS client per language, SigV4 auth, credential precedence, short-term API keys, workspace_id, and region requirements" |
| Claude Platform on AWS - IAM actions | `https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions.md` | "Extract the IAM action names, resource ARNs, and policy examples required for each API capability" |
### Admin API (Organization Management)
| Topic | URL | Extraction Prompt |
| -------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Admin API Guide | `https://platform.claude.com/docs/en/manage-claude/admin-api.md` | "Extract Admin API authentication, SDK/CLI usage, and member/invite/key management" |
| Admin API Reference | `https://platform.claude.com/docs/en/api/admin.md` | "Extract endpoint parameters, responses, and pagination for the Admin API" |
| Workspaces | `https://platform.claude.com/docs/en/manage-claude/workspaces.md` | "Extract workspace create/list/archive and member management via API" |
| Rate Limits API | `https://platform.claude.com/docs/en/manage-claude/rate-limits-api.md` | "Extract org and workspace rate limit report endpoints and filters" |
| WIF Admin | `https://platform.claude.com/docs/en/manage-claude/wif-admin-api.md` | "Extract service account, federation issuer, and federation rule management" |
| Usage & Cost Reports | `https://platform.claude.com/docs/en/manage-claude/usage-cost-api.md` | "Extract usage and cost report endpoints (curl-only, not in the SDKs)" |
### Tools
| Topic | URL | Extraction Prompt |
| -------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Code Execution | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool.md` | "Extract code execution tool setup, file upload, container reuse, and response handling" |
| Computer Use | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use.md` | "Extract computer use tool setup, capabilities, and implementation examples" |
| Bash Tool | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool.md` | "Extract bash tool schema, reference implementation, and security considerations" |
| Text Editor | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool.md` | "Extract text editor tool commands, schema, and reference implementation" |
| Memory Tool | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool.md` | "Extract memory tool commands, directory structure, and implementation patterns" |
| Tool Search | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool.md` | "Extract tool search setup, when to use, and cache interaction" |
| Programmatic Tool Calling | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling.md` | "Extract PTC setup, script execution model, and tool invocation from code" |
| Skills | `https://platform.claude.com/docs/en/agents-and-tools/skills.md` | "Extract skill folder structure, SKILL.md format, and loading behavior" |
| Skills Guide | `https://platform.claude.com/docs/en/build-with-claude/skills-guide.md` | "Extract the Skills API (/v1/skills) usage and the migration steps from skills-2025-10-02" |
### Advanced Features
| Topic | URL | Extraction Prompt |
| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------- |
| Structured Outputs | `https://platform.claude.com/docs/en/build-with-claude/structured-outputs.md` | "Extract output_config.format usage and schema enforcement" |
| Compaction | `https://platform.claude.com/docs/en/build-with-claude/compaction.md` | "Extract compaction setup, trigger config, and streaming with compaction" |
| Context Editing | `https://platform.claude.com/docs/en/build-with-claude/context-editing.md` | "Extract context editing thresholds, what gets cleared, and configuration" |
| Citations | `https://platform.claude.com/docs/en/build-with-claude/citations.md` | "Extract citation format and implementation" |
| Context Windows | `https://platform.claude.com/docs/en/build-with-claude/context-windows.md` | "Extract context window sizes and token management" |
### Managed Agents
Use these when a managed-agents binding, behavior, or wire-level detail isn't covered in the cached `shared/managed-agents-*.md` concept files or in `{lang}/managed-agents/README.md`.
| Topic | URL | Extraction Prompt |
| --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Overview | `https://platform.claude.com/docs/en/managed-agents/overview.md` | "Extract the high-level architecture and how agents/sessions/environments/vaults fit together" |
| Quickstart | `https://platform.claude.com/docs/en/managed-agents/quickstart.md` | "Extract the minimal end-to-end agent -> environment -> session -> stream code path" |
| Agent Setup | `https://platform.claude.com/docs/en/managed-agents/agent-setup.md` | "Extract agent create/update/list-versions/archive lifecycle and parameters" |
| Define Outcomes | `https://platform.claude.com/docs/en/managed-agents/define-outcomes.md` | "Extract outcome definitions, evaluation hooks, and success criteria configuration" |
| Sessions | `https://platform.claude.com/docs/en/managed-agents/sessions.md` | "Extract session lifecycle, status transitions, idle/terminated semantics, and resume rules" |
| Environments | `https://platform.claude.com/docs/en/managed-agents/environments.md` | "Extract environment config (cloud/networking), management endpoints, and reuse model" |
| Self-Hosted Sandboxes | `https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes.md` | "Extract config:{type:self_hosted}, ANTHROPIC_ENVIRONMENT_KEY, EnvironmentWorker.run/handle_item, environments.work.poller(drain), beta_agent_toolset, ant beta:worker poll/run, webhook-driven wake, memory stores (ANTHROPIC_WORK_SECRET, memory_sync_interval/memory_sync_deletes)" |
| Self-Hosted Sandboxes - Security | `https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security.md` | "Extract what the customer owns (hardening, egress, key custody, trust boundaries) vs what Anthropic cannot do" |
| Events and Streaming | `https://platform.claude.com/docs/en/managed-agents/events-and-streaming.md` | "Extract event stream types, stream-first ordering, reconnect/dedupe, and steering patterns" |
| Tools | `https://platform.claude.com/docs/en/managed-agents/tools.md` | "Extract built-in toolset, custom tool definitions, and tool result wire format" |
| Files | `https://platform.claude.com/docs/en/managed-agents/files.md` | "Extract file upload, mount paths, session resources, and listing/downloading session outputs" |
| Permission Policies | `https://platform.claude.com/docs/en/managed-agents/permission-policies.md` | "Extract permission policy types (allow/deny/confirm) and per-tool config" |
| Multi-Agent | `https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration.md` | "Extract multi-agent composition patterns, sub-agent invocation, and result handoff" |
| Observability | `https://platform.claude.com/docs/en/managed-agents/observability.md` | "Extract logging, tracing, and usage telemetry exposed by managed agents" |
| Webhooks | `https://platform.claude.com/docs/en/managed-agents/webhooks.md` | "Extract webhook endpoint registration, HMAC signature verification, supported event types, and delivery semantics" |
| GitHub | `https://platform.claude.com/docs/en/managed-agents/github.md` | "Extract github_repository resource shape, multi-repo mounting, and token rotation" |
| MCP Connector | `https://platform.claude.com/docs/en/managed-agents/mcp-connector.md` | "Extract MCP server declaration on agents and vault-based credential injection at session" |
| Vaults | `https://platform.claude.com/docs/en/managed-agents/vaults.md` | "Extract vault create, credential add/rotate, OAuth refresh shape, and archive" |
| Skills | `https://platform.claude.com/docs/en/managed-agents/skills.md` | "Extract skill packaging and loading model for managed agents" |
| Memory | `https://platform.claude.com/docs/en/managed-agents/memory.md` | "Extract memory resource shape, scoping, and lifecycle" |
| Onboarding | `https://platform.claude.com/docs/en/managed-agents/onboarding.md` | "Extract first-run setup, prerequisites, and account/region requirements" |
| Cloud Containers | `https://platform.claude.com/docs/en/managed-agents/cloud-containers.md` | "Extract cloud container runtime, image config, and network/storage knobs" |
| Migration | `https://platform.claude.com/docs/en/managed-agents/migration.md` | "Extract migration paths from earlier APIs/preview shapes to GA managed agents" |
### Anthropic CLI
The `ant` CLI provides terminal access to the Claude API. Every API resource is exposed as a subcommand. It is the recommended way to create agents and environments from version-controlled YAML (`ant beta:agents create < agent.yaml` - see `shared/anthropic-cli.md`), and also exposes sessions and every other API resource for scripting and interactive inspection.
| Topic | URL | Extraction Prompt |
| ------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Anthropic CLI | `https://platform.claude.com/docs/en/api/sdks/cli.md` | "Extract CLI install, authentication, command structure, and the beta:agents/environments/sessions commands" |
| Authentication overview | `https://platform.claude.com/docs/en/manage-claude/authentication.md` | "Extract the credential options (API keys, interactive OAuth login, Workload Identity Federation) and when to use each" |
| WIF reference | `https://platform.claude.com/docs/en/manage-claude/wif-reference.md` | "Extract credential precedence order, the profile configuration file schema, and the configuration directory layout" |
---
## Claude API SDK Repositories
WebFetch these when a binding (class, method, namespace, field) isn't covered in the cached `{lang}/` skill files or in the managed-agents docs above. The SDKs include beta managed-agents support for `/v1/agents`, `/v1/sessions`, `/v1/environments`, and related resources - search the repo for `BetaManagedAgents`, `beta.agents`, `beta.sessions`, or the equivalent namespace for that language.
| SDK | URL | Extraction Prompt |
| ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Python | `https://github.com/anthropics/anthropic-sdk-python` | "Extract beta managed-agents namespaces, classes, and method signatures (`client.beta.agents`, `client.beta.sessions`)" |
| TypeScript | `https://github.com/anthropics/anthropic-sdk-typescript` | "Extract beta managed-agents namespaces, classes, and method signatures (`client.beta.agents`, `client.beta.sessions`)" |
| Java | `https://github.com/anthropics/anthropic-sdk-java` | "Extract beta managed-agents classes, builders, and method signatures (`client.beta().agents()`, `BetaManagedAgents*`)" |
| Go | `https://github.com/anthropics/anthropic-sdk-go` | "Extract beta managed-agents types and method signatures (`client.Beta.Agents`, `BetaManagedAgents*` event types)" |
| Ruby | `https://github.com/anthropics/anthropic-sdk-ruby` | "Extract beta managed-agents methods and parameter shapes (`client.beta.agents`, `client.beta.sessions`)" |
| C# | `https://github.com/anthropics/anthropic-sdk-csharp` | "Extract beta managed-agents classes and method signatures (NuGet package, `BetaManagedAgents*` types)" |
| PHP | `https://github.com/anthropics/anthropic-sdk-php` | "Extract beta managed-agents classes and method signatures (`$client->beta->agents`, `BetaManagedAgents*` params)" |
Each SDK repo also ships runnable programs under `examples/` - including the refusal-fallback / `fallbacks` examples (client-side middleware registration, fallback state, server-side `fallbacks` param). Fetch those for exact per-language syntax instead of translating another language's example.
### SDK major-version upgrade guides
Authoritative change lists for upgrading the SDK package itself across a major version. The bundled `{lang}/claude-api/sdk-upgrade.md` is the executable form; when the two disagree, the repository guide wins.
| SDK | URL | Extraction Prompt |
| ------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Python (0.x -> 1.x) | `https://github.com/anthropics/anthropic-sdk-python/blob/main/MIGRATION.md` | "Extract every breaking change with its before/after code, the new minimum Python version, and the upgrade command" |
---
## Fallback Strategy
If WebFetch fails (network issues, URL changed):
1. Use cached content from the language-specific files (note the cache date)
2. Inform user the data may be outdated
3. Suggest they check platform.claude.com or the GitHub repos directly
# Managed Agents - Endpoint Reference
All endpoints require `x-api-key` and `anthropic-version: 2023-06-01` headers. Managed Agents endpoints additionally require the `anthropic-beta` header.
> Most users should define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md`. The endpoints below are the underlying API that the CLI and SDKs drive.
## Beta Headers
```
anthropic-beta: managed-agents-2026-04-01
```
The SDK adds this header automatically for all `client.beta.{agents,environments,sessions,vaults,memory_stores,deployments,deployment_runs}.*` calls. Skills endpoints use `skills-2025-10-02`; Files endpoints use `files-api-2025-04-14`.
---
## SDK Method Reference
All resources are under the `beta` namespace. Python and TypeScript share identical method names.
| Resource | Python / TypeScript (`client.beta.*`) | Go (`client.Beta.*`) |
| --- | --- | --- |
| Agents | `agents.create` / `retrieve` / `update` / `list` / `archive` | `Agents.New` / `Get` / `Update` / `List` / `Archive` |
| Agent Versions | `agents.versions.list` | `Agents.Versions.List` |
| Environments | `environments.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `Environments.New` / `Get` / `Update` / `List` / `Delete` / `Archive` |
| Environment Work (self-hosted) | `environments.work.poller` / `stats` / `stop` | See `shared/managed-agents-self-hosted-sandboxes.md` |
| Sessions | `sessions.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `Sessions.New` / `Get` / `Update` / `List` / `Delete` / `Archive` |
| Session Events | `sessions.events.list` / `send` / `stream` | `Sessions.Events.List` / `Send` / `StreamEvents` |
| Session Threads | `sessions.threads.list` / `retrieve` / `archive`; `sessions.threads.events.list` / `stream` | `Sessions.Threads.List` / `Get` / `Archive`; `Sessions.Threads.Events.List` / `StreamEvents` |
| Session Resources | `sessions.resources.add` / `retrieve` / `update` / `list` / `delete` | `Sessions.Resources.Add` / `Get` / `Update` / `List` / `Delete` |
| Deployments | `deployments.create` / `update` / `pause` / `unpause` / `archive` / `run` | Not yet documented - WebFetch the SDK repo (`shared/live-sources.md`) |
| Deployment Runs | `deployment_runs.list` / `retrieve` (TS: `deploymentRuns.*`) | Not yet documented - WebFetch the SDK repo (`shared/live-sources.md`) |
| Vaults | `vaults.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `Vaults.New` / `Get` / `Update` / `List` / `Delete` / `Archive` |
| Credentials | `vaults.credentials.create` / `retrieve` / `update` / `list` / `delete` / `archive` / `mcp_oauth_validate` | `Vaults.Credentials.New` / `Get` / `Update` / `List` / `Delete` / `Archive` / `McpOauthValidate` |
| Memory Stores | `memory_stores.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `MemoryStores.New` / `Get` / `Update` / `List` / `Delete` / `Archive` |
| Memories | `memory_stores.memories.create` / `retrieve` / `update` / `list` / `delete` | `MemoryStores.Memories.New` / `Get` / `Update` / `List` / `Delete` |
| Memory Versions | `memory_stores.memory_versions.list` / `retrieve` / `redact` | `MemoryStores.MemoryVersions.List` / `Get` / `Redact` |
**Naming quirks to watch for:**
- Agents and Session Threads have **no delete** - only `archive`. Archive is **permanent**: the agent becomes read-only, new sessions cannot reference it, and there is no unarchive. Confirm with the user before archiving a production agent. Environments, Sessions, Vaults, Credentials, and Memory Stores have both `delete` and `archive`; Session Resources, Files, Skills, and Memories are `delete`-only; Memory Versions have neither - only `redact`.
- Session resources use `add` (not `create`).
- Go's event stream is `StreamEvents` (not `Stream`).
- The self-hosted worker class is `EnvironmentWorker` from `anthropic.lib.environments` / `@anthropic-ai/sdk/helpers/beta/environments` / `anthropic-sdk-go/lib/environments`; `client.beta.environments.work.worker(...)` is a factory that returns the same class, alongside the `environments.work.poller/stats/stop` client methods.
**Agent shorthand:** `agent` on session create accepts three forms - a bare string (`agent="agent_abc123"`, latest version), a pinned reference `{type: "agent", id, version}`, or `{type: "agent_with_overrides", id, version?, model?, system?, tools?, mcp_servers?, skills?}` to override those fields for this session only (see `shared/managed-agents-core.md` -> Override agent configuration for a session).
**Model shorthand:** `model` on agent create accepts either a bare string (`model="claude-opus-5"` - uses `standard` speed) or the full config object, which takes `speed`, `effort`, and `inference_geo` alongside `id`: `{id: "claude-opus-5", speed: "fast"}`, `{id: "claude-opus-5", effort: "high"}`, `{id: "claude-opus-5", inference_geo: "us"}`. `effort` accepts a level string (`low`/`medium`/`high`/`xhigh`/`max`) or `{type: "<level>"}`, and is **agent-configuration only** - an `effort` inside a per-session `model` override is ignored. `inference_geo` (`"us"` | `"global"`) pins the geography serving the agent's model requests, and unlike `effort` **is** applied in a per-session `model` override. See `shared/managed-agents-core.md` -> Effort on the agent model / Pinning inference geography. Note: `speed: "fast"` is supported on Claude Opus 5 and Opus 4.8 - on the Claude API only, which includes Managed Agents but not Amazon Bedrock, Google Cloud, or Microsoft Foundry. Opus 4.7 fast mode has been removed; `speed: "fast"` on Opus 4.7 returns an error.
---
## Agents
**Step one of every flow.** Sessions require a pre-created agent - there is no inline agent config under `managed-agents-2026-04-01`.
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `GET` | `/v1/agents` | ListAgents | List agents |
| `POST` | `/v1/agents` | CreateAgent | Create a saved agent configuration |
| `GET` | `/v1/agents/{agent_id}` | GetAgent | Get agent details |
| `POST` | `/v1/agents/{agent_id}` | UpdateAgent | Update agent configuration. `version` is **optional**: supply it (>= 1) for optimistic concurrency - a mismatch returns 409 - or omit it for an unconditional last-write-wins update. |
| `POST` | `/v1/agents/{agent_id}/archive` | ArchiveAgent | Archive an agent. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive - this is the terminal state. |
| `GET` | `/v1/agents/{agent_id}/versions` | ListAgentVersions | List agent versions |
## Sessions
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `GET` | `/v1/sessions` | ListSessions | List sessions (paginated) |
| `POST` | `/v1/sessions` | CreateSession | Create a new session |
| `GET` | `/v1/sessions/{session_id}` | GetSession | Get session details |
| `POST` | `/v1/sessions/{session_id}` | UpdateSession | Update session `metadata`/`title`, `agent.tools`/`agent.mcp_servers` (session-local override; session must be `idle`), or `budget` - change the cap (higher or lower; the new value must exceed the consumed list cost) or remove it with `null`; removal is one-way, and a budget can never be added post-create. `vault_ids` is create-only (rejected on update). See `shared/managed-agents-core.md` -> Updating the agent configuration mid-session / Session budgets. |
| `DELETE` | `/v1/sessions/{session_id}` | DeleteSession | Delete a session |
| `POST` | `/v1/sessions/{session_id}/archive` | ArchiveSession | Archive a session |
## Events
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `GET` | `/v1/sessions/{session_id}/events` | ListEvents | List events (polling, paginated) |
| `POST` | `/v1/sessions/{session_id}/events` | SendEvents | Send events (user message, tool result) |
| `GET` | `/v1/sessions/{session_id}/events/stream` | StreamEvents | Stream events via SSE. Optional `event_deltas[]=agent.message` / `agent.thinking` opts in to live-preview `event_start`/`event_delta` events - see `shared/managed-agents-events.md` § Live previews. |
## Session Threads
Per-subagent event streams in multiagent sessions. See `shared/managed-agents-multiagent.md`.
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `GET` | `/v1/sessions/{session_id}/threads` | ListThreads | List threads (paginated) |
| `GET` | `/v1/sessions/{session_id}/threads/{thread_id}` | GetThread | Retrieve one thread (carries `agent` snapshot, `status`, `parent_thread_id`, `stats`, `usage`) |
| `POST` | `/v1/sessions/{session_id}/threads/{thread_id}/archive` | ArchiveThread | Archive a thread |
| `GET` | `/v1/sessions/{session_id}/threads/{thread_id}/events` | ListThreadEvents | List past events for one thread (paginated) |
| `GET` | `/v1/sessions/{session_id}/threads/{thread_id}/stream` | StreamThreadEvents | Stream one thread via SSE (SDK: `threads.events.stream`) |
## Session Resources
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------------- | ---------------- | ---------------------------------------- |
| `GET` | `/v1/sessions/{session_id}/resources` | ListResources | List resources attached to session |
| `POST` | `/v1/sessions/{session_id}/resources` | AddResource | Attach `file` or `github_repository` resource (SDK method: `add`, not `create`). `memory_store` resources attach at session-create time only. Self-hosted environments accept **only** `memory_store` (at create); `file` / `github_repository` are rejected there. |
| `GET` | `/v1/sessions/{session_id}/resources/{resource_id}` | GetResource | Get a single resource |
| `POST` | `/v1/sessions/{session_id}/resources/{resource_id}` | UpdateResource | Update resource |
| `DELETE` | `/v1/sessions/{session_id}/resources/{resource_id}` | DeleteResource | Remove resource from session |
## Environments
| Method | Path | Operation | Description |
| -------- | ---------------------------------------------------------------- | -------------------- | ----------------------------------- |
| `POST` | `/v1/environments` | CreateEnvironment | Create environment |
| `GET` | `/v1/environments` | ListEnvironments | List environments |
| `GET` | `/v1/environments/{environment_id}` | GetEnvironment | Get environment details |
| `POST` | `/v1/environments/{environment_id}` | UpdateEnvironment | Update environment |
| `DELETE` | `/v1/environments/{environment_id}` | DeleteEnvironment | Delete environment. Returns 204. |
| `POST` | `/v1/environments/{environment_id}/archive` | ArchiveEnvironment | Archive environment. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive - this is the terminal state. |
| `GET` | `/v1/environments/{environment_id}/work/stats` | WorkQueueStats | Self-hosted work-queue depth/pending/workers. `x-api-key` auth. See `shared/managed-agents-self-hosted-sandboxes.md`. |
| `POST` | `/v1/environments/{environment_id}/work/{work_id}/stop` | StopWork | Self-hosted: stop a claimed work item. `x-api-key` auth. |
For `type: "self_hosted"`, `config` is the bare `{"type": "self_hosted"}` - `networking` and `packages` do not apply. (`networking` never governs `web_search` / `web_fetch` in either type - those are restricted per-tool with `allowed_domains` / `blocked_domains` in the agent toolset; see `shared/managed-agents-tools.md`.)
## Deployments
Scheduled deployments (`depl_` IDs) run an agent on a recurring cron schedule - each firing creates a session. See `shared/managed-agents-scheduled-deployments.md` for the conceptual guide (cron/DST semantics, failure behavior, lifecycle).
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `POST` | `/v1/deployments` | CreateDeployment | Create a scheduled deployment |
| `POST` | `/v1/deployments/{deployment_id}` | UpdateDeployment | Update deployment configuration (see `shared/managed-agents-scheduled-deployments.md`) |
| `POST` | `/v1/deployments/{deployment_id}/pause` | PauseDeployment | Suppress scheduled triggers (reversible; manual runs still allowed) |
| `POST` | `/v1/deployments/{deployment_id}/unpause` | UnpauseDeployment | Resume from the next occurrence (no backfill) |
| `POST` | `/v1/deployments/{deployment_id}/archive` | ArchiveDeployment | **Terminal** - schedule stops, deployment becomes immutable |
| `POST` | `/v1/deployments/{deployment_id}/run` | RunDeployment | Trigger a manual run immediately (`trigger_context.type: "manual"`); works while paused |
## Deployment Runs
Each trigger attempt (scheduled or manual) writes a `deployment_run` record (`drun_` IDs) carrying either the created `session_id` or an `error.type` (`environment_archived`, `agent_archived`, `vault_not_found`, `session_rate_limited`, `service_unavailable`).
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `GET` | `/v1/deployment_runs?deployment_id=...` | ListDeploymentRuns | List runs for a deployment (paginated; filter failures with `has_error=true`) |
| `GET` | `/v1/deployment_runs/{deployment_run_id}` | GetDeploymentRun | Retrieve a single run by ID (a `deployment_run.*` webhook event carries this as `data.id`) |
## Vaults
Vaults store credentials that Anthropic manages on your behalf - MCP credentials (OAuth with auto-refresh, or static bearer tokens) and `environment_variable` credentials substituted into outbound requests at egress. Attach to sessions via `vault_ids`. See `managed-agents-tools.md` §Vaults for the conceptual guide and credential shapes.
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `POST` | `/v1/vaults` | CreateVault | Create a vault |
| `GET` | `/v1/vaults` | ListVaults | List vaults |
| `GET` | `/v1/vaults/{vault_id}` | GetVault | Get vault details |
| `POST` | `/v1/vaults/{vault_id}` | UpdateVault | Update vault |
| `DELETE` | `/v1/vaults/{vault_id}` | DeleteVault | Delete vault |
| `POST` | `/v1/vaults/{vault_id}/archive` | ArchiveVault | Archive vault |
## Credentials
Credentials are individual secrets stored inside a vault.
| Method | Path | Operation | Description |
| -------- | ----------------------------------------------------------------- | ------------------ | ---------------------------- |
| `POST` | `/v1/vaults/{vault_id}/credentials` | CreateCredential | Create a credential |
| `GET` | `/v1/vaults/{vault_id}/credentials` | ListCredentials | List credentials in vault |
| `GET` | `/v1/vaults/{vault_id}/credentials/{credential_id}` | GetCredential | Get credential metadata |
| `POST` | `/v1/vaults/{vault_id}/credentials/{credential_id}` | UpdateCredential | Update credential |
| `DELETE` | `/v1/vaults/{vault_id}/credentials/{credential_id}` | DeleteCredential | Delete credential |
| `POST` | `/v1/vaults/{vault_id}/credentials/{credential_id}/archive` | ArchiveCredential | Archive credential |
| `POST` | `/v1/vaults/{vault_id}/credentials/{credential_id}/mcp_oauth_validate` | McpOauthValidate | Validate an MCP OAuth credential |
## Memory Stores
Workspace-scoped persistent memory that survives across sessions. Attach to a session via a `{"type": "memory_store", "memory_store_id": ...}` entry in `resources[]` (session-create time only). See `shared/managed-agents-memory.md` for the conceptual guide, the FUSE-mount agent interface, preconditions, and versioning.
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ------------------ | ---------------------------------------- |
| `POST` | `/v1/memory_stores` | CreateMemoryStore | Create a store (`name`, `description`, `metadata`) |
| `GET` | `/v1/memory_stores` | ListMemoryStores | List stores (`include_archived`, `created_at_{gte,lte}`) |
| `GET` | `/v1/memory_stores/{memory_store_id}` | GetMemoryStore | Get store details |
| `POST` | `/v1/memory_stores/{memory_store_id}` | UpdateMemoryStore | Update store |
| `DELETE` | `/v1/memory_stores/{memory_store_id}` | DeleteMemoryStore | Delete store |
| `POST` | `/v1/memory_stores/{memory_store_id}/archive` | ArchiveMemoryStore | Archive store. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive. |
## Memories
Individual text documents inside a store (<= 100KB each). `create` creates at a `path` and returns `409` (`memory_path_conflict_error`, with `conflicting_memory_id`) if the path is occupied; `update` mutates by `mem_...` ID (rename and/or content). Only `update` accepts a `precondition` (`{"type": "content_sha256", "content_sha256": ...}`) - on mismatch returns `409` (`memory_precondition_failed_error`). List endpoints accept `view: "basic"|"full"` (controls whether `content` is populated; `retrieve` defaults to `full`).
| Method | Path | Operation | Description |
| -------- | ----------------------------------------------------------------- | -------------- | ---------------------------------------- |
| `GET` | `/v1/memory_stores/{memory_store_id}/memories` | ListMemories | Returns `Memory \| MemoryPrefix`; filter by `path_prefix`, `depth` |
| `POST` | `/v1/memory_stores/{memory_store_id}/memories` | CreateMemory | Create at `path` (SDK: `memories.create`); `409 memory_path_conflict_error` if occupied |
| `GET` | `/v1/memory_stores/{memory_store_id}/memories/{memory_id}` | GetMemory | Read one memory (defaults to `view="full"`) |
| `PATCH` | `/v1/memory_stores/{memory_store_id}/memories/{memory_id}` | UpdateMemory | Change `content`, `path`, or both by ID; optional `precondition` |
| `DELETE` | `/v1/memory_stores/{memory_store_id}/memories/{memory_id}` | DeleteMemory | Delete (optional `expected_content_sha256`) |
## Memory Versions
Immutable per-mutation snapshots (`memver_...`) - the audit and rollback surface. `operation` in `created` / `modified` / `deleted`.
| Method | Path | Operation | Description |
| -------- | ----------------------------------------------------------------------------- | --------------------- | ---------------------------------------- |
| `GET` | `/v1/memory_stores/{memory_store_id}/memory_versions` | ListMemoryVersions | Newest-first; filter by `memory_id`, `operation`, `session_id`, `api_key_id`, `created_at_{gte,lte}` |
| `GET` | `/v1/memory_stores/{memory_store_id}/memory_versions/{version_id}` | GetMemoryVersion | List fields + full `content` |
| `POST` | `/v1/memory_stores/{memory_store_id}/memory_versions/{version_id}/redact` | RedactMemoryVersion | Clear `content`/`content_sha256`/`content_size_bytes`/`path`; preserve actor + timestamps |
## Files
| Method | Path | Operation | Description |
| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `POST` | `/v1/files` | UploadFile | Upload a file |
| `GET` | `/v1/files` | ListFiles | List files |
| `GET` | `/v1/files/{file_id}` | GetFile | Get file metadata (SDK method: `retrieve_metadata`) |
| `GET` | `/v1/files/{file_id}/content` | DownloadFile | Download file content |
| `DELETE` | `/v1/files/{file_id}` | DeleteFile | Delete a file |
## Skills
| Method | Path | Operation | Description |
| -------- | --------------------------------------------------------------- | ------------------ | ---------------------------- |
| `POST` | `/v1/skills` | CreateSkill | Create a skill |
| `GET` | `/v1/skills` | ListSkills | List skills |
| `GET` | `/v1/skills/{skill_id}` | GetSkill | Get skill details |
| `DELETE` | `/v1/skills/{skill_id}` | DeleteSkill | Delete a skill |
| `POST` | `/v1/skills/{skill_id}/versions` | CreateVersion | Create skill version |
| `GET` | `/v1/skills/{skill_id}/versions` | ListVersions | List skill versions |
| `GET` | `/v1/skills/{skill_id}/versions/{version}` | GetVersion | Get skill version |
| `DELETE` | `/v1/skills/{skill_id}/versions/{version}` | DeleteVersion | Delete skill version |
---
## Request/Response Schema Quick Reference
### CreateAgent Request Body
**Always start here.** `model`, `system`, `tools`, `mcp_servers`, `skills` are top-level fields on this object - they do NOT go on the session.
```json
{
"name": "string (required, 1-256 chars)",
"model": "claude-opus-5 (required - bare string, or {id, speed?, effort?, inference_geo?} object)",
"description": "string (optional, up to 2048 chars)",
"system": "string (optional, up to 100,000 chars)",
"tools": [
{ "type": "agent_toolset_20260401" }
],
"skills": [
{ "type": "anthropic", "skill_id": "xlsx" },
{ "type": "custom", "skill_id": "skill_abc123", "version": "1" }
],
"mcp_servers": [
{
"type": "url",
"name": "github",
"url": "https://api.githubcopilot.com/mcp/"
}
],
"multiagent": {
"type": "coordinator",
"agents": [
"agent_abc123",
{ "type": "agent", "id": "agent_def456", "version": 4 },
{ "type": "self" }
]
},
"metadata": {
"key": "value (max 16 pairs, keys <=64 chars, values <=512 chars)"
}
}
```
> Limits: `tools` max 128, `skills` max 20, `mcp_servers` max 20 (unique names). `multiagent.agents` 1-20 entries (string ID | `{type:"agent",id,version?}` | `{type:"self"}` | `{type:"advisor",model}`, at most one advisor) - see `shared/managed-agents-multiagent.md`.
### CreateSession Request Body
```json
{
"agent": "agent_abc123 (required - string shorthand for latest version, or {type: \"agent\", id, version} object)",
"environment_id": "env_abc123 (required)",
"title": "string (optional)",
"resources": [
{
"type": "github_repository",
"url": "https://github.com/owner/repo (required)",
"authorization_token": "ghp_... (required)",
"mount_path": "/workspace/repo (optional - defaults to /workspace/<repo-name>)",
"checkout": { "type": "branch", "name": "main" }
}
],
"initial_events": [
{ "type": "user.message", "content": [{ "type": "text", "text": "Review the auth module." }] }
],
"vault_ids": ["vlt_abc123 (optional - vault credentials: MCP auth + environment variables)"],
"budget": {
"type": "limit",
"max_list_cost": { "amount": "2500", "currency": "USD" }
},
"metadata": {
"key": "value"
}
}
```
> The `agent` field accepts a string ID, `{type: "agent", id, version}`, or `{type: "agent_with_overrides", id, version?, ...}` for session-local overrides of `model`/`system`/`tools`/`mcp_servers`/`skills`. Outside the overrides form, those fields live on the agent, not here. An `effort` inside a `model` override is ignored - set it on the agent. An `inference_geo` inside a `model` override **is** applied (omitting it clears the agent's pin for this session).
>
> **`budget`** (optional, create-only) is a hard dollar cap on the session's list-priced spend; `amount` is an integer string in minor units (cents - `"2500"` = $25.00), `USD` only. It can be changed or removed later via session update, never added. See `shared/managed-agents-core.md` -> Session budgets.
>
> **`initial_events`** (optional, max 50) sends events at creation and starts the agent loop in the same call. Only `user.message` and `user.define_outcome` are accepted - no `system.message`, and none of the tool-result kinds. Validation is all-or-nothing. See `shared/managed-agents-core.md` -> Seeding a session with `initial_events`.
>
> **`checkout`** accepts `{type: "branch", name: "..."}` or `{type: "commit", sha: "..."}`. Omit for the repo's default branch.
### CreateEnvironment Request Body
```json
{
"name": "string (required)",
"description": "string (optional)",
"config": {
"type": "cloud | self_hosted",
"networking": {
"type": "unrestricted | limited (union - see SDK types)"
},
"packages": { }
},
"metadata": { "key": "value" }
}
```
### CreateDeployment Request Body
```json
{
"name": "Weekly compliance scan",
"agent": "agent_abc123 (required - same shapes as CreateSession)",
"environment_id": "env_abc123 (required)",
"initial_events": [
{ "type": "user.message", "content": [{ "type": "text", "text": "Run the weekly compliance scan." }] }
],
"schedule": {
"type": "cron",
"expression": "0 20 * * 5",
"timezone": "America/New_York"
}
}
```
> Optional session config (`resources`, `vault_ids`, etc.) is supported the same way as on CreateSession, including `budget` - copied onto each fired session; unlike a session's, it can be added where none exists and re-added after clearing (see `shared/managed-agents-scheduled-deployments.md` § Deployment budgets). Response includes `status`, `paused_reason`, and `schedule.upcoming_runs_at` (next fire times). See `shared/managed-agents-scheduled-deployments.md`.
### SendEvents Request Body
```json
{
"events": [
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Hello"
}
]
}
]
}
```
> `system.message` events (append system-level context for this turn and later ones) use the same envelope with `type: "system.message"` - supported on Claude Opus 5, Claude Opus 4.8, Claude Sonnet 5, Claude Fable 5.1, and Claude Mythos 5.1, checked against the agent's *primary* model only; see `shared/managed-agents-events.md` § Adding system context mid-session.
### Define Outcome Event
```json
{
"type": "user.define_outcome",
"description": "Build a DCF model for Costco in .xlsx",
"rubric": { "type": "file", "file_id": "file_01..." },
"max_iterations": 5
}
```
> `rubric` is required: `{type: "text", content}` or `{type: "file", file_id}`. `max_iterations` default 3, max 20. Echoed back with `outcome_id` + `processed_at`. See `shared/managed-agents-outcomes.md`.
### Tool Result Event
```json
{
"type": "user.custom_tool_result",
"custom_tool_use_id": "sevt_abc123",
"content": [{ "type": "text", "text": "Result data" }],
"is_error": false
}
```
---
## Error Handling
Managed Agents endpoints use the standard Anthropic API error format. Errors are returned with an HTTP status code and a JSON body containing `type`, `error`, and `request_id`:
```json
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "Description of what went wrong"
},
"request_id": "req_011CRv1W3XQ8XpFikNYG7RnE"
}
```
Include the `request_id` when reporting issues to Anthropic - it lets us trace the request end-to-end. The inner `error.type` is one of the following:
| Status | Error type | Description |
|---|---|---|
| 400 | `invalid_request_error` | The request was malformed or missing required parameters |
| 401 | `authentication_error` | Invalid or missing API key |
| 403 | `permission_error` | The API key doesn't have permission for this operation |
| 404 | `not_found_error` | The requested resource doesn't exist |
| 409 | `invalid_request_error` | The request conflicts with the resource's current state (e.g., sending to an archived session) |
| 413 | `request_too_large` | The request body exceeds the maximum allowed size |
| 429 | `rate_limit_error` | Too many requests - check rate limit headers for retry timing |
| 500 | `api_error` | An internal server error occurred |
| 529 | `overloaded_error` | The service is temporarily overloaded - retry with backoff |
Note that `409 Conflict` carries `error.type: "invalid_request_error"` (there is no separate `conflict_error` type); inspect both the HTTP status and the `message` to distinguish conflicts from other invalid requests.
---
## Pagination
Most Managed Agents list endpoints use the `page` / `next_page` cursor scheme:
| Field | Where | Notes |
|---|---|---|
| `limit` | query | Max items per page |
| `page` | query | Opaque cursor from a previous response - pass a `next_page` or `prev_page` value here |
| `order` | query | `asc` / `desc` on endpoints that support sorting. A cursor encodes the `order` of the request that produced it - reusing it with a different `order` returns 400. Other params (filters, `limit`) can change between paginated requests. |
| `next_page` | response | Cursor for the next page; `null` when there are no more results |
| `prev_page` | response | Cursor for the previous page on endpoints that support backward pagination - currently **only `GET /v1/sessions`**. `null` on the first page. On endpoints that don't support it, the field is **absent** (not `null`). |
Every SDK exposes an auto-paginating iterator that follows `next_page`. In Python and TypeScript, iterate the list result directly; the other SDKs expose the iterator via a separate method (iterating the plain list result returns one page). SDK auto-pagination is **forward-only** - to go back a page, read `prev_page` from the response and pass it back as the `page` parameter yourself.
> Warning: Some endpoints use a **different** cursor scheme: Message Batches, Files, Models, and several Admin API endpoints take `after_id`/`before_id` and return `has_more`/`first_id`/`last_id` instead of `page`/`next_page`. Some `page`-scheme endpoints (e.g. `GET /v1/skills`) also return a `has_more` boolean alongside `next_page`. Check the endpoint's reference page for its exact pagination fields.
---
## Rate Limits
Managed Agents endpoints have per-organization request-per-minute (RPM) limits, separate from your [Messages API token limits](https://platform.claude.com/docs/en/api/rate-limits). Model inference inside a session still draws from your organization's standard ITPM/OTPM limits.
| Endpoint group | Scope | RPM | Max concurrent |
|---|---|---|---|
| Create operations (Agents, Sessions, Vaults) | organization | 300 | - |
| All other operations (Agents, Sessions, Vaults) | organization | 600 | - |
| All operations (Environments) | organization | 60 | 5 |
Files and Skills endpoints use the standard tier-based [rate limits](https://platform.claude.com/docs/en/api/rate-limits).
When a limit is exceeded the API returns `429` with a `rate_limit_error` (see [Error Handling](#error-handling) for the response envelope) and a `retry-after` header indicating how many seconds to wait before retrying. The Anthropic SDK reads this header and retries automatically.
# Managed Agents - Common Client Patterns
Patterns you'll write on the client side when driving a Managed Agent session, grounded in working SDK examples.
Code samples are TypeScript - other languages follow the same shape; see `{lang}/managed-agents/README.md` (cURL and C#: `curl/managed-agents.md`) for equivalents.
---
## 1. Lossless stream reconnect
**Problem:** SSE has no replay. If the connection drops mid-session, a naive reconnect re-opens the stream from "now" and you silently miss every event emitted in between.
**Solution:** on reconnect, fetch the full event history via `events.list()` *before* consuming the live stream, and dedupe on event ID as the live stream catches up.
```ts
const seenEventIds = new Set<string>()
const stream = await client.beta.sessions.events.stream(session.id)
// Stream is now open and buffering server-side. Read history first.
for await (const event of client.beta.sessions.events.list(session.id)) {
seenEventIds.add(event.id)
handle(event)
}
// Tail the live stream. Dedupe only gates handle() - terminal checks must run
// even for already-seen events, or a terminal event that was in the history
// response gets skipped by `continue` and the loop never exits.
for await (const event of stream) {
if (!seenEventIds.has(event.id)) {
seenEventIds.add(event.id)
handle(event)
}
if (event.type === 'session.status_terminated') break
if (event.type === 'session.status_idle' && event.stop_reason.type !== 'requires_action') break
}
```
---
## 2. `processed_at` - queued vs processed
Every event on the stream carries `processed_at` (ISO 8601), set when the event finishes processing. For client-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`) it's `null` while the event is queued behind earlier ones, and populated once the agent processes it - so the same event appears on the stream twice, once with `null` and once with a timestamp. (Exception: a `user.interrupt` sent while the session is paused at its budget is accepted and ignored - it never appears at all; see `shared/managed-agents-events.md` § Reaching a session budget.)
**Three event types skip the queued phase:** `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result` are processed on receipt and echoed back with `processed_at` already populated. A pending -> acknowledged UI that assumes "first sighting is always `null`" will never clear for these - treat a populated `processed_at` on first sighting as immediately acknowledged.
```ts
for await (const event of stream) {
if (event.type === 'user.message') {
if (event.processed_at == null) onQueued(event.id)
else onProcessed(event.id, event.processed_at)
}
}
```
Use this to drive pending -> acknowledged UI state for anything you send. How you map a locally-rendered optimistic message to the server-assigned `event.id` is application-specific (typically via the return value of `events.send()` or FIFO ordering).
---
## 3. Interrupt a running session
Send `user.interrupt` as a normal event. The session keeps running until it reaches a safe boundary, then goes idle.
```ts
await client.beta.sessions.events.send(session.id, {
events: [{ type: 'user.interrupt' }],
})
// Drain until the session is truly done - see Pattern 5 for the full gate.
for await (const event of stream) {
if (event.type === 'session.status_terminated') break
if (
event.type === 'session.status_idle' &&
event.stop_reason.type !== 'requires_action'
) break
}
```
Reference: `interrupt.ts` - sends the interrupt the moment it sees `span.model_request_start`, drains to idle, then verifies via `sessions.retrieve()`.
---
## 4. `tool_confirmation` round-trip
When the agent has `permission_policy: { type: 'always_ask' }`, any call to that tool fires an `agent.tool_use` event with `evaluated_permission === 'ask'` and the session goes idle waiting for a decision. Respond with `user.tool_confirmation`.
```ts
for await (const event of stream) {
if (event.type === 'agent.tool_use' && event.evaluated_permission === 'ask') {
await client.beta.sessions.events.send(session.id, {
events: [{
type: 'user.tool_confirmation',
tool_use_id: event.id, // not a toolu_ id - use event.id
result: 'allow', // or 'deny'
// deny_message: '...', // optional, only with result: 'deny'
}],
})
}
}
```
Key points:
- `tool_use_id` is `event.id` (typically `sevt_...`), **not** a `toolu_...` ID.
- `result` is `'allow' | 'deny'`. Use `deny_message` to tell the model *why* you denied - it gets surfaced back to the agent.
- Multiple pending tools: respond once per `agent.tool_use` event with `evaluated_permission === 'ask'`.
Reference: `tool-permissions.ts`.
---
## 5. Correct idle-break gate
Do not break on `session.status_idle` alone. The session goes idle transiently - e.g. between parallel tool executions, while waiting for a `user.tool_confirmation`, or while awaiting a `user.custom_tool_result`. Break when idle with a non-`requires_action` `stop_reason` (terminal, or `budget_reached` - resumable only by a budget update, so break unless you intend to change or remove the budget), or on `session.status_terminated`.
```ts
for await (const event of stream) {
handle(event)
if (event.type === 'session.status_terminated') break
if (event.type === 'session.status_idle') {
if (event.stop_reason.type === 'requires_action') continue // waiting on you - handle it
break // end_turn, retries_exhausted, or budget_reached - see list below
}
}
```
`stop_reason.type` values on `session.status_idle`:
- `requires_action` - agent is waiting on a client-side event (tool confirmation, custom tool result). Handle it, don't break. **Self-hosted exception:** if the session went `requires_action`-idle with no pending `agent.tool_use` (always_ask) or `agent.custom_tool_use` to answer, the worker failed the claimed work item (typically a memory-store mount error, logged only on the worker host). Don't `continue` forever on that - surface it, fix the host, and send `user.interrupt` to re-queue the work (`shared/managed-agents-self-hosted-sandboxes.md` § Memory stores -> Troubleshooting).
- `retries_exhausted` - terminal failure. Break, then check `sessions.retrieve()` for the error state.
- `end_turn` - normal completion.
- `budget_reached` - the session hit its spend cap and paused. Not terminal and not resumable by any event: change (typically raise) or remove the session's `budget` to resume, or treat it as done. A `session.usage` event with the final cost immediately precedes this idle. See `shared/managed-agents-core.md` § Session budgets.
---
## 6. Post-idle status-write race
The SSE stream emits `session.status_idle` slightly before the session's queryable status reflects it. Clients that break on idle and immediately call `sessions.delete()` or `sessions.archive()` will intermittently 400 with "cannot delete/archive while running."
Poll before cleanup:
```ts
let s
for (let i = 0; i < 10; i++) {
s = await client.beta.sessions.retrieve(session.id)
if (s.status !== 'running') break
await new Promise(r => setTimeout(r, 200))
}
if (s?.status !== 'running') {
await client.beta.sessions.archive(session.id)
} // else: still running after 2s - don't archive, let it settle or escalate
```
---
## 7. Stream-first, then send
Always open the stream **before** sending the kickoff event. Otherwise the agent may process the event and emit the first events before your consumer is attached, and you'll miss them.
```ts
const stream = await client.beta.sessions.events.stream(session.id)
await client.beta.sessions.events.send(session.id, {
events: [{ type: 'user.message', content: [{ type: 'text', text: 'Hello' }] }],
})
for await (const event of stream) { /* ... */ }
```
The `Promise.all([stream, send])` shape works too, but stream-first is simpler and has the same effect - the stream starts buffering the moment it's opened.
---
## 8. File-mount gotchas
**The mounted resource has a different `file_id` than the file you uploaded.** Session creation makes a session-scoped copy.
```ts
const uploaded = await client.beta.files.upload({ file, purpose: 'agent_resource' })
// uploaded.id -> the original file
const session = await client.beta.sessions.create({
/* ... */
resources: [{ type: 'file', file_id: uploaded.id, mount_path: '/workspace/data.csv' }],
})
// session.resources[0].file_id !== uploaded.id <- different IDs
```
Delete the original via `files.delete(uploaded.id)`; the session-scoped copy is garbage-collected with the session. `mount_path` must be absolute - see `shared/managed-agents-environments.md`.
---
## 9. Secrets for non-MCP APIs and CLIs - keep them host-side via custom tools
**Problem:** you want the agent to call a third-party API or run a CLI that needs a secret (API key, token, service-account credential), but you can't or don't want to hand the secret to a vault.
**First check:** for cloud environments, the first-class answer is now a vault `environment_variable` credential - the agent's shell sees an opaque placeholder and the real secret is substituted at egress. See `shared/managed-agents-tools.md` -> Vaults. Use this pattern instead when that doesn't fit: **self-hosted sandboxes** (env-var credentials not yet supported there), clients that reject the placeholder via local format validation, secrets that must never leave your infrastructure, or calls that need host-side binaries.
**Solution:** move the authenticated call to your side. Declare a custom tool on the agent; when the agent emits `agent.custom_tool_use`, your orchestrator (the process reading the SSE stream) executes the call with its own credentials and responds with `user.custom_tool_result`. The container never sees the key.
```ts
// Agent template: declare the tool, no credentials
tools: [{ type: 'custom', name: 'linear_graphql', input_schema: { /* query, vars */ } }]
// Orchestrator: handle the call with host-side creds
for await (const event of stream) {
if (event.type === 'agent.custom_tool_use' && event.name === 'linear_graphql') {
const result = await linear.request(event.input.query, event.input.vars) // host's key
await client.beta.sessions.events.send(session.id, {
events: [{
type: 'user.custom_tool_result',
custom_tool_use_id: event.id,
content: [{ type: 'text', text: JSON.stringify(result) }],
}],
})
}
}
```
Same shape works for `gh` CLI, local eval scripts, or anything else that needs host-side auth or binaries.
**Security note:** this does not expose a public endpoint. `agent.custom_tool_use` arrives on the SSE stream your orchestrator already holds open with your Anthropic API key, and `user.custom_tool_result` goes back via `events.send()` under the same key. Your orchestrator is a client, not a server - nothing unauthenticated is listening.
**Do not embed API keys in the system prompt or user messages as a workaround.** Prompts and messages are stored in the session's event history, returned by `events.list()`, and included in compaction summaries - a secret placed there is durably persisted and readable via the API for the life of the session.
# Managed Agents - Core Concepts
## Architecture
Managed Agents is built around four core concepts:
| Concept | Endpoint | What it is |
|---|---|---|
| **Agent** | `/v1/agents` | A persisted, versioned object defining the agent's capabilities and persona: model, system prompt, tools, MCP servers, skills. **Must be created before starting a session.** See the Agents section below. |
| **Session** | `/v1/sessions` | A stateful interaction with an agent. References a pre-created agent by ID + an environment + initial instructions. Produces an event stream. |
| **Environment** | `/v1/environments` | A template defining the configuration for container provisioning. |
| **Container** | N/A | An isolated compute instance where the agent's **tools** execute (bash, file ops, code). The agent loop does not run here - it runs on Anthropic's orchestration layer and acts on the container via tool calls. |
```
+-------------------------------------+
| Anthropic orchestration layer |
Agent (config) ------->| (agent loop: Claude + tool calls) |
+--------------+----------------------+
| tool calls
v
Environment (template) --> Container (tool execution workspace)
|
Session -+
+-- Resources (files, repos, memory stores - attached at startup)
+-- Vault IDs (MCP credential references)
+-- Conversation (event stream in/out)
```
> **Agent creation is a prerequisite.** Sessions reference a pre-created agent by ID - `model`/`system`/`tools` live on the agent object, never on the session. Every flow starts with `POST /v1/agents`.
---
## Session Lifecycle
```
rescheduling -> running <-> idle -> terminated
```
| Status | Description |
| -------------- | ------------------------------------------------------------------ |
| `idle` | Agent has finished the current task, and is awaiting input. It's either waiting for input to continue working via a `user.message`, blocked awaiting a `user.custom_tool_result` or `user.tool_confirmation`, or paused because the session budget cap was reached. The `stop_reason` attached contains more information about why the Agent has stopped working. |
| `running` | Session has starting running, and the Agent is actively doing work. |
| `rescheduling` | Session is (re)scheduling after a retryable error has occurred, ready to be picked up by the orchestration system. |
| `terminated` | Session has ended and is in an irreversible, unusable state - **either on completion or because of an unrecoverable error**. Terminated does not by itself mean failure; fetch the session to tell the two apart. |
- Events can be sent when the session is `running` or `idle`. Messages are queued and processed in order. Exception: a session paused at its budget (`stop_reason: budget_reached`) accepts only **settle events** - events that resolve work already in progress (`user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`, `user.interrupt`) rather than starting new work - see § Session budgets.
- The agent transitions `idle -> running` when it receives a new event, then back to `idle` when done.
- Errors surface as `session.error` events in the stream, not as a status value.
Every session has a live trace view in the Anthropic Console at `https://platform.claude.com/workspaces/{workspace}/sessions/{session_id}`. Print this URL immediately after creating a session so the user can watch tool calls and messages stream in real time. **`{workspace}` is the workspace the API key belongs to** - use `default` only when that's the org's Default workspace. The session response does **not** include a workspace field and the Console has no workspace-agnostic session route, so for non-default workspaces substitute the workspace's ID (visible in the Console URL bar, or expose it as a config value alongside the API key). A `default` link to a session that lives in another workspace lands on a **"Session not found"** page - the **Search workspaces** button there will locate it, but it is not an automatic redirect.
### Built-in session features
- **Context compaction** - if you approach max context, the API automatically condenses session history to keep the interaction going
- **Prompt caching** - historical repeated tokens are cached, reducing processing time and cost
- **Extended thinking** - on by default; `agent.thinking` events signal thinking progress and carry no thinking content
### Session operations
| Operation | Notes |
|---|---|
| List / fetch | Paginated list or single resource by ID |
| Update | `title`, `metadata`, and the session-local `agent.tools`/`agent.mcp_servers` can be overridden (see § Updating the agent configuration mid-session). `budget` can only be changed or removed (see § Session budgets). `vault_ids` is create-only - update requests setting it are rejected. |
| Archive | Session becomes **read-only**. Not reversible. |
| Delete | Permanently deletes session, event history, container, and checkpoints. |
These are ops/inspection calls - typically made from a terminal, not application code. From the shell (see `shared/anthropic-cli.md`):
```sh
ant beta:sessions list --transform '{id,title,status,created_at}' --format jsonl
ant beta:sessions retrieve --session-id "$SID"
ant beta:sessions:events stream --session-id "$SID" # watch events live
ant beta:sessions archive --session-id "$SID"
ant beta:sessions delete --session-id "$SID"
```
---
## Sessions
A session is a running agent instance inside an environment.
### Session Object
Key fields returned by the API:
| Field | Type | Description |
| --------------- | -------- | --------------------------------------------------- |
| `type` | string | Always `"session"` |
| `id` | string | Unique session ID |
| `title` | string | Human-readable title |
| `status` | string | `idle`, `running`, `rescheduling`, `terminated` |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |
| `archived_at` | string | ISO 8601 timestamp (nullable) |
| `environment_id` | string | Environment ID |
| `agent` | object | Agent configuration |
| `resources` | array | Attached files, repos, and memory stores |
| `metadata` | object | User-provided key-value pairs (max 8 keys) |
| `usage` | object | Cumulative usage: token counts, `server_tool_use` (web search/fetch request counts), `list_cost` (consumption priced at public list rates, as `{amount, currency}` with the amount an integer string in minor units - cents), and `active_seconds` (time with >=1 thread running; concurrent-thread overlap counted once - unlike `stats.active_seconds`, which sums per-thread time) |
| `budget` | object | The session's spend cap, when one was set at creation - see § Session budgets |
| `stats` | object | Timing statistics - `stats.active_seconds` sums per-thread time, unlike `usage.active_seconds` |
### Creating a session
**A session is meaningless without an agent.** Sessions reference a pre-created agent by ID. Create the agent first via `agents.create()`, then reference it:
```ts
// 1. Create the agent (reusable, versioned)
const agent = await client.beta.agents.create(
{
name: "Coding Assistant",
model: "claude-opus-5",
system: "You are a helpful coding agent.",
tools: [{ type: "agent_toolset_20260401"}],
},
);
// 2. Start a session that references it
const session = await client.beta.sessions.create(
{
agent: agent.id, // string shorthand -> latest version. Or: { type: "agent", id: agent.id, version: agent.version }
environment_id: environmentId,
title: "Hello World Session",
},
);
```
> Tip: **Watch it live in Console.** While developing, print a link so you can click through to the session UI instead of parsing the event stream: `https://platform.claude.com/workspaces/{workspace}/sessions/{session.id}`. Use `default` for `{workspace}` only when the API key belongs to the org's Default workspace; otherwise substitute the workspace's ID (the session response does not carry it - read it from the Console URL bar or make it a config value). Include this `print`/`console.log` in code you generate for users who are iterating locally.
**Session creation parameters:**
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ---------------------------------------------- |
| `agent` | string or object | **Yes** | Three forms: string shorthand `"agent_abc123"` (latest version); pinned `{type: "agent", id, version}`; or `{type: "agent_with_overrides", id, version?, ...}` to override `model`/`system`/`tools`/`mcp_servers`/`skills` for this session only - see § Override agent configuration for a session |
| `environment_id`| string | **Yes** | Environment ID |
| `title` | string | No | Human-readable name (appears in logs/dashboards) |
| `resources` | array | No | Files, GitHub repos, or memory stores, attached to the container at startup. Memory stores are session-create-only (not addable via `resources.add()`). |
| `initial_events`| array | No | Events to send at creation, processed in order - collapses create + first send into one call. See § Seeding a session with `initial_events` below. |
| `vault_ids` | array | No | Vault IDs (`vlt_*`) - MCP credentials with auto-refresh + `environment_variable` secrets substituted at egress. See `shared/managed-agents-tools.md` -> Vaults. |
| `budget` | object | No | Hard dollar cap on the session's spend: `{type: "limit", max_list_cost: {amount, currency}}`. **Create-only** - can be changed or removed later, never added. See § Session budgets. |
| `metadata` | object | No | User-provided key-value pairs |
#### Seeding a session with `initial_events`
Creating a session without `initial_events` registers the session in `idle` and starts no work; the sandbox is provisioned when the session first needs it. Passing a **non-empty** `initial_events` array starts the agent loop in the same call - the session is **created directly in `running`**, never passing through `idle`. A client that waits for an `idle -> running` transition to know work began will wait forever; check `status` on the create response instead.
```python
session = client.beta.sessions.create(
agent=AGENT_ID,
environment_id=ENVIRONMENT_ID,
initial_events=[
{"type": "user.message", "content": [{"type": "text", "text": "Review the auth module."}]},
],
)
```
- **Only `user.message` and `user.define_outcome` are accepted**, max **50** events. The tool-result kinds (`user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`) are rejected because no agent turn exists yet, and `user.interrupt` because there is no turn to stop. Unlike a scheduled deployment's `initial_events`, a session's does **not** accept `system.message`.
- Each event is validated and persisted before the create response returns, in list order, with a server-assigned ID - exactly as if you had posted it to the send-events endpoint immediately after creation. Per-event content rules are the same as on that endpoint.
- **The events are not echoed on the create response.** Read them back with `sessions.events.list(session.id)` if you need their server-assigned IDs.
- **Validation is all-or-nothing:** if any event fails, the whole request is rejected and no session is created. An empty list is equivalent to omitting the field.
- Rejections: more than one `user.define_outcome` -> 400; a `user.define_outcome` without a `rubric` -> 400; more than 100 file-sourced `document` content blocks across the whole list -> 400; a request body over 32 MB -> 413.
An outcome-driven session is therefore a single call - pass one `user.define_outcome` in `initial_events` instead of creating the session and then sending the event (see `shared/managed-agents-outcomes.md`).
**Agent configuration fields** (passed to `agents.create()`, not `sessions.create()`):
| Field | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------------------------------- |
| `name` | string | **Yes** | Human-readable name (1-256 chars) |
| `model` | string or object | **Yes** | Claude model ID (bare string, or an object taking `id`, `speed`, `effort`, and `inference_geo`). All Claude 4.5+ models supported. See § Effort on the agent model and § Pinning inference geography below. |
| `system` | string | No | System prompt - defines the agent's behavior (up to 100K chars) |
| `tools` | array | No | Encompasses three kinds: (1) pre-built Claude Agent tools (`agent_toolset_20260401`), (2) MCP tools (`mcp_toolset`), and (3) custom client-side tools. Max 128. |
| `mcp_servers` | array | No | MCP server connections - standardized third-party capabilities (e.g. GitHub, Asana). Max 20, unique names. See `shared/managed-agents-tools.md` -> MCP Servers. |
| `skills` | array | No | Customized "best-practices" context with progressive disclosure. Max 20. See `shared/managed-agents-tools.md` -> Skills. |
| `description` | string | No | Description of the agent (up to 2048 chars) |
| `multiagent` | object | No | `{type: "coordinator", agents: [...]}` - roster this agent may delegate to. See `shared/managed-agents-multiagent.md`. |
| `metadata` | object | No | Arbitrary key-value pairs (max 16, keys <=64 chars, values <=512 chars) |
### Session budgets
A **session budget** is an optional hard spend ceiling set at session creation. The platform continuously prices everything the session consumes at **public list rates** (the session's **list cost**) and stops issuing new model requests once that total reaches the cap. A session at its budget **pauses and goes `idle` with `stop_reason: budget_reached`** - it is not terminated; history and sandbox are preserved, and changing or removing the budget resumes the paused work automatically.
```python
session = client.beta.sessions.create(
agent=AGENT_ID,
environment_id=ENVIRONMENT_ID,
budget={
"type": "limit",
"max_list_cost": {"amount": "2500", "currency": "USD"}, # minor units: "2500" = $25.00
},
)
```
- `type` is always `"limit"`. `max_list_cost.amount` is the amount in **minor units of the currency (cents), as an integer string** with no leading zeros, > 0 - `"2500"` is $25.00, `"50"` is fifty cents. A string rather than a number so no float rounding is ever applied; decimal forms such as `"25.00"` are rejected. `max_list_cost.currency` is uppercase ISO-4217; **`USD` is the only supported currency.**
- **What counts toward list cost:** model tokens at each served model's list price, web searches at $10 per 1,000, and session running time at $0.08/hour. List cost is *not* your contracted price - with negotiated discounts, the session hits the cap when the list-price total does, and billed spend may be lower.
- **Enforcement is a pre-request gate:** before every model request the platform checks whether consumed list cost has reached the cap and pauses the thread if it has; the request that crosses the cap completes, so the final figure can exceed the cap by at most one model request per running thread. Treat the budget as a bound on new work, not an exact stop.
- The reported `list_cost` is **rounded to the nearest cent** while enforcement compares exact amounts - rounding can move the reported figure up to half a cent in either direction from the exact amount, so a session whose reported `list_cost` equals its cap may not yet be paused. Treat `stop_reason: budget_reached` (or the 400 on `user.message`), not the reported figure, as the signal that the cap was reached.
- **Create-only.** Adding a budget to a session created without one is a 400. Updates accept exactly two changes: **change the cap** (the new value can be higher or lower than the old cap, but must be strictly greater than the consumed list cost, else 400: `budget.max_list_cost must be greater than the session's consumed list cost`) or **remove** (`budget: null` - the `session.updated` event carries `budget: null` rather than a separate flag). Because the consumed cost usually sits a fraction past the old cap when the session pauses, base the new value on the session's reported `usage.list_cost`, not the old `max_list_cost`. **Removal is one-way**: a removed budget can never be re-added; to keep a cap, change it instead.
- **At the cap, only settle events are accepted** - events that resolve work already in progress rather than starting new work: `user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`, `user.interrupt`. A `user.interrupt` sent while the session is paused at its budget (all threads paused at the cap) is accepted and ignored: it does not appear in the event list and changes nothing. Raise or remove the budget to continue. Anything that starts new work (e.g. `user.message`) is a 400 naming that list. No event resumes the session - only a budget change/removal does.
- **Multiagent:** one budget shared across all threads, no per-thread caps. Threads pause independently; each thread's consumption is priced at its own served model. A pending tool ask outranks the cap: a session with one thread at `requires_action` and another at `budget_reached` reports `requires_action` at the session level - answer it as usual (settle events aren't blocked).
- **Models without a list price can't be budgeted:** a budgeted create whose agent (or any roster agent, including the advisor's model) uses an unpriced model is a 400. If a running budgeted session's usage comes to include one, changing the budget is rejected - remove the budget to resume.
- Stream behavior at the cap and the `session.usage` event: `shared/managed-agents-events.md` § Reaching a session budget.
- Scheduled deployments can carry a budget too - copied onto each fired session, with different update semantics (clearable and re-addable): `shared/managed-agents-scheduled-deployments.md` § Deployment budgets.
> **Not the same thing as Messages-API task budgets.** Session budgets are hard, dollar-denominated, platform-enforced caps on one session. `task_budget` on the Messages API is an advisory, token-denominated budget the model uses to pace itself within one agentic loop.
---
## Agents
**This is where every Managed Agents flow begins.** The agent object is a persisted, versioned configuration - you create it once, then reference it by ID every time you start a session. No agent -> no session.
### Agent Object
The API is **flat** - `model`, `system`, `tools` etc. are top-level fields, not wrapped in an `agent:{}` sub-object.
| Field | Type | Required | Description |
| ------------------ | -------- | -------- | -------------------------------------------------- |
| `name` | string | Yes | Human-readable name |
| `model` | string or object | Yes | Claude model ID - bare string, or `{id, speed?, effort?, inference_geo?}` |
| `system` | string | No | System prompt |
| `tools` | array | No | Agent toolset / MCP toolset / custom tools |
| `mcp_servers` | array | No | MCP server connections |
| `skills` | array | No | Skill references (max 20) |
| `description` | string | No | Description of the agent |
| `multiagent` | object | No | Coordinator roster - see `shared/managed-agents-multiagent.md` |
| `metadata` | object | No | Arbitrary key-value pairs |
### Lifecycle: create once, run many, update in place
The agent is a **persistent resource**, not a per-run parameter. The intended pattern:
```
+- setup (once) ---------+ +- runtime (every invocation) -+
| agents.create() | | sessions.create( |
| -> store agent_id | ---> | agent={type:..., id: ID} |
| in config/env/db | | ) |
+------------------------+ +------------------------------+
```
**Anti-pattern:** calling `agents.create()` at the top of every script run. This accumulates orphaned agent objects, pays create latency on every invocation, and defeats the versioning model. If you see `agents.create()` in a function that's called per-request or per-cron-tick, that's wrong - hoist it to one-time setup and persist the ID.
> **Recommended - define agents and environments as YAML + apply via the `ant` CLI.** The split is **CLI for the control plane, SDK for the data plane**: agents and environments are relatively static resources you manage with `ant` (version-controlled YAML, applied from CI); sessions are dynamic and driven by your application through the SDK. See `shared/anthropic-cli.md` -> *Version-controlled Managed Agents resources* for the `ant beta:agents create < agent.yaml` / `update --version N` flow. The SDK `agents.create()` call shown elsewhere in this doc is the in-code equivalent - use it when you need to provision programmatically, but prefer the YAML flow for anything a human maintains.
### Effort on the agent model
Pass `model` as an object to set the effort level: `{"id": "claude-opus-5", "effort": "high"}`. `effort` accepts a level string (`low`, `medium`, `high`, `xhigh`, `max`) or an object such as `{"type": "high"}`. The create/update response echoes it in object form and fills in omitted `model` fields with their defaults.
> Warning: **Effort is agent configuration only.** An `effort` set inside a per-session `model` override is **not applied** - the session runs at the agent's effort. To change effort you must update the agent (or point the session at a different agent). This is the one field where the override form silently does nothing rather than erroring.
The same object form carries `speed` for fast mode: `{"id": "claude-opus-5", "speed": "fast"}`.
### Pinning inference geography (`inference_geo`)
The `model` object also takes `inference_geo` to pin the geography that serves the agent's model requests: `{"id": "claude-opus-5", "inference_geo": "us"}`. Accepts `"us"` or `"global"` - and unlike the Messages API, where `inference_geo` is a top-level request parameter, here it is always nested inside `model`, never top-level. When unset, each model request follows the workspace's default inference geo at the time it's served.
- **Validated at every stage:** the pin is checked against the workspace's `allowed_inference_geos` when the agent is saved, when a session is created from it, and on every turn the session serves. If the workspace allowlist later narrows so the pin is no longer allowed, new sessions can't be created from the agent and **running sessions refuse further turns** - pins are never grandfathered (workspaces rely on them for compliance).
- Setting `inference_geo` on a model that doesn't support geographic inference pinning returns a 400.
- **Fixed for a session's lifetime** - the pin can't change mid-session. Set it on the agent, or set/clear it for one session with a `model` override at session create (see § Override agent configuration for a session).
- **Multiagent rosters must be geo-uniform:** the coordinator's pin and every roster member's must all be the same value or all be unset - see `shared/managed-agents-multiagent.md`.
- Unlike `effort`, an `inference_geo` inside a per-session `model` override **is applied** - and because overrides replace the `model` object in full, an override that *omits* `inference_geo` clears the agent's pin for that session.
### Versioning
Each `POST /v1/agents/{id}` (update) creates a new immutable version - a sequential integer, starting at 1 and incrementing on each update. The agent's history is append-only - you can't edit a past version.
**`version` on update is optional.** Supply it for optimistic concurrency, or omit it to apply the update unconditionally:
| `version` | Behavior | Fits |
|---|---|---|
| Supplied (must be >= 1) | 409 if it doesn't match the agent's current version - **even when the fields you send already equal the stored values**. Re-read and retry. | Interactive callers; the recommended default |
| Omitted | Applies unconditionally. The most recent update silently replaces any concurrent one, with no error to either caller. | Declarative apply loops - e.g. a CI job syncing checked-in agent definitions, where the loop owns the agent |
**Update semantics.** Omitted fields are preserved. Scalar fields (`model`, `system`, `name`, `description`) are replaced; `system` and `description` can be cleared with `null`, while `model` and `name` cannot. Array fields (`tools`, `mcp_servers`, `skills`) are replaced wholesale - `null` or `[]` clears them. **`effort` is the sole exception inside a `model` object you supply:** if the model `id` is unchanged, omitting `effort` leaves the stored level alone; if you change the `id`, an omitted `effort` resets to the new model's default. Other `model` fields are replaced along with the object - **supplying `model` without `inference_geo` clears the agent's inference geo pin.**
**Why version:**
- **Reproducibility** - pin a session to a known-good config: `{type: "agent", id, version: 3}`
- **Safe iteration** - update the agent without breaking sessions already running on the old version
- **Rollback** - if a new system prompt regresses, pin new sessions back to the prior version while you debug
**`version` is optional.** Omit it (or use the string shorthand `agent="agent_abc123"`) to get the latest version at session-creation time. Pass it explicitly (`{type: "agent", id, version: N}`) to pin for reproducibility.
**Getting the version to pin:** `agents.create()` and `agents.update()` both return `version` in the response. Store it alongside `agent_id`. To fetch the current latest for an existing agent: `GET /v1/agents/{id}` -> `.version`.
**When to update vs create new:** Update (`POST /v1/agents/{id}`) when it's conceptually the same agent with tweaked behavior (better prompt, extra tool). Create a new agent when it's a different persona/purpose. Rule of thumb: if you'd give it the same `name`, update.
### Agent Endpoints
| Operation | Method | Path |
| ---------------- | -------- | ------------------------------------- |
| Create | `POST` | `/v1/agents` |
| List | `GET` | `/v1/agents` |
| Get | `GET` | `/v1/agents/{id}` |
| Update | `POST` | `/v1/agents/{id}` |
| Archive | `POST` | `/v1/agents/{id}/archive` |
> Warning: **Archive is permanent.** Archiving makes the agent read-only: existing sessions continue to run, but **new sessions cannot reference it**, and there is no unarchive. Since agents have no `delete`, this is the terminal lifecycle state. Never archive a production agent as routine cleanup - confirm with the user first.
### Using an Agent in a Session
Reference the agent by string ID (latest version) or by object with an explicit version:
```python
# String shorthand - uses the agent's latest version
session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment_id,
)
# Or pin to a specific version (int)
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=environment_id,
)
```
### Override agent configuration for a session
The third `agent` form, `agent_with_overrides`, replaces parts of the agent's configuration for **a single session** - try a different model or grant an extra tool without versioning the agent. Pass `id` (and optionally `version`; omitted = latest, same default as the other two forms) plus any of `model`, `system`, `tools`, `mcp_servers`, `skills`:
```python
session = client.beta.sessions.create(
agent={
"type": "agent_with_overrides",
"id": agent.id,
"model": "claude-opus-5", # replace the agent's model for this session
"system": None, # clear the system prompt for this session
},
environment_id=environment_id,
)
```
Each overridable field follows tri-state rules:
- **Omit** -> the session inherits the value from the referenced agent version.
- **`null` (or `[]` for list fields)** -> the session runs with that field cleared. Applies in full to `system` and `skills`. Three exceptions: `model` is never clearable (`model: null` -> 400 `agent_model_required`); clearing `tools` returns 400 when the session's effective `skills` is non-empty (skills require the `read` tool); and clearing `mcp_servers` returns 400 when the effective `tools` still contains an `mcp_toolset` referencing one of the agent's servers - override `tools` in the same request to drop those entries, then clear `mcp_servers`.
- **A value** -> replaces the agent's value **in full**. Overrides never merge - a `tools` override must list every tool the session should have. One exception: an `effort` level inside a `model` override is **not applied** (set it on the agent instead - see § Effort on the agent model). An `inference_geo` inside a `model` override **is** applied - and because the object is replaced in full, an override that omits it clears the agent's pin, so the session follows the workspace's default inference geo. The overridden value is validated against the workspace's `allowed_inference_geos` at session create.
Overrides are session-local: they do **not** modify the agent resource or create a new agent version. The response's `agent` object reflects the post-override configuration, while its `id` and `version` still identify the base agent - so you can trace a session back to its base. In multiagent sessions, overrides apply to the coordinator and its `{type: "self"}` copies; roster agents referenced by ID always use their own as-created configuration (see `shared/managed-agents-multiagent.md`).
### Updating the agent configuration mid-session
`sessions.update()` can change `agent.tools` and `agent.mcp_servers` (including permission policies and the per-tool web settings - `allowed_domains` / `blocked_domains` etc., see `shared/managed-agents-tools.md` § Web search & web fetch settings) on an **existing** session. Updated domain lists apply to the rest of the session. This is a **session-local override** - it does not create a new agent version and does not propagate back to the agent object. The provided arrays are **full replacements**; to append one tool, `GET` the session, modify, and `POST` back. The session must be `idle` - interrupt first if running. `vault_ids` is **create-only**: the update param exists in the SDK but is rejected by the API ("Not yet supported") - attach vaults when you create the session.
Among the agent-configuration fields, only `tools` and `mcp_servers` can change after a session is created - to run with a `model`, `system`, or `skills` other than the agent's values, use `agent_with_overrides` at create time (above). (`title`, `metadata`, and `budget` have their own session-update paths - see § Session operations / § Session budgets.) The agent's model configuration - including its `inference_geo` pin - and its configured `system` field are fixed for the session's lifetime; you can still **append system-level context between turns** by sending a `system.message` event (see `shared/managed-agents-events.md` § Adding system context mid-session).
```python
client.beta.sessions.update(
session.id,
agent={
"tools": [
{"type": "agent_toolset_20260401"},
{"type": "mcp_toolset", "mcp_server_name": "linear"},
],
"mcp_servers": [{"type": "url", "name": "linear", "url": "https://mcp.linear.app/sse"}],
},
)
```
# Managed Agents - Environments & Resources
## Environments
Creating a session requires an `environment_id`. Environments are **reusable configuration templates** for spinning up containers in Anthropic's infrastructure - you might create different environments for different use cases (e.g. data visualization vs web development, with different package sets). Anthropic handles scaling, container lifecycle, and work orchestration.
**Environment names must be unique.** Creating an environment with an existing name returns 409.
### Networking
| Network Policy | Description |
| ---------------- | ------------------------------------------------------------- |
| `unrestricted` | Full egress (except legal blocklist) |
| `limited` | Deny-by-default; opt in via `allowed_hosts` / `allow_package_managers` / `allow_mcp_servers` |
```json
{
"networking": {
"type": "limited",
"allow_package_managers": true,
"allow_mcp_servers": true,
"allowed_hosts": ["api.example.com"]
}
}
```
All three `limited` fields are optional. `allow_package_managers` (default `false`) permits PyPI/npm/etc.; `allow_mcp_servers` (default `false`) permits the agent's configured MCP server endpoints without listing them in `allowed_hosts`.
**MCP caveat:** Under `limited` networking, either set `allow_mcp_servers: true` or add each MCP server domain to `allowed_hosts`. Otherwise the container can't reach them and tools silently fail.
**Packages caveat:** Under `limited` networking, `packages` requires `allow_package_managers: true`; otherwise the request fails with a 400. Listing the registry in `allowed_hosts` is not enough.
**`networking` does not govern `web_search` / `web_fetch`.** Those tools run on Anthropic's servers (in cloud *and* self-hosted environments), so `limited` egress and `allowed_hosts` don't restrict them. To restrict the sites they can reach, set `allowed_domains` / `blocked_domains` on the tool's `configs` entry in the agent toolset - see `shared/managed-agents-tools.md` § Web search & web fetch settings.
### Creating an environment
The SDK adds `managed-agents-2026-04-01` automatically. TypeScript:
```ts
const env = await client.beta.environments.create({
name: "my_env",
config: {
type: "cloud",
networking: { type: "unrestricted" },
},
});
```
### Self-hosted sandboxes
To run tool execution in **your own infrastructure** instead of Anthropic's, set `config: {type: "self_hosted"}` - the agent loop stays on Anthropic's side, but `bash` / file ops / code execute in a container you control via an outbound-polling worker. The `networking` block does not apply (you control egress). Resource mounting (`file`, `github_repository`) and memory stores behave differently - see `shared/managed-agents-self-hosted-sandboxes.md` for the worker, credentials, and cloud-vs-self-hosted comparison.
### Environment CRUD
| Operation | Method | Path | Notes |
| ---------------- | -------- | ------------------------------------------ | ----- |
| Create | `POST` | `/v1/environments` | |
| List | `GET` | `/v1/environments` | Paginated (`limit`, `after_id`, `before_id`) |
| Get | `GET` | `/v1/environments/{id}` | |
| Update | `POST` | `/v1/environments/{id}` | Changes apply only to **new** containers; existing sessions keep their original config |
| Delete | `DELETE` | `/v1/environments/{id}` | Returns 204. |
| Archive | `POST` | `/v1/environments/{id}/archive` | Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive - terminal state. |
---
## Resources
Attach files, GitHub repositories, and memory stores to a session. Resources are resolved during session creation, so a bad `file_id` or an unreachable repo surfaces on the create call rather than mid-run. Creating a session does **not** by itself start work or provision the sandbox - without `initial_events` the session is only registered, and the sandbox comes up when the session first needs it (see `shared/managed-agents-core.md` -> Seeding a session with `initial_events`). Max **999 file resources** per session. Multiple GitHub repositories per session are supported. For `type: "memory_store"` resources (persistent cross-session memory - max 8 per session), see `shared/managed-agents-memory.md`.
### File Uploads (input - host -> agent)
Upload a file first via the Files API, then reference by `file_id` + `mount_path`:
```ts
// 1. Upload
const file = await client.beta.files.upload({
file: fs.createReadStream("data.csv"),
purpose: "agent",
});
// 2. Attach as a session resource
const session = await client.beta.sessions.create({
agent: agent.id,
environment_id: envId,
resources: [
{ type: "file", file_id: file.id, mount_path: "/workspace/data.csv" }
],
});
```
**`mount_path` is required** and must be absolute. Parent directories are created automatically. Agent working directory defaults to `/workspace`. Files are mounted read-only - the agent writes modified versions to new paths.
### Session outputs (output - agent -> host)
The agent can write files to `/mnt/session/outputs/` during a session. These are automatically captured by the Files API and can be listed and downloaded afterwards:
```ts
// After the turn completes, list output files scoped to this session:
for await (const f of client.beta.files.list({
scope_id: session.id,
betas: ["managed-agents-2026-04-01"],
})) {
console.log(f.filename, f.size_bytes);
const resp = await client.beta.files.download(f.id);
const text = await resp.text();
}
```
**Requirements:**
- The `write` tool (or `bash`) must be enabled for the agent to create output files.
- Session-scoped `files.list` / `files.download` captures outputs written to `/mnt/session/outputs/`.
- The filter parameter is **`scope_id`** (REST query param `?scope_id=<session_id>`). The SDK's files resource auto-adds only the `files-api-2025-04-14` header, so pass `betas: ["managed-agents-2026-04-01"]` explicitly (or both headers on raw HTTP) - without it the API may reject `scope_id` as an unknown field. Requires `@anthropic-ai/sdk` >= 0.88.0 / `anthropic` (Python) >= 0.92.0 - older versions don't type `scope_id`. The `ant` CLI does **not** expose this flag yet; use the SDK or curl.
- Pass the session ID returned by `sessions.create()` verbatim (e.g. `sesn_011CZx...`) - the API validates the prefix.
- There's a brief indexing lag (~1-3s) between `session.status_idle` and output files appearing in `files.list`. Retry once or twice if empty.
> **Fallback when `scope_id` filtering is unavailable** (older SDK, or endpoint returns an error): send a follow-up `user.message` asking the agent to `read` each file under `/mnt/session/outputs/` and return the contents. The agent streams the file bodies back as `agent.message` text. This works for text files only and costs output tokens - use it to unblock, not as the primary path.
This gives you a bidirectional file bridge: upload reference data in, download agent artifacts out.
### GitHub Repositories
Clones a GitHub repository into the session container during initialization, before the agent begins execution. The agent can read, edit, commit, and push via `bash` (`git`). Multiple repositories per session are supported - add one `resources` entry per repo. Repositories are cached, so future sessions that use the same repository start faster.
Mounting a repository also loads any skills stored in its root `.claude/skills` directory - discovered once per session, from the repository state checked out at session start (cloud sandboxes only). See `shared/managed-agents-tools.md` -> Skills from a GitHub repository.
Repositories are attached for the lifetime of the session - to change which repositories are mounted, create a new session. You **can** rotate a repository's `authorization_token` on a running session via `client.beta.sessions.resources.update(resource_id, {session_id, authorization_token})`; the resource `id` is returned at session creation and by `resources.list()`.
**Fields:**
| Field | Required | Notes |
|---|---|---|
| `type` | Yes | `"github_repository"` |
| `url` | Yes | The GitHub repository URL |
| `authorization_token` | Yes | GitHub Personal Access Token with repository access. **Never echoed in API responses.** |
| `mount_path` | No | Path where the repository will be cloned. Defaults to `/workspace/<repo-name>`. |
| `checkout` | No | `{type: "branch", name: "..."}` or `{type: "commit", sha: "..."}`. Defaults to the repo's default branch. |
**Token permission levels** (fine-grained PATs):
- `Contents: Read` - clone only
- `Contents: Read and write` - push changes and create pull requests
**How auth works:** `authorization_token` is never placed inside the container. `git pull` / `git push` and GitHub REST calls against the attached repository are routed through an Anthropic-side git proxy that injects the token after the request leaves the sandbox. Code running in the container - including anything the agent writes - cannot read or exfiltrate it.
> Important: **To generate pull requests** you also need GitHub **MCP server** access - the `github_repository` resource gives filesystem + git access only. See `shared/managed-agents-tools.md` -> MCP Servers. The PR workflow is: edit files in the mounted repo -> push branch via `bash` (authenticated via the git proxy using `authorization_token`) -> create PR via the MCP `create_pull_request` tool (authenticated via the vault).
**TypeScript:**
```ts
// 1. Create the agent - declare GitHub MCP (no auth here)
const agent = await client.beta.agents.create(
{
name: 'GitHub Agent',
model: 'claude-opus-5',
mcp_servers: [
{ type: 'url', name: 'github', url: 'https://api.githubcopilot.com/mcp/' },
],
tools: [
{ type: 'agent_toolset_20260401', default_config: { enabled: true } },
{ type: 'mcp_toolset', mcp_server_name: 'github' },
],
},
);
// 2. Start a session - attach vault for MCP auth + mount the repo
const session = await client.beta.sessions.create({
agent: agent.id,
environment_id: envId,
vault_ids: [vaultId], // vault contains the GitHub MCP OAuth credential
resources: [
{
type: 'github_repository',
url: 'https://github.com/owner/repo',
authorization_token: process.env.GITHUB_TOKEN, // repo clone token (!= MCP auth)
checkout: { type: 'branch', name: 'main' },
},
],
});
```
**Python:**
```python
import os
agent = client.beta.agents.create(
name="GitHub Agent",
model="claude-opus-5",
mcp_servers=[{
"type": "url",
"name": "github",
"url": "https://api.githubcopilot.com/mcp/",
}],
tools=[
{"type": "agent_toolset_20260401", "default_config": {"enabled": True}},
{"type": "mcp_toolset", "mcp_server_name": "github"},
],
)
session = client.beta.sessions.create(
agent=agent.id,
environment_id=env_id,
vault_ids=[vault_id], # vault contains the GitHub MCP OAuth credential
resources=[{
"type": "github_repository",
"url": "https://github.com/owner/repo",
"authorization_token": os.environ["GITHUB_TOKEN"], # repo clone token (!= MCP auth)
"checkout": {"type": "branch", "name": "main"},
}],
)
```
---
## Files API
Upload and manage files for use as session resources, and download files the agent wrote to `/mnt/session/outputs/`.
| Operation | Method | Path | SDK |
| ---------------- | -------- | ------------------------------------- | --- |
| Upload | `POST` | `/v1/files` | `client.beta.files.upload({ file })` |
| List | `GET` | `/v1/files?scope_id=...` | `client.beta.files.list({ scope_id, betas: ["managed-agents-2026-04-01"] })` |
| Get Metadata | `GET` | `/v1/files/{id}` | `client.beta.files.retrieveMetadata(id)` |
| Download | `GET` | `/v1/files/{id}/content` | `client.beta.files.download(id)` -> `Response` |
| Delete | `DELETE` | `/v1/files/{id}` | `client.beta.files.delete(id)` |
The `scope_id` filter on List scopes the results to files written to `/mnt/session/outputs/` by that session. Without the filter, you get all files uploaded to your account.
# Managed Agents - Events & Steering
## Events
### Sending Events
Send events to a session via `POST /v1/sessions/{id}/events`.
| Event Type | When to Send |
| ------------------------- | --------------------------------------------------- |
| `user.message` | Send a user message |
| `user.interrupt` | Interrupt the agent while it's running |
| `user.tool_confirmation` | Approve/deny a tool call (when `always_ask` policy) |
| `user.custom_tool_result` | Provide result for a custom tool call |
| `user.define_outcome` | Start a rubric-graded iterate loop - see `shared/managed-agents-outcomes.md` |
| `system.message` | Append privileged system-level context for this turn and every turn after it; see § Adding system context mid-session |
#### Adding system context mid-session (`system.message`)
The `system` field on the agent definition sets the top-level system prompt and is fixed for the session's lifetime. A `system.message` event **appends** to the session's system context as a `role: "system"` turn - it does not replace that prompt. The content applies to the accompanying turn and all subsequent turns. Use it for a different persona, revised constraints, or runtime-fetched context that should shape behavior going forward:
```python
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "system.message",
"content": [
{"type": "text", "text": "The user's current timezone is America/New_York."},
],
},
],
)
```
Constraints:
- **Model-gated: Claude Opus 5, Claude Opus 4.8, Claude Sonnet 5, Claude Fable 5.1, and Claude Mythos 5.1.** Only the agent's **primary** model is checked - `system.message` lands on the primary thread only, so subagent models are not considered. On an unsupported primary model the event is rejected with a `model_does_not_support_mid_conversation_system` validation error.
- **While the session is idle with `stop_reason: requires_action`** (blocked on `user.custom_tool_result` / `user.tool_confirmation`), a `system.message` is accepted **only when it trails a tool result event in the same request**. Sent on its own - or alongside a `user.message` - it is rejected until the pending tool events are resolved.
- `content` accepts 1-1000 text items.
### Receiving Events
Three methods:
1. **Streaming (SSE)**: `GET /v1/sessions/{id}/events/stream` - real-time Server-Sent Events. **Long-lived** - the server sends periodic heartbeats to keep the connection alive.
2. **Polling**: `GET /v1/sessions/{id}/events` - paginated event list (query params: `limit` default 1000, `page`). **Returns immediately** - this is a plain paginated GET, not a long-poll.
3. **Webhooks**: Anthropic POSTs session state transitions to your HTTPS endpoint - thin payloads (IDs only), HMAC-signed, Console-registered. See `shared/managed-agents-webhooks.md`.
**No-code inspection - the Console session viewer** (Console sidebar -> **Managed Agents** -> **Sessions**; Developers and Admins only). Point users here for debugging before they parse the stream themselves: a session list (ID, name, status, agent, tokens in/out, cost; filter by status/created, search by ID); a **timeline minimap** with one lane per thread in multiagent sessions; the **transcript** grouped by model request (thinking, tool calls with inputs/results, streaming text) with a **Filter events** box (matches ID, type, tool name, or text; Enter steps between matches) and copy/download-as-JSON (filtered export when a filter is active); and an **Inspector** side panel (toggle with `d`) with five tabs - **Session** (details, metadata, cumulative-cost chart vs. budget), **Events** (raw events in server order, JSON per event, plus a **Deltas** view for messages that streamed while the page was open), **Tools** (every configured tool with call counts, failures, median duration; jump to any call), **Resources** (mounted files, repos, memory stores with per-session memory changes, `/mnt/session/outputs` files, skills under `/workspace/skills`), **Threads** (status, context size, cost per thread; context-size chart for the current thread; switch threads). Deep-link with `?event={event_id}` on the session URL - handy to include in error reports alongside the Console link from `shared/managed-agents-core.md`.
All **persisted** events carry `id`, `type`, and `processed_at` (ISO 8601), set when the event finishes processing. On events you send, `processed_at` is `null` while the event is still queued behind earlier ones - **except** `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result`, which are processed on receipt and echoed back with `processed_at` already populated. The stream-only `event_start` / `event_delta` preview events (see § Live previews) carry only the `id` of the event they preview.
> Warning: **Robust polling (raw HTTP).** If you bypass the SDK and roll your own poll loop, don't rely on `requests` or `httpx` timeouts as wall-clock caps - they're **per-chunk** read timeouts, reset every time a byte arrives. A trickling response (heartbeats, a wedged chunked-encoding body, a misbehaving proxy) can keep the call blocked indefinitely even with `timeout=(5, 60)` or `httpx.Timeout(120)`. Neither library has a "total wall-clock" timeout built in. For a hard deadline: track `time.monotonic()` at the loop level and break/cancel if a single request exceeds your budget (e.g. via a watchdog thread, or `asyncio.wait_for()` around async httpx). **Prefer the SDK** - `client.beta.sessions.events.stream()` and `client.beta.sessions.events.list()` handle timeout + retry sanely.
>
> If `GET /v1/sessions/{id}/events` (paginated) ever hangs after headers, you've likely hit `GET /v1/sessions/{id}/events/stream` by mistake or a server-side stall - report it; don't treat it as a client-config problem.
### Event Types (Received)
Event types use dot notation, grouped by namespace:
| Event Type | Description |
| --- | --- |
| `agent.message` | Agent text output |
| `agent.thinking` | Progress signal that the agent is thinking - it does **not** carry the thinking content |
| `agent.tool_use` | Agent used a built-in tool (`agent_toolset_20260401`) |
| `agent.tool_result` | Result from a built-in tool |
| `agent.mcp_tool_use` | Agent used an MCP tool |
| `agent.mcp_tool_result` | Result from an MCP tool |
| `agent.custom_tool_use` | Agent invoked a custom tool - session goes idle, you respond with `user.custom_tool_result` |
| `agent.thread_context_compacted` | Conversation context was compacted |
| `session.status_idle` | Agent has finished the current task, and is awaiting input. It's either waiting for input to continue working via a `user.message`, blocked awaiting a `user.custom_tool_result` or `user.tool_confirmation`, or paused because the session budget cap was reached. The `stop_reason` attached contains more information about why the Agent has stopped working. |
| `session.status_running` | Session has starting running, and the Agent is actively doing work. |
| `session.status_rescheduled` | Session is (re)scheduling after a retryable error has occurred, ready to be picked up by the orchestration system. |
| `session.status_terminated` | Session ended and is irreversibly unusable - **on completion or on error**, not error-only. |
| `session.updated` | A session update changed at least one field - carries only the changed fields (a budget removal carries `budget: null`) |
| `session.usage` | Snapshot of the session's cumulative usage and tracked list cost - see § Reaching a session budget below |
| `session.error` | Error occurred during processing |
| `span.model_request_start` | Model inference started |
| `span.model_request_end` | Model inference completed |
| `span.outcome_evaluation_start` / `_ongoing` / `_end` | Grader progress for outcome-oriented sessions - see `shared/managed-agents-outcomes.md` |
| `session.thread_created` | Subagent thread spawned (multiagent), or an advisor consultation started (thread name `anthropic.advisor`) - see `shared/managed-agents-multiagent.md` |
| `session.thread_status_running` / `_idle` / `_rescheduled` / `_terminated` | Thread status transitions - mostly seen in multiagent sessions, but a single-agent session's primary thread also emits `_idle` when pausing at a session budget (§ Reaching a session budget). `_idle` carries `stop_reason`. |
| `agent.thread_message_sent` / `_received` | Cross-thread message, carries `to_session_thread_id` / `from_session_thread_id` (multiagent) |
The stream also echoes back user-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`, `user.define_outcome`) - except a `user.interrupt` sent while the session is paused at its budget, which is accepted and ignored and never appears (§ Reaching a session budget).
Stream-only delta preview events (`event_start`, `event_delta`) are the one exception to the `{domain}.{action}` naming convention - see § Live previews below; they never appear in `GET /v1/sessions/{id}/events`.
---
## Live previews
By default, assistant text reaches the stream as buffered `agent.message` events - emitted only after the model request that produced them finishes. **Live previews** let you render that text incrementally while the model is still generating. The buffered `agent.message` is always the authoritative record; a client that ignores previews still receives a complete, correct stream. The wire format is **not** Messages-API streaming: the delta type is `content_delta`, not `content_block_delta`, so Messages-API accumulator code does not carry over unchanged.
**Opt in per stream connection** by adding the `event_deltas[]` query parameter, repeated once per event type to preview. Accepted values: `agent.message`, `agent.thinking` - any other value returns a 400, as does a request with more than 100 values. **Both stream endpoints accept it:** the session-level stream (`GET /v1/sessions/{id}/events/stream`) and each session thread's own stream (`GET /v1/sessions/{sid}/threads/{tid}/stream`). In a shell, quote the URL or percent-encode the brackets as `%5B%5D` - bare `[]` is a glob pattern.
**Previews are thread-scoped.** A connection previews only the thread it is reading. A child thread's previews are delivered on that child's stream and are *never* cross-posted to the session-level stream, whose previews stay scoped to the primary thread. To watch a subagent's text as the model generates it, open that subagent's thread stream - see `shared/managed-agents-multiagent.md`. Run one accumulator instance per connection.
```python
stream = client.beta.sessions.events.stream(
session_id=session.id,
event_deltas=["agent.message"],
)
```
When a previewed event begins, the stream emits an `event_start` carrying the upcoming event's `type` and `id`; for `agent.message` it's followed by `event_delta` events carrying incremental text:
```json
{"type": "event_start", "event": {"type": "agent.message", "id": "sevt_01abc..."}}
{"type": "event_delta", "event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "Here is the summary"}}}
```
`event_start` and `event_delta` have no `id` or `processed_at` of their own - the only identifier they carry is the `id` of the event they preview. For `agent.thinking`, **only** the `event_start` is emitted (a "thinking has started" signal) - no deltas follow, and the buffered `agent.thinking` that concludes the preview carries no thinking content either. It is a progress signal, not a content carrier; there is nothing to read out of it.
**Accumulate-and-reconcile pattern.** Treat the preview as a scratch buffer keyed by `(event_id, index)`. On `event_start`, create an empty entry for the announced `id`. On each `event_delta`, append `delta.content.text` to `(event_id, delta.index)` and render the running text. When the buffered `agent.message` arrives, match it by `id`, **discard the accumulated preview**, and render the message's content instead. The identifiers always line up: `event_start.event.id`, every `event_delta.event_id`, and the buffered event's `id` are the same value. On a normal turn the order is fixed: `session.status_running` -> `span.model_request_start` -> `event_start` -> `event_delta`* -> buffered `agent.message` -> `span.model_request_end`. If the turn errors or is interrupted the buffered event may never arrive, but `span.model_request_end` still does - close any unreconciled preview when you see it. Python/TypeScript/Go SDKs ship an accumulator helper that implements this; in other SDKs apply the manual pattern to the generated event types.
**Two guarantees the pattern relies on:** concatenating a preview's deltas in arrival order, keyed by `(event_id, index)`, yields a *prefix* of `content[index].text` in the buffered event (a prefix, not necessarily the whole text - deltas may be shed under load); and a connection emits at most one `event_start` per `event_id`, with the buffered event as the last thing that connection delivers for that `id`.
**Limitations:**
- **Best effort** - under load the server may shed deltas for an event; you receive a contiguous prefix and then no further deltas for that event. The buffered `agent.message` still arrives complete. Never treat an accumulated preview as final.
- **No replay on reconnect** - deltas are delivered only to the connection that opted in, while it's open; this holds for the session-level stream and each thread stream alike. A connection opened after a model request started receives no deltas for that in-flight event. After a drop, follow the consolidation pattern in § Reconnecting after a dropped stream - the history fetch returns any buffered events emitted during the gap; missed deltas cannot be re-requested.
- **One thread, text only** - previews cover assistant text on the thread the connection is reading. Tool use, tool results, MCP results, and activity on any *other* thread are never previewed on that connection.
- **Never persisted** - `event_start` / `event_delta` exist only on the live SSE stream, never in `GET /v1/sessions/{id}/events` or any thread's event history.
**Troubleshooting:**
| You see | What it means |
| --- | --- |
| Buffered events but no `event_start` / `event_delta` | This connection didn't opt in (`event_deltas[]` is per connection, not per session), or the turn ran on a different thread. List `GET /v1/sessions/{sid}/threads` to find which one ran. |
| 404 on the stream URL | Wrong path or ID, or the request carries no managed-agents beta header - the thread endpoints are beta-gated, so without it they don't exist. The thread path is `/threads/{tid}/stream`, **not** `/threads/{tid}/events/stream` (which doesn't exist) and not `/events/stream` (session level only). |
| 400 naming `event_deltas` | Only `agent.message` and `agent.thinking` are accepted, max 100 values. |
---
## Steering Patterns
Practical patterns for driving a session via the events surface.
### Stream-first ordering
**Open the stream before sending events.** The stream only delivers events that occur *after* it's opened - it does not replay current state or historical events. If you send a message first and open the stream second, early events (including fast status transitions) arrive buffered in a single batch and you lose the ability to react to them in real time.
```ts
// Correct - stream and send concurrently
const [response] = await Promise.all([
streamEvents(sessionId), // opens SSE connection
sendMessage(sessionId, text),
]);
// Wrong - events before stream opens arrive as a single buffered batch
await sendMessage(sessionId, text);
const response = await streamEvents(sessionId);
```
**For full history,** use `GET /v1/sessions/{id}/events` (paginated list) - the stream only gives you live events from connection onward.
### Reconnecting after a dropped stream
**The SSE stream has no replay.** If your connection drops (httpx read timeout, network blip) and you reconnect, you only get events emitted *after* reconnection. Any events emitted during the gap are lost from the stream.
**The consolidation pattern:** on every (re)connect, overlap the stream with a history fetch and dedupe by event ID:
```python
def connect_with_consolidation(client, session_id):
# 1. Open the SSE stream first
stream = client.beta.sessions.events.stream(session_id=session_id)
# 2. Fetch history to cover any gap
history = client.beta.sessions.events.list(
session_id=session_id,
)
# 3. Yield history first, then stream - dedupe by event.id
seen = set()
for ev in history.data:
seen.add(ev.id)
yield ev
for ev in stream:
if ev.id not in seen:
seen.add(ev.id)
yield ev
```
### Message queuing
**You don't have to wait for a response before sending the next message.** User events are queued server-side and processed in order. This is useful for chat bridges where the user sends rapid follow-ups:
```ts
// All three go into one session; agent processes them in order
await sendMessage(sessionId, "Summarize the README");
await sendMessage(sessionId, "Actually also check the CONTRIBUTING guide");
await sendMessage(sessionId, "And compare the two");
// Stream once - agent responds to all three as a coherent turn
```
Events can be sent up to the Session at any time. There is no need to wait on a specific session status to enqueue new events via `client.beta.sessions.events.send()`. One exception: a session paused at its budget (`stop_reason: budget_reached`) accepts only settle events - a `user.message` there is a 400. See § Reaching a session budget.
### Interrupt
A `user.interrupt` event **jumps the queue** (ahead of any pending user messages) and forces the session into `idle`. Exception: while the session is paused at its budget, an interrupt is accepted and ignored - it is never persisted and changes nothing (§ Reaching a session budget). Use this for "stop" / "nevermind" / "cancel" commands:
```ts
await client.beta.sessions.events.send(sessionId, {
events: [{ type: 'user.interrupt' }],
});
```
The agent stops mid-task. It does not see the interrupt as a message - it just halts. Send a follow-up `user` event to explain what to do instead. If an outcome is active, the interrupt also marks `span.outcome_evaluation_end.result: "interrupted"` (see `shared/managed-agents-outcomes.md`) - though not at a budget pause, where the interrupt is accepted and ignored (see § Reaching a session budget).
**The interrupted turn ends with `stop_reason: end_turn`** - the same value a turn that finishes on its own carries. There is no interruption-specific stop reason, so a drain loop can't distinguish the two from `stop_reason` alone; track that you sent the interrupt.
**Against an already-`idle` session an interrupt is normally a no-op.** The exception is a session on a self-hosted environment whose worker failed the claimed work item (a memory-store mount error, for instance): it sits `idle` with `stop_reason: requires_action` and no error event, and `user.interrupt` re-queues the work for the next worker claim (`shared/managed-agents-self-hosted-sandboxes.md` § Memory stores -> Troubleshooting).
**In a multiagent session, omitting `session_thread_id` interrupts every non-archived thread, including the primary** - it is not primary-only. Pass `session_thread_id` to stop one thread. See `shared/managed-agents-multiagent.md`.
> **Note**: Interrupt events may have empty IDs in the current implementation. When troubleshooting, use the `processed_at` timestamp along with surrounding event IDs. (Not applicable to an interrupt sent at the budget cap - that event is never persisted, so there is nothing to locate.)
### Reaching a session budget
A session created with a budget (see `shared/managed-agents-core.md` § Session budgets) pauses instead of overspending. Before every model request the platform checks whether consumed list cost has reached the cap and pauses the thread if it has, and the session goes idle with `stop_reason: budget_reached` rather than terminating. On the stream, the pause arrives as three events, in order:
1. `session.thread_status_idle` with `stop_reason: budget_reached`, for each thread as it pauses. When a thread's final request both crosses the cap and finishes its turn, that thread reports `stop_reason: end_turn` while the session still reports `budget_reached` - key on the **session-level** `stop_reason`, not thread-level ones, to detect the pause.
2. `session.usage` - a snapshot of the session's cumulative usage and tracked list cost.
3. `session.status_idle` with `stop_reason: budget_reached`. The `session.usage` event always immediately precedes this idle.
While at the cap the session accepts **only settle events** (`user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`, `user.interrupt`); anything that starts new work, including `user.message`, is a 400 naming that list. A `user.interrupt` sent while the session is paused at its budget (all threads paused at the cap) is accepted and ignored: it does not appear in the event list and changes nothing. Raise or remove the budget to continue. When one thread waits on a tool ask and another is paused at the cap, the session-level `stop_reason` is `requires_action`, not `budget_reached` - settling the ask doesn't trigger a model request, so respond as usual.
**No event resumes a session paused at its cap.** Update the session's budget instead: change it to a value above the consumed list cost (higher or lower than the old cap), or remove it with `"budget": null`. An accepted update resumes the paused work automatically.
**`session.usage`** carries the session's cumulative token totals, `list_cost` (`{amount, currency}`, rounded to the nearest cent), `active_seconds` (concurrent-thread overlap counted once - the figure runtime cost is priced on), `server_tool_use` counts (`web_search_requests`, and `web_fetch_requests` - informational, currently always 0 since web fetch is not metered), and an echo of the session's `budget` when one is set. It appears in the events list and the session stream - a stream reader sees the final cost of the work that hit the cap without an extra fetch; child threads' own streams do not carry it. The same totals live on the session object's `usage` field, and each thread's own `usage` carries per-thread `list_cost` and `active_seconds` - but per-thread costs do **not** sum to the session total: the session figure additionally includes session running time and each figure is rounded independently, so the session figure is the authoritative one. To enforce a spend limit, set a budget rather than polling usage and interrupting the session yourself - the platform's gate runs before each model request.
### Event payloads
some events carry useful metadata beyond the status change itself:
`session.status_idle` - includes a `stop_reason` field which elaborates on why the session stopped and what type of further action is required by the user.
```json
{
"id": "sevt_456",
"processed_at": "2026-04-07T04:27:43.197Z",
"stop_reason": {
"event_ids": [
"sevt_123"
],
"type": "requires_action"
},
"type": "status_idle"
}
```
`span.model_request_end` contains a `model_usage` field for cost tracking and efficiency analysis:
```json
{
"type": "span.model_request_end",
"id": "sevt_456",
"is_error": false,
"model_request_start_id": "sevt_123",
"model_usage": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 6656,
"input_tokens": 3571,
"output_tokens": 727
},
"processed_at": "2026-04-07T04:11:32.189Z"
}
```
**`agent.thread_context_compacted`** - emitted when the conversation history was summarized to fit context. Includes `pre_compaction_tokens` so you know how much was squeezed:
```json
{
"id": "sevt_abc123",
"processed_at": "2026-03-24T14:05:15.787Z",
"type": "agent.thread_context_compacted"
}
```
### Archive
When done with a session, archive it to free resources:
```ts
await client.beta.sessions.archive(sessionId);
```
> Archiving a **session** is routine cleanup - sessions are per-run and disposable. **Do not generalize this to agents or environments**: those are persistent, reusable resources, and archiving them is permanent (no unarchive; new sessions cannot reference them). See `shared/managed-agents-overview.md` -> Common Pitfalls.
# Managed Agents - Memory Stores
> **Public beta.** Memory stores ship under the `managed-agents-2026-04-01` beta header; the SDK sets it automatically on all `client.beta.memory_stores.*` calls. If `client.beta.memory_stores` is missing, upgrade to the latest SDK release.
Sessions are ephemeral by default - when one ends, anything the agent learned is gone. A **memory store** is a workspace-scoped collection of small text documents that persists across sessions. When a store is attached to a session (via `resources[]`), it is mounted into the container as a filesystem directory; the agent reads and writes it with the ordinary file tools, and a system-prompt note tells it the mount is there.
Every mutation to a memory produces an immutable **memory version** (`memver_...`), giving you an audit trail and point-in-time rollback/redact.
> Warning: **Never store credentials, API keys, or tokens in memory stores.** Memories persist across sessions and are returned verbatim into future contexts - a key written once is replayed into every later session that mounts the store. Use vault `environment_variable` credentials instead (`shared/managed-agents-tools.md` -> Vaults). If a secret has already been written, delete the memory and redact the affected versions (see "Redact a version" below).
## Object model
| Object | ID prefix | Scope | Notes |
| --- | --- | --- | --- |
| Memory store | `memstore_...` | Workspace | Attach to sessions via `resources[]` |
| Memory | `mem_...` | Store | One text file, addressed by `path` (<= 100KB each - prefer many small files) |
| Memory version | `memver_...` | Memory | Immutable snapshot per mutation; `operation` in `created` / `modified` / `deleted` |
## Create a store
`description` is passed to the agent so it knows what the store contains - write it for the model, not for humans.
```python
store = client.beta.memory_stores.create(
name="User Preferences",
description="Per-user preferences and project context.",
)
print(store.id) # memstore_01Hx...
```
Other SDKs: TypeScript `client.beta.memoryStores.create({...})`; Go `client.Beta.MemoryStores.New(ctx, ...)`. See `shared/managed-agents-api-reference.md` -> SDK Method Reference for the full per-language table.
Stores support `retrieve` / `update` / `list` (with `include_archived`, `created_at_{gte,lte}` filters) / `delete` / **`archive`**. Archive makes the store read-only - existing session attachments continue, new sessions cannot reference it; no unarchive.
### Seed with content (optional)
Pre-load reference material before any session runs. `memories.create` creates a memory at the given `path`; if a memory already exists there the call returns `409` (`memory_path_conflict_error`, with the `conflicting_memory_id`). The store ID is the first positional argument.
```python
client.beta.memory_stores.memories.create(
store.id,
path="/formatting_standards.md",
content="All reports use GAAP formatting. Dates are ISO-8601...",
)
```
## Attach to a session
Memory stores go in the session's `resources[]` array alongside `file` and `github_repository` resources (see `shared/managed-agents-environments.md` -> Resources). Memory stores attach at **session create time only** - `sessions.resources.add()` does not accept `memory_store`. Sessions on **self-hosted** environments attach them the same way (and `memory_store` is the *only* resource type those environments accept) - see the self-hosted note below.
```python
session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
resources=[
{
"type": "memory_store",
"memory_store_id": store.id,
"access": "read_write", # or "read_only"; default is "read_write"
"instructions": "User preferences and project context. Check before starting any task.",
}
],
)
```
| Field | Required | Notes |
| --- | --- | --- |
| `type` | Yes | `"memory_store"` |
| `memory_store_id` | Yes | `memstore_...` |
| `access` | - | `"read_write"` (default) or `"read_only"` - enforced at the filesystem level on the cloud mount; on self-hosted sandboxes enforced by the worker's `write`/`edit` tools and by the upload path (see below) |
| `instructions` | - | Session-specific guidance for this store, in addition to the store's `name`/`description`. <= 4,096 chars. |
**Max 8 memory stores per session.** Attach multiple when different slices of memory have different owners or lifecycles - e.g. one read-only shared-reference store plus one read-write per-user store, or one store per end-user/team/project sharing a single agent config.
### How the agent sees it (FUSE mount)
Each attached store is mounted in the session container at `/mnt/memory/<store-name>/`. The agent interacts with it using the standard file tools (`bash`, `read`, `write`, `edit`, `glob`, `grep`) - there are no dedicated memory tools. On cloud sandboxes `access: "read_only"` makes the mount read-only at the filesystem level (on self-hosted sandboxes it is enforced by the worker's `write`/`edit` tools and the upload path - see below); `"read_write"` allows the agent to create, edit, and delete files under it. A short description of each mount (name, path, `instructions`, access) is automatically injected into the system prompt so the agent knows the store exists without you having to mention it.
Writes the agent makes under the mount are persisted back to the store and produce memory versions just like host-side `memories.update` calls.
**Self-hosted sandboxes: a synced local copy, not a live mount.** On a `self_hosted` environment the SDK worker (`EnvironmentWorker` - Python, TypeScript, Go; the `ant` CLI worker does not mount stores) downloads each attached store to the same `/mnt/memory/<store-name>/` path and reconciles it with the store on an interval, so writes are visible to other sessions only after sync, conflicts resolve in favor of the store, and `read_only` is enforced by the worker's tools rather than the filesystem (`bash` can still alter the local copy). Everything else - sync interval, per-session `secret`, host prep, troubleshooting - lives in `shared/managed-agents-self-hosted-sandboxes.md` § Memory stores. Not available on self-hosted environments on Claude Platform on AWS.
## Manage memories directly (host-side)
Use these for review workflows, correcting bad memories, or seeding stores out-of-band.
### List
Returns `Memory | MemoryPrefix` entries - a `MemoryPrefix` (`type: "memory_prefix"`, just a `path`) is a directory-like node when listing hierarchically. Use `path_prefix` to scope (include a trailing slash: `"/notes/"` matches `/notes/a.md` but not `/notes_backup/old.md`) and `depth` to bound the tree walk. Pass `view="full"` to include `content` in each item; the default `"basic"` returns metadata only.
```python
for m in client.beta.memory_stores.memories.list(store.id, path_prefix="/"):
if m.type == "memory":
print(f"{m.path} ({m.content_size_bytes} bytes, sha={m.content_sha256[:8]})")
else: # "memory_prefix"
print(f"{m.path}/")
```
### Read
```python
mem = client.beta.memory_stores.memories.retrieve(memory_id, memory_store_id=store.id)
print(mem.content)
```
`retrieve` defaults to `view="full"` (content included); `view` matters mainly on list endpoints.
### Create vs. update
| Operation | Addressed by | Semantics |
| --- | --- | --- |
| `memories.create(store_id, path=..., content=...)` | **Path** | Create at `path`. `409` (`memory_path_conflict_error`, includes `conflicting_memory_id`) if the path is already occupied. |
| `memories.update(mem_id, memory_store_id=..., path=..., content=...)` | **`mem_...` ID** | Mutate existing memory. Change `content`, `path` (rename), or both. Renaming onto an occupied path returns the same `409 memory_path_conflict_error`. |
```python
mem = client.beta.memory_stores.memories.create(
store.id,
path="/preferences/formatting.md",
content="Always use tabs, not spaces.",
)
client.beta.memory_stores.memories.update(
mem.id,
memory_store_id=store.id,
path="/archive/2026_q1_formatting.md", # rename
)
```
### Optimistic concurrency (precondition on `update`)
`memories.update` accepts a `precondition` so you can read -> modify -> write back without clobbering a concurrent writer. The only supported type is `content_sha256`. On mismatch the API returns `409` (`memory_precondition_failed_error`) - re-read and retry against fresh state.
```python
client.beta.memory_stores.memories.update(
mem.id,
memory_store_id=store.id,
content="CORRECTED: Always use 2-space indentation.",
precondition={"type": "content_sha256", "content_sha256": mem.content_sha256},
)
```
### Delete
```python
client.beta.memory_stores.memories.delete(mem.id, memory_store_id=store.id)
```
Pass `expected_content_sha256` for a conditional delete.
## Audit and rollback - memory versions
Every mutation creates an immutable `memver_...` snapshot. Versions accumulate for the lifetime of the parent memory; `memories.retrieve` always returns the current head, the version endpoints give you history.
| Operation that triggers it | `operation` field on the version |
| --- | --- |
| `memories.create` at a new path | `"created"` |
| `memories.update` changing `content`, `path`, or both (or an agent-side write to the mount) | `"modified"` |
| `memories.delete` | `"deleted"` |
Each version also records `created_by` - an actor object with `type` in `session_actor` / `api_actor` / `user_actor` - and, after redaction, `redacted_at` + `redacted_by`.
### List versions
Newest-first, paginated. Filter by `memory_id`, `operation`, `session_id`, `api_key_id`, or `created_at_gte` / `created_at_lte`. Pass `view="full"` to include `content`; default is metadata-only.
```python
for v in client.beta.memory_stores.memory_versions.list(store.id, memory_id=mem.id):
print(f"{v.id}: {v.operation}")
```
### Retrieve a version
```python
version = client.beta.memory_stores.memory_versions.retrieve(
version_id, memory_store_id=store.id
)
print(version.content)
```
### Redact a version
Scrubs content from a historical version while preserving the audit trail (actor + timestamps). Clears `content`, `content_sha256`, `content_size_bytes`, and `path`; everything else stays. Use for leaked secrets, PII, or user-deletion requests.
```python
client.beta.memory_stores.memory_versions.redact(version_id, memory_store_id=store.id)
```
## Endpoint reference
See `shared/managed-agents-api-reference.md` -> Memory Stores / Memories / Memory Versions for the full HTTP method/path tables. Raw HTTP base path:
```
POST /v1/memory_stores
POST /v1/memory_stores/{memory_store_id}/archive
GET /v1/memory_stores/{memory_store_id}/memories
PATCH /v1/memory_stores/{memory_store_id}/memories/{memory_id}
GET /v1/memory_stores/{memory_store_id}/memory_versions
POST /v1/memory_stores/{memory_store_id}/memory_versions/{version_id}/redact
```
For cURL examples and the CLI (`ant beta:memory-stores ...`), WebFetch the Memory URL in `shared/live-sources.md` -> Managed Agents.
# Managed Agents - Multiagent Sessions
A coordinator agent can delegate to other agents within one session. All agents **share the container and filesystem**; each runs in its own **thread** - a context-isolated event stream with its own conversation history, model, system prompt, tools, MCP servers, and skills (from that agent's own config). Threads are persistent: the coordinator can send a follow-up to a subagent it called earlier and that subagent retains its prior turns.
The SDK sets the `managed-agents-2026-04-01` beta header automatically on all `client.beta.{agents,sessions}.*` calls; no additional header is required for multiagent.
---
## When to use it - start with `self`, then add cheaper workers
**If the agent's work splits into independent pieces** - several sources to research, many files or records to process, anything shaped like "look into N things, then summarize" - or one piece would fill its context with reading, **use a multiagent session instead of one long single-threaded loop.** Each delegated piece runs in its own thread with a fresh context window, threads run in parallel in the same container, and only each subagent's report comes back, so the coordinator's context stays small. There is no orchestration code to write: the coordinator is given delegation tools automatically and decides when to use them, and your client still creates one session and reads one stream.
**Step 1 - the smallest useful roster is the agent itself.** Add a `multiagent` block whose only entry is `{"type": "self"}`. The coordinator can then hand self-contained sub-tasks to copies of itself - same model, system prompt, and tools, minus the ability to delegate further - and combine what they report. Nothing else changes.
```python
agent = client.beta.agents.create(
name="Research assistant",
description="Researches a question end to end. A copy can be spawned to own one well-scoped sub-question.",
model="claude-opus-5",
system="You are a research assistant. When a request splits into independent sub-questions, delegate each to a copy of yourself, one self-contained task per copy, then verify and combine their reports.",
tools=[{"type": "agent_toolset_20260401"}],
multiagent={"type": "coordinator", "agents": [{"type": "self"}]}, # the only change vs. a single agent
)
session = client.beta.sessions.create(agent=agent.id, environment_id=env.id) # unchanged
```
**Step 2 - move the reading-heavy work to a cheaper model.** Delegated research work is mostly searching, reading, and extracting: many input tokens, little hard reasoning. Create a second agent on a smaller model with a narrow `system` prompt and only the tools it needs, and list it next to `self`. A roster entry is only a reference: the worker runs on its own `model`, `system`, and `tools`, and its tokens are billed at its own model's rates. The large model spends its tokens on planning, checking, and synthesis; the small model does the bulk reading.
```python
worker = client.beta.agents.create(
name="Web researcher",
description="Fast, low-cost, read-only researcher. Give it one well-scoped question; it searches, reads, and reports findings with sources.",
model="claude-haiku-4-5",
system="Answer exactly the question you are given. Search and read as much as you need, then report concise findings with a source URL or file path for every claim.",
tools=[{
"type": "agent_toolset_20260401",
"default_config": {"enabled": False},
"configs": [{"name": n, "enabled": True} for n in ("read", "glob", "grep", "web_fetch", "web_search")],
}],
)
lead = client.beta.agents.create(
name="Research lead",
description="Plans and synthesizes research. A copy can be spawned to own one large sub-analysis.",
model="claude-opus-5",
system="Plan the work. Delegate each independent, reading-heavy question to Web researcher, one self-contained task per spawn, several in parallel. Keep verification and the final synthesis for yourself; spawn a copy of yourself only for a sub-analysis that needs your full capability.",
tools=[{"type": "agent_toolset_20260401"}],
multiagent={"type": "coordinator", "agents": [worker.id, {"type": "self"}]},
)
```
**Step 3 - add dedicated specialists.** When the sub-tasks call for different skills, give each its own agent - its own model, a narrow `system` prompt, and only the tools it needs - and roster them by ID next to `self`. Here the lead makes a change itself, sends the same review brief to several read-only reviewer threads for independent passes (one rostered agent can be spawned many times), and hands a test writer a self-contained brief; it then de-duplicates the findings, checks each against the code, and keeps the fix and the summary for itself.
```python
reviewer = client.beta.agents.create(
name="Concurrency reviewer",
description="Read-only reviewer for race conditions, deadlocks, lost updates, and retry/idempotency bugs. Give it the changed file paths and the invariants that must hold; it reports findings with file:line evidence. Spawn several on the same change for independent reviews.",
model="claude-sonnet-5",
system="Review only the files you are pointed at. Look for concurrency bugs: unsynchronized shared state, lock ordering, non-atomic read-modify-write, retries without idempotency. Report each finding as file:line, the interleaving that triggers it, and a suggested fix; say plainly if you found none.",
tools=[{"type": "agent_toolset_20260401", "default_config": {"enabled": False},
"configs": [{"name": n, "enabled": True} for n in ("read", "glob", "grep")]}],
)
test_writer = client.beta.agents.create(
name="Test writer",
description="Writes and runs tests. Give it the module path, the behavior to pin down, and the test command; it adds test files, runs them, and reports results with output.",
model="claude-sonnet-5",
system="Write focused tests for the behavior you are given, run them with the command you are given, and report pass/fail, the relevant output, and the paths of files you added. Do not edit non-test code; if the code under test looks wrong, report that instead.",
tools=[{"type": "agent_toolset_20260401", "default_config": {"enabled": True},
"configs": [{"name": n, "enabled": False} for n in ("web_fetch", "web_search")]}],
)
lead = client.beta.agents.create(
name="Engineering lead",
description="Plans and makes code changes and integrates specialist reports. A copy can be spawned to own one independent change.",
model="claude-opus-5",
system="Make the change yourself. Then, in parallel, send the changed paths and invariants to three Concurrency reviewers and the module path and test command to Test writer. Merge and de-duplicate the reviewers' findings, check each against the code before acting on it, fix, and have Test writer re-run. Keep design decisions and the final summary for yourself.",
tools=[{"type": "agent_toolset_20260401"}],
multiagent={"type": "coordinator", "agents": [reviewer.id, test_writer.id, {"type": "self"}]},
)
```
The same shape fits a pipeline of different specialists: a fast document extractor (for example on Claude Haiku 4.5) that writes one JSON file per input document, a verifier that checks each file against its source, and a lead that applies the corrections and writes the final table to `/mnt/session/outputs/`. Put the input and output paths in every task: threads share the container's filesystem, not each other's conversation.
- **Good fits:** parallel research across sources; reading large amounts of material without filling the coordinator's context; specialists with narrow prompts and tool sets rather than one agent carrying every tool. **Poor fit:** a small single-step task - every delegation costs a round-trip and a re-briefing.
- **Write `name` and `description` for the coordinator to read.** The coordinator chooses whom to spawn from each roster entry's name and description (the `self` entry is listed under the coordinator's own name), so say what each agent is good at and what to hand it. Names must be unique across the roster; don't name an agent `self`.
- **Say how to delegate in the coordinator's `system` prompt** - what to hand off and to whom, how many at once, what to keep for itself, and what is too small to be worth delegating (the *Delegating to subagents* sample prompt in `shared/model-migration.md` is a starting point). Subagents see none of the coordinator's conversation, so each task must carry the paths, constraints, and report format it needs. Spawning returns immediately; the subagent's report arrives in a later coordinator turn.
- **Web tool domain lists layer, never widen.** A roster agent's `web_search` / `web_fetch` calls are bound by its own `allowed_domains` / `blocked_domains`, by those of every agent that called it, and by the coordinator's current lists (allow-lists intersect, block-lists union). Keep each roster agent's allow-list inside the coordinator's - disjoint lists leave the tool present but every call fails `url_not_allowed`. See `shared/managed-agents-tools.md` § Web search & web fetch settings.
- **Limits:** 1-20 roster entries (at most one `self`; each rostered agent can be spawned many times), one level of delegation (a roster member must not have its own `multiagent`), and at most 25 concurrent threads per session - archive finished threads if a long session needs more (see *Interrupting and archiving threads* below).
The sections below are the reference for rosters, threads, events, and client-side handling; the platform guide is `https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration.md`.
---
## Declare the roster on the coordinator
`multiagent` is a **top-level field** on `agents.create()` / `agents.update()` - **not** a `tools[]` entry. `agents` lists 1-20 roster entries. Nothing changes on `sessions.create()` - the roster is resolved from the coordinator's config.
```python
orchestrator = client.beta.agents.create(
name="Engineering lead",
model="claude-opus-5",
system="You coordinate engineering work. Delegate code review to the reviewer and test writing to the test agent.",
tools=[{"type": "agent_toolset_20260401"}],
multiagent={
"type": "coordinator",
"agents": [
reviewer.id, # bare string - latest version
{"type": "agent", "id": test_writer.id, "version": 4}, # pinned version
{"type": "self"}, # the coordinator itself
],
},
)
session = client.beta.sessions.create(agent=orchestrator.id, environment_id=env.id)
```
| Roster entry | Shape | Notes |
|---|---|---|
| String shorthand | `"agent_abc123"` | References the latest version of a stored agent. |
| Agent reference | `{type: "agent", id, version?}` | Omit `version` to pin the latest at coordinator save time. |
| Self | `{type: "self"}` | The coordinator can spawn copies of itself. |
| Advisor | `{type: "advisor", model}` | A model the session's primary thread can consult mid-turn. At most one per roster. See § Advisor below. |
If the session was created with `agent_with_overrides` (see `shared/managed-agents-core.md` -> Override agent configuration for a session), those overrides apply to the **coordinator and its `self` copies**. Roster agents referenced by ID always use their own as-created configuration - overrides do not propagate to them.
The coordinator's thread receives delegation tools for working the roster: `list_agents` (see the roster) and `send_to_agent` (task or message a member). Up to **20 unique agents** in the roster; the coordinator may spawn **multiple copies** of each. **One level of delegation only** - and it is enforced rather than silently flattened: rostering an agent that itself carries a `multiagent.agents` roster fails the create or update with a validation error.
**Inference geo pins must be roster-uniform.** When agents pin an inference geography (`model.inference_geo` - see `shared/managed-agents-core.md` § Pinning inference geography), the coordinator's pin and every roster member's must all be the same value or all be unset. A mismatched roster is a 400 validation error, both when the agent is saved and when a session-create `model` override changes any of the pins.
---
## Threads
The session-level event stream is the **primary thread** - it shows the coordinator's trace plus a condensed view of subagent activity (thread status transitions and cross-thread messages, not every subagent tool call). Drill into a specific subagent via the per-thread endpoints:
| Operation | HTTP | SDK (`client.beta.sessions.threads.*`) |
|---|---|---|
| List threads | `GET /v1/sessions/{sid}/threads` | `.list(session_id)` |
| Retrieve one | `GET /v1/sessions/{sid}/threads/{tid}` | `.retrieve(thread_id, session_id=...)` |
| Archive | `POST /v1/sessions/{sid}/threads/{tid}/archive` | `.archive(thread_id, session_id=...)` |
| List thread events | `GET /v1/sessions/{sid}/threads/{tid}/events` | `.events.list(thread_id, session_id=...)` |
| Stream thread events | `GET /v1/sessions/{sid}/threads/{tid}/stream` | `.events.stream(thread_id, session_id=...)` |
Each `SessionThread` carries `id`, `status` (`running` | `idle` | `rescheduling` | `terminated`), `agent` (a resolved snapshot of the agent config - `id`, `name`, `model`, `system`, `tools`, `skills`, `mcp_servers`, `version` - except advisor threads, whose `agent` is the two-field advisor form `{"type": "advisor", "model": ...}` - see § Advisor), `parent_thread_id` (null for the primary thread, which is included in the list), `archived_at`, and optional `stats`/`usage`. Per-thread `usage.list_cost` figures do **not** sum to the session total - the session figure additionally includes session running time and each figure is rounded independently; the session-level `usage.list_cost` is authoritative. **Session status aggregates thread statuses** - if any thread is `running`, `session.status` is `running`. Max **25 concurrent threads** (advisor threads are exempt - see § Advisor). When draining a per-thread stream, break on `session.thread_status_idle` (and check its `stop_reason` as you would for the session-level idle).
**A session budget is one shared cap across all threads** - no per-thread caps. Each thread's consumption is priced at its own served model, and threads pause independently (`stop_reason: budget_reached`) as the shared cap is reached; one thread can pause while another finishes its in-flight request. A thread waiting on `requires_action` outranks the cap at the session level. See `shared/managed-agents-core.md` § Session budgets.
---
## Multiagent events (on the session stream)
| Event | Payload highlights | Meaning |
|---|---|---|
| `session.thread_created` | `session_thread_id`, `agent_name` | A new thread was created. |
| `session.thread_status_running` | `session_thread_id`, `agent_name` | Thread started activity. |
| `session.thread_status_idle` | `session_thread_id`, `agent_name`, **`stop_reason`** | Thread is awaiting input - or paused at the session's shared budget (`stop_reason: budget_reached`). Inspect `stop_reason` (same shape as `session.status_idle.stop_reason`). |
| `session.thread_status_rescheduled` | `session_thread_id`, `agent_name` | Thread is rescheduling after a retryable error. |
| `session.thread_status_terminated` | `session_thread_id`, `agent_name` | Thread ended - completed its work and self-terminated (advisor consultation threads - see § Advisor), was archived, or hit a terminal error. |
| `agent.thread_message_sent` | `to_session_thread_id`, `to_agent_name`, `content` | *This* thread sent a message to another thread. On the primary stream: the coordinator sent a task or follow-up to an agent. |
| `agent.thread_message_received` | `from_session_thread_id`, `from_agent_name`, `content` | A message arrived on *this* thread from another. On the primary stream: an agent sent a report or question to the coordinator. |
> **Direction is relative to the thread whose stream carries the event**, not to the coordinator. The same delegated task is an `agent.thread_message_sent` on the primary stream and an `agent.thread_message_received` on the child's own stream. Reading `_received` as "a subagent finished" is wrong once you're reading a child stream.
---
## Previewing a subagent's text
Each thread's stream accepts the same `event_deltas[]` parameter as the session-level stream, so you can watch a subagent's text as the model generates it:
```
GET /v1/sessions/{sid}/threads/{tid}/stream?event_deltas%5B%5D=agent.message
```
**Previews are thread-scoped.** A child's previews are delivered only on that child's stream and never cross-posted to the session-level stream, whose previews stay scoped to the primary thread. So watching a subagent live means opening its thread stream - the session stream will not show it, no matter what you pass.
> Warning: **Only plain assistant text previews.** A subagent's *reply to its coordinator* rides `agent.thread_message_sent` and is never previewed. A worker that does nothing but report back therefore streams no deltas at all, even with a correct opt-in on the right thread. To get a live preview out of a subagent, its prompt has to make it write the answer as a plain assistant message in its own thread first, and only then report to the coordinator. Run one accumulator per connection, and exit the read loop on `session.thread_status_idle`. Opt-in, accumulate, and reconcile details: `shared/managed-agents-events.md` -> Live previews.
---
## Advisor
An `{"type": "advisor", "model": "<model id>"}` roster entry gives the session's **primary thread** an advisor: a model it can consult mid-turn for strategic guidance (planning an approach, getting unstuck, reviewing work before finishing). The entry has exactly two fields - `type` and `model` - and can sit alongside any other roster forms; a roster with no other entries works too. The advisor is also available as a server tool on the Messages API (`advisor_20260301` - see `shared/tool-use-concepts.md` -> Advisor); the Managed Agents surface differs in configuration and delivery: the roster entry has **no `max_uses`, `max_tokens`, or `caching` fields**, and advice arrives through thread events rather than `advisor_tool_result` blocks.
```python
agent = client.beta.agents.create(
name="Backend engineer",
model="claude-sonnet-5",
system="You implement backend features end to end.",
multiagent={
"type": "coordinator",
"agents": [{"type": "advisor", "model": "claude-opus-5"}],
},
)
```
(Claude Opus 5 is the default advisor choice. It is a redacted advisor - the agent reads its advice server-side, but the client sees `[{"type": "redacted"}]`; see *Plaintext vs redacted delivery* below. For client-readable advice, a plaintext advisor such as `claude-opus-4-8` is valid only when the agent's own model is `claude-opus-4-8` or below - agents on Claude Opus 5, Claude Fable 5.1, or Claude Mythos 5.1 can only pair with redacted advisors, so client-readable advice is not available for them (pairing table: `shared/tool-use-concepts.md`).)
**Rules:**
- **At most one advisor entry per roster.** The entry occupies the reserved roster name `anthropic.advisor` - a roster that also lists a member literally named `anthropic.advisor` is a 400. In responses, the advisor entry is echoed **last** in the roster regardless of submitted position.
- **Pairing is validated at agent save:** the advisor model must meet a minimum capability bar, and the agent's own model must not be more capable than its advisor (equals can pair). Invalid pairing -> 400. The valid pairs mirror the Messages advisor tool's executor<->advisor table (`shared/tool-use-concepts.md`).
- **Only the primary thread consults it.** The advisor is not a roster agent: invisible to the coordinator's `list_agents` tool, unreachable via `send_to_agent`, and roster agents cannot consult it.
**How consultations work.** Each consultation runs as a platform-spawned thread named `anthropic.advisor` that terminates itself when done; the advice is delivered to the primary thread as an `agent.thread_message_received` event. Typical event order (the reserved name rides `agent_name` on lifecycle events and `from_agent_name` on the delivery):
1. `session.thread_created`
2. `session.thread_status_running`
3. `agent.thread_message_received` - the advice
4. `session.thread_status_idle` (`stop_reason: end_turn`)
5. `session.thread_status_terminated`
No `agent.tool_use` and no `agent.thread_message_sent` are emitted for a consultation, and **the advice delivery is not guaranteed to precede the advisor thread's idle/terminated events** - don't treat those as "advice already delivered."
**Plaintext vs redacted delivery.** Whether your client can read the advice is the advisor model's policy, mirroring the Messages advisor tool's result variants: models that return plaintext there deliver readable text content here; models that return redacted results deliver `[{"type": "redacted"}]` as the message content on every client surface, while the agent still reads the full advice server-side. Advisor thinking is never surfaced. Clients cannot send `redacted` blocks themselves - an event containing one is a 400.
**Failure and interruption.** A failed consultation - or one abandoned via a `user.interrupt` carrying the advisor thread's `session_thread_id` - never fails the agent's turn: the agent continues after a generic notice. A session-level `user.interrupt` during a consultation halts the whole session as usual (every thread, primary included), terminating the advisor thread with no advice delivered.
**Threads, billing, caching.** Advisor threads are **exempt from the 25-concurrent-thread limit**. They appear in the session's thread list with `agent` set to the advisor form as configured (`{"type": "advisor", "model": ...}`) and `parent_thread_id` set to the primary thread. Consultations are billed at the advisor model's rates; their tokens appear in the advisor thread's usage and the session's totals. Advisor-side prompt caching is automatic - nothing to configure.
**Removing the advisor:** update the agent with a roster that omits the entry; if the advisor is the roster's only entry, clear the roster with `"multiagent": null`.
---
## Tool permissions and custom tools from subagent threads
When a subagent needs your client (an `always_ask` confirmation, or a custom tool result), the request is **cross-posted to the primary thread** with `session_thread_id` identifying the originating thread - so you only need to watch the session stream. Reply with `user.tool_confirmation` (carrying `tool_use_id`) or `user.custom_tool_result` (carrying `custom_tool_use_id`), and **echo the `session_thread_id` from the originating event** (the SDK param type and docstring expect it). The server also routes by the tool-use ID, so the echo is belt-and-suspenders rather than load-bearing - but include it.
```python
for event_id in stop.event_ids:
pending = events_by_id[event_id]
confirmation = {
"type": "user.tool_confirmation",
"tool_use_id": event_id,
"result": "allow",
}
if pending.session_thread_id is not None:
confirmation["session_thread_id"] = pending.session_thread_id
client.beta.sessions.events.send(session.id, events=[confirmation])
```
The same pattern applies to `user.custom_tool_result`.
---
## Interrupting and archiving threads
- **`user.interrupt` without `session_thread_id` interrupts every non-archived thread in the session, including the primary** - it is not a primary-only stop. Pass `session_thread_id` to target one thread.
- **Against a child thread blocked on `requires_action`**, the interrupt closes each pending tool call with an *error* tool result (`"Tool execution was interrupted before completion. Please retry."`) and re-emits `session.thread_status_idle` with `stop_reason: end_turn` directly - the model is not sampled. Against a thread already `idle`, the interrupt is a no-op - with one exception: a session on a self-hosted environment whose worker failed the claimed work item (e.g. a memory-store mount error) sits `idle`, and a `user.interrupt` re-queues that work so the next worker claim retries (`shared/managed-agents-self-hosted-sandboxes.md` § Memory stores -> Troubleshooting).
- **Archive requires the thread to be idle, and `requires_action` counts as idle** - a thread parked on a pending tool call can be archived directly. Only a *running* thread must be interrupted first.
---
## Pitfalls
- **Don't put the roster on `sessions.create()` or in `tools[]`.** `multiagent` is a top-level agent field; update the coordinator, then start a session that references it.
- **Don't assume shared context.** Threads share the filesystem but not conversation history or tools. If the coordinator needs a subagent to act on something, it must say so in the delegated message (or write it to disk).
- **Depth > 1 is a validation error.** Rostering an agent that itself carries a `multiagent.agents` roster fails the create or update - only the session's coordinator delegates.
For per-language bindings beyond Python, WebFetch `https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration.md` (see `shared/live-sources.md`).
# Managed Agents - Onboarding Flow
> **Invoked via `/claude-api managed-agents-onboard`?** You're in the right place. Run the interview below - don't summarize it back to the user, ask the questions.
Claude Managed Agents is a hosted agent: Anthropic runs the agent loop and provisions a sandboxed container per session where the agent's tools execute (or your own worker, with a `self_hosted` environment - see `shared/managed-agents-self-hosted-sandboxes.md`). You supply an **agent config** (tools, skills, model, system prompt - reusable, versioned) and an **environment config** (the sandbox - reusable across agents). Each run is a **session**.
The flow is four beats - **describe -> agent -> environment -> session** - the same arc as the Console quickstart, and the same philosophy: **value before credentials**. The user goes from idea to a runnable session before any auth ask; each credential is *flagged* at the moment the design makes it relevant (§2) and *collected* once, at session setup (§4), where it binds (`sessions.create()`) and gets exercised (smoke-test). Read `shared/managed-agents-core.md` alongside this - it has full detail for each knob; this doc is the interview script.
---
## 1. Describe the task
**Open with a one-breath signpost and a single open prompt - don't guess, don't questionnaire.** In your own words:
> Managed Agents is hosted - Anthropic runs the agent loop, the sandbox, and the infrastructure; you just define the agent. We'll do this in three moves: the agent, the environment it runs in, then a live test session. So: describe the agent you want - what should it do, and what kicks it off (a person, an event, a schedule)?
Let them answer in full before configuring anything.
## 2. Configure the agent - propose, don't interrogate
Their description does the interview's work. Draft the agent config from it and **present it as a proposal with your suggestions inline** - the user reacts to a concrete config instead of answering a question list. At most one batched follow-up for true gaps. Suggest where the description gives you an opening:
- **Tools** - enable the full prebuilt toolset by default (`agent_toolset_20260401`: `bash`, `read`, `write`, `edit`, `glob`, `grep`, `web_fetch`, `web_search`). **Suggest MCP servers** for any third-party service the job names (GitHub, Linear, Slack, ...) - and flag the credential each one implies as you suggest it ("Linear MCP -> you'll need a Linear API token at kickoff"), so §4's auth step is a formality, not a surprise. Collection itself waits for §4. Custom tools only if the user's own app must answer calls (name, description, input schema - their handler code is theirs; don't generate it).
- **Skills** - **suggest** prebuilt `xlsx`/`docx`/`pptx`/`pdf` when the job produces those artifacts; custom by `skill_id` (max 20 total per agent, prebuilt + custom combined).
- **Outcome** - if the description implies checkable "done" criteria (or you can elicit them in the follow-up: not "a good report" but "a CSV with a numeric `price` column per SKU"), **suggest an Outcome kickoff** - the harness grades and iterates against a rubric (`shared/managed-agents-outcomes.md`).
- **On-hand resources** - repos on disk (`github_repository`: URL, optional `mount_path`/`checkout`; token comes in §4), files to seed (Files API upload -> `{type: "file", file_id, mount_path}`; read-only), if the job references them.
- **Model** - default `claude-opus-5`; `claude-fable-5-1` for the hardest long-horizon work (`shared/model-migration.md` -> Migrating to Claude Fable 5.1).
> Important: **PR creation needs the GitHub MCP server too** - a `github_repository` mount is filesystem-only. Edit in the mount -> push branch via `bash` -> open the PR via the MCP `create_pull_request` tool.
Full detail per knob: `shared/managed-agents-tools.md` (toolset, MCP, custom tools, skills), `shared/managed-agents-environments.md` (repos, files).
## 3. Environment
Usually zero or one question:
- **Reuse or create?** Environments are shared across agents - check for an existing one first.
- **Networking** - default unrestricted egress. Switch to `limited` only if the user wants egress control - then set `allow_mcp_servers: true` or list every MCP server domain in `allowed_hosts`, or those tools fail silently.
- **Suggest `self_hosted`** when the signals are there: tools must run on their own infra, secrets can't leave it, or they need binaries/data the cloud container won't have (`shared/managed-agents-self-hosted-sandboxes.md`; on Claude Platform on AWS the worker authenticates with IAM instead of an environment key and sessions there can't attach memory stores). Otherwise `cloud` - don't raise it unprompted for simple jobs.
## 4. Session - auth, then test run
**Auth happens here - collect the credentials flagged in §2, now that the config is settled:** a vault (existing or `vaults.create()`) + `vaults.credentials.create()` for each MCP server declared in §2, `environment_variable` credentials for API keys the job uses (substituted at egress; the sandbox sees a placeholder), and the `authorization_token` for each repo mount. Credentials are write-only; MCP credentials match servers by URL and auto-refresh. See `shared/managed-agents-tools.md` -> Vaults.
**Silent viability gate - run this yourself before emitting anything; surface only the gaps.** Walk the job clause by clause: every verb maps to an enabled tool or MCP server ("open a PR" -> GitHub MCP, not just the mount); every MCP server and repo mount has its credential from the auth step; every external host is reachable under the networking choice; every file/repo/dataset the job references is mounted; "done" is checkable. If something's missing, say so and resolve it - don't emit a config you already know is under-resourced.
**Kickoff - pick one, never both:**
- `user.message` - conversational.
- `user.define_outcome` + rubric - when §2 settled on an Outcome; the harness iterates and grades until the rubric passes.
- **Scheduled shape?** Skip per-session kickoff entirely - create a **deployment** (`deployments.create()` with `schedule` + `initial_events`); each firing creates the session autonomously. See `shared/managed-agents-scheduled-deployments.md`.
Mechanics to bake into the runtime code: session creation resolves resources (a bad mount surfaces there, before tokens) but does not itself provision the sandbox; open the event stream *before* sending the kickoff; break on `session.status_terminated`, or `session.status_idle` with any non-`requires_action` `stop_reason` - terminal, or `budget_reached`, which is not terminal (only a budget change/removal resumes it) (`shared/managed-agents-client-patterns.md` Pattern 5); usage lands on `span.model_request_end`; artifacts land in `/mnt/session/outputs/` (`files.list({scope_id: session.id, ...})`).
## 5. Integrate - emit the code
Go straight from the last answer to the code - no preamble, no lecture about setup-vs-runtime; the two-block structure shows it. Generate **two clearly-separated blocks**:
**Block 1 - Setup (run once, store the IDs).** Prefer **YAML files + `ant` CLI** - agents and environments are version-controlled definitions users should check in and apply from CI:
1. `<name>.agent.yaml` (flat: `name`, `model`, `system`, `tools`, `mcp_servers`, `skills`) and `<name>.environment.yaml`
2. ```sh
AGENT_ID=$(ant beta:agents create < <name>.agent.yaml --transform id -r)
ENV_ID=$(ant beta:environments create < <name>.environment.yaml --transform id -r)
# CI sync: ant beta:agents update --agent-id "$AGENT_ID" --version N < <name>.agent.yaml
```
SDK fallback if the user asks - and **required on Claude Platform on AWS**, where auth is SigV4 and the `ant` CLI has no SigV4 mode (use the platform client from `shared/claude-platform-on-aws.md`): label it `# ONE-TIME SETUP - run once, save the IDs` and call `environments.create()` -> `agents.create()`.
> Warning: **Deployments are newer than the rest of the MA surface.** Before emitting `ant beta:deployments ...` or `client.beta.deployments` / `client.beta.deployment_runs` calls, verify the user's installed CLI/SDK exposes them (`ant beta:deployments --help`; `hasattr(client.beta, "deployments")`). If not, emit raw HTTP against `POST /v1/deployments` with the `managed-agents-2026-04-01` beta header (plus `oauth-2025-04-20` when authenticating with a Bearer token from `ant auth print-credentials`), and leave an upgrade note marking what simplifies to SDK calls.
**Scheduled shape? The deployment is setup, not runtime.** Create it in Block 1, after the agent/environment IDs exist (`deployments.create()` with `schedule` + `initial_events`). Block 2 is then **not** a session loop - there is no per-run kickoff to send. Emit instead: a manual-run trigger (`POST /v1/deployments/{id}/run`) so the user can test now rather than wait for the first firing - the manual run doubles as the smoke test - plus a fetch helper (latest `deployment_runs` entry -> `session_id` -> Console URL + `files.list(scope_id=session_id)` for the artifacts).
**Block 2 - Runtime (every invocation; conversational and Outcome shapes).** SDK code in the detected language (Python/TS/cURL - SKILL.md -> Language Detection); don't emit shell loops here:
1. Load `agent_id` + `env_id` from config/env
2. `sessions.create(agent=AGENT_ID, environment_id=ENV_ID, resources=[...], vault_ids=[...])`, then print the Console URL so the user can watch live: `https://platform.claude.com/workspaces/default/sessions/{session.id}` (swap `default` for their workspace slug)
3. **Smoke-test when the job depends on MCP servers, credentials, or locked-down hosts** - those failures don't surface at `sessions.create()`, only on first use. One cheap probe turn ("Confirm you can reach <service> and list 1-2 items; don't start the task"), verify, then send the real kickoff. Skip when there are no external dependencies.
4. Open stream -> send the §4 kickoff -> loop with the terminal gate from §4.
> Warning: **Never emit `agents.create()` and `sessions.create()` in the same unguarded block** - that teaches creating a new agent per run, the #1 anti-pattern. Single-script requests: wrap creation in `if not os.getenv("AGENT_ID"):`.
Pull exact syntax from `{lang}/managed-agents/README.md` for your detected language (cURL and C#: use `curl/managed-agents.md` as the wire-level reference). Don't invent field names.
# Managed Agents - Outcomes
An **outcome** elevates a session from *conversation* to *work*: you state what "done" looks like, and the harness runs an iterate -> grade -> revise loop until the artifact meets the rubric, hits `max_iterations`, or is interrupted. A separate **grader** (independent context window) scores each iteration against your rubric and feeds per-criterion gaps back to the agent.
The SDK sets the `managed-agents-2026-04-01` beta header automatically on all `client.beta.sessions.*` calls; no additional header is required for outcomes.
---
## The `user.define_outcome` event
Outcomes are not a field on `sessions.create()`. You create a normal session, then send a `user.define_outcome` event. The agent starts working on receipt - **do not also send a `user.message`** to kick it off.
You can collapse both calls into one by passing a single `user.define_outcome` in the session's `initial_events` array - same event, same rules, one round trip (see `shared/managed-agents-core.md` -> Seeding a session with `initial_events`). More than one `user.define_outcome` in that array, or one without a `rubric`, rejects the whole create with a 400.
```python
session = client.beta.sessions.create(
agent=AGENT_ID,
environment_id=ENVIRONMENT_ID,
title="Financial analysis on Costco",
)
client.beta.sessions.events.send(
session_id=session.id,
events=[
{
"type": "user.define_outcome",
"description": "Build a DCF model for Costco in .xlsx",
"rubric": {"type": "text", "content": RUBRIC_MD},
# or: "rubric": {"type": "file", "file_id": rubric.id}
"max_iterations": 5, # optional; default 3, max 20
}
],
)
```
| Field | Type | Notes |
|---|---|---|
| `type` | `"user.define_outcome"` | |
| `description` | string | The task. This is what the agent works toward - no separate `user.message` needed. |
| `rubric` | `{type: "text", content}` \| `{type: "file", file_id}` | **Required.** Markdown with explicit, independently gradeable criteria. Upload once via `client.beta.files.upload(...)` (beta `files-api-2025-04-14`) to reuse across sessions. |
| `max_iterations` | int | Optional. Default **3**, max **20**. |
The event is echoed back on the stream with a server-assigned `outcome_id` and `processed_at`.
> **Writing rubrics.** Use explicit, gradeable criteria ("CSV has a numeric `price` column"), not vibes ("data looks good") - the grader scores each criterion independently, so vague criteria produce noisy loops. If you don't have a rubric, have Claude analyze a known-good artifact and turn that analysis into one.
---
## Outcome-specific events
These appear on the standard event stream (`sessions.events.stream` / `.list`) alongside the usual `agent.*` / `session.*` events.
| Event | Payload highlights | Meaning |
|---|---|---|
| `span.outcome_evaluation_start` | `outcome_id`, `iteration` (0-indexed) | Grader began scoring iteration *N*. |
| `span.outcome_evaluation_ongoing` | `outcome_id` | Heartbeat while the grader runs. Grader reasoning is opaque - you see *that* it's working, not *what* it's thinking. |
| `span.outcome_evaluation_end` | `outcome_evaluation_start_id`, `outcome_id`, `iteration`, `result`, `explanation`, `usage` | Grader finished one iteration. `result` drives what happens next (table below). |
### `span.outcome_evaluation_end.result`
| `result` | Next |
|---|---|
| `satisfied` | Session -> `idle`. Terminal for this outcome. |
| `needs_revision` | Agent starts another iteration. |
| `max_iterations_reached` | No further grader cycles. Agent may run one final revision, then session -> `idle`. |
| `failed` | Session -> `idle`. Rubric fundamentally doesn't match the task (e.g. description and rubric contradict). |
| `interrupted` | Emitted whenever a `user.interrupt` arrives while an outcome is active - **even if evaluation hadn't started**. In that case `outcome_evaluation_start_id` is an empty string rather than an event ID, so don't use it as a lookup key without checking. (Except an interrupt sent while paused at the session budget, which is accepted and ignored - see `shared/managed-agents-events.md` § Reaching a session budget.) |
```json
{
"type": "span.outcome_evaluation_end",
"id": "sevt_01jkl...",
"outcome_evaluation_start_id": "sevt_01def...",
"outcome_id": "outc_01a...",
"result": "satisfied",
"explanation": "All 12 criteria met: revenue projections use 5 years of historical data, ...",
"iteration": 0,
"usage": { "input_tokens": 2400, "output_tokens": 350, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 1800 },
"processed_at": "2026-03-25T14:03:00Z"
}
```
---
## Checking status & retrieving deliverables
**Status** - either watch the stream for `span.outcome_evaluation_end`, or poll the session and read `outcome_evaluations`:
```python
session = client.beta.sessions.retrieve(session.id)
for ev in session.outcome_evaluations:
print(f"{ev.outcome_id}: {ev.result}") # outc_01a...: satisfied
```
**Deliverables** - the agent writes to `/mnt/session/outputs/`. Once idle, fetch via the Files API with `scope_id=session.id`. This is the same session-outputs mechanism documented in `shared/managed-agents-environments.md` -> Session outputs (including the dual-beta-header requirement on `files.list`).
---
## Interaction rules & pitfalls
- **One outcome at a time.** Chain by sending the next `user.define_outcome` only after the previous one's terminal `span.outcome_evaluation_end` (`satisfied` / `max_iterations_reached` / `failed` / `interrupted`). The session retains history across chained outcomes.
- **Steering is allowed but optional.** You *may* send `user.message` events mid-outcome to nudge direction, but the agent already knows to keep working until terminal - don't send "keep going" prompts. (Exception: a session paused at its budget (`stop_reason: budget_reached`) accepts only settle events - a steering `user.message`, or a chained `user.define_outcome`, is a 400 there; see `shared/managed-agents-events.md` § Reaching a session budget.)
- **`user.interrupt` pauses the current outcome** - it marks `result: "interrupted"` and leaves the session `idle`, ready for a new outcome or conversational turn. (Exception: sent while paused at the session budget, the interrupt is accepted and ignored and the outcome stays active - see `shared/managed-agents-events.md` § Reaching a session budget.)
- **After terminal, the session is reusable** - continue conversationally or define a new outcome.
- **Outcome != session-create field.** Don't put `outcome`, `rubric`, or `description` on `sessions.create()` - outcomes are always sent as a `user.define_outcome` event.
- **Idle-break gate is unchanged.** In your drain loop, keep using `event.type === 'session.status_idle' && event.stop_reason?.type !== 'requires_action'` - do **not** gate on `span.outcome_evaluation_end` alone (on `needs_revision` the session keeps running). See `shared/managed-agents-client-patterns.md` Pattern 5.
For the raw HTTP shapes and per-language SDK bindings beyond Python, WebFetch `https://platform.claude.com/docs/en/managed-agents/define-outcomes.md` (see `shared/live-sources.md`).
# Managed Agents - Overview
Managed Agents provisions a container per session as the agent's workspace. The agent loop runs on Anthropic's orchestration layer; the container is where the agent's *tools* execute - bash commands, file operations, code. You create a persisted **Agent** config (model, system prompt, tools, MCP servers, skills), then start **Sessions** that reference it. The session streams events back to you; you send user messages and tool results in.
## Warning: THE MANDATORY FLOW: Agent (once) -> Session (every run)
**Why agents are separate objects: versioning.** An agent is a persisted, versioned config - every update creates a new immutable version, and sessions pin to a version at creation time. This lets you iterate on the agent (tweak the prompt, add a tool) without breaking sessions already running, roll back if a change regresses, and A/B test versions side-by-side. None of that works if you `agents.create()` fresh on every run.
Every session references a pre-created `/v1/agents` object. Create the agent once, store the ID, and reuse it across runs.
| Step | Call | Frequency |
|---|---|---|
| 1 | `POST /v1/agents` - `model`, `system`, `tools`, `mcp_servers`, `skills` live here | **ONCE.** Store `agent.id` **and** `agent.version`. |
| 2 | `POST /v1/sessions` - `agent: "agent_abc123"` or `{type: "agent", id, version}` | **Every run.** String shorthand uses latest version. |
If you're about to write `sessions.create()` with `model`, `system`, or `tools` on the session body - **stop**. Those fields live on `agents.create()`. The session takes a *pointer* only.
**When generating code, separate setup from runtime.** `agents.create()` belongs in a setup script (or a guarded `if agent_id is None:` block), not at the top of the hot path. If the user's code calls `agents.create()` on every invocation, they're accumulating orphaned agents and paying the create latency for nothing. The correct shape is: define the agent as a version-controlled YAML manifest, apply it once with `ant beta:agents create < agent.yaml` (or a guarded setup script - see `shared/anthropic-cli.md`), persist the returned ID (config file, env var, secrets manager), and have every run load the ID and call `sessions.create()`.
**To change the agent's behavior, use `POST /v1/agents/{id}` - don't create a new one.** Each update bumps the version; running sessions keep their pinned version, new sessions get the latest (or pin explicitly via `{type: "agent", id, version}`). See `shared/managed-agents-core.md` -> Agents -> Versioning. To change `tools`/`mcp_servers` on **one running session** without touching the agent object, use `sessions.update()` (`vault_ids` attaches at session create only) - see `shared/managed-agents-core.md` -> Updating the agent configuration mid-session.
## Beta Headers
Managed Agents is in beta. The SDK sets required beta headers automatically:
| Beta Header | What it enables |
| ------------------------------ | ---------------------------------------------------- |
| `managed-agents-2026-04-01` | Agents, Environments, Sessions, Events, Session Resources, Session Threads, Outcomes, Multiagent, Vaults, Credentials, Memory Stores, Deployments |
| `skills-2025-10-02` | Skills API (for managing custom skill definitions) |
| `files-api-2025-04-14` | Files API for file uploads |
**Which beta header goes where:** The SDK sets `managed-agents-2026-04-01` automatically on `client.beta.{agents,environments,sessions,vaults,memory_stores,deployments,deployment_runs}.*` calls, and `files-api-2025-04-14` / `skills-2025-10-02` automatically on `client.beta.files.*` / `client.beta.skills.*` calls. You do NOT need to add the Skills or Files beta header when calling Managed Agents endpoints. On raw HTTP the Managed Agents header **grants Files API access on its own**, so uploading a file for use as a session resource does not need `files-api-2025-04-14` alongside it. (Direct Skills API calls over cURL do still need `skills-2025-10-02`; the `ant` CLI and the SDKs send it for you.) **Exception - session-scoped file listing:** `client.beta.files.list({scope_id: session.id})` is a Files endpoint that takes a Managed Agents parameter, so it needs **both** headers. Pass `betas: ["managed-agents-2026-04-01"]` explicitly on that call (the SDK adds the Files header; you add the Managed Agents one). See `shared/managed-agents-environments.md` -> Session outputs.
## Reading Guide
| User wants to... | Read these files |
| -------------------------------------- | ------------------------------------------------------- |
| **Get started from scratch / "help me set up an agent"** | `shared/managed-agents-onboarding.md` - guided interview (WHERE->WHO->WHAT->WATCH), then emit code |
| Understand how the API works | `shared/managed-agents-core.md` |
| See the full endpoint reference | `shared/managed-agents-api-reference.md` |
| **Create an agent** (required first step) | `shared/managed-agents-core.md` (Agents section) + language file |
| Update/version an agent | `shared/managed-agents-core.md` (Agents -> Versioning) - update, don't re-create |
| Create a session | `shared/managed-agents-core.md` + `{lang}/managed-agents/README.md` (cURL/C#: `curl/managed-agents.md`) |
| Configure tools and permissions | `shared/managed-agents-tools.md` |
| Restrict which sites `web_search` / `web_fetch` can reach; localize search; cap fetched content | `shared/managed-agents-tools.md` (§ Web search & web fetch settings) - `allowed_domains` / `blocked_domains` / `user_location` / `max_content_tokens` on the toolset `configs` entry; **not** the environment's `networking` |
| Set up MCP servers | `shared/managed-agents-tools.md` (MCP Servers section) |
| Stream events / handle tool_use | `shared/managed-agents-events.md` + language file |
| Get notified of session state changes via webhook (no polling) | `shared/managed-agents-webhooks.md` - Console-registered endpoint, HMAC verify, thin payload + fetch |
| Define an outcome / rubric-graded iterate loop | `shared/managed-agents-outcomes.md` - `user.define_outcome` event, grader, `span.outcome_evaluation_*` events |
| Coordinate multiple agents / subagents / threads | `shared/managed-agents-multiagent.md` - `multiagent: {type: "coordinator", agents: [...]}` on the agent, session threads, cross-posted tool confirmations |
| Set up environments | `shared/managed-agents-environments.md` + language file |
| Run tool execution in your own infra / VPC (self-hosted sandbox) | `shared/managed-agents-self-hosted-sandboxes.md` - `config:{type:"self_hosted"}`, `ANTHROPIC_ENVIRONMENT_KEY`, `EnvironmentWorker.run()` / `ant beta:worker poll` |
| Upload files / attach repos | `shared/managed-agents-environments.md` (Resources) |
| Give agents persistent memory across sessions | `shared/managed-agents-memory.md` - memory stores, `memory_store` session resource, preconditions, versions/redact. On self-hosted sandboxes: `shared/managed-agents-self-hosted-sandboxes.md` § Memory stores (SDK worker syncs a local copy) |
| Inspect a session without code (transcript, per-tool stats, cost, threads) | `shared/managed-agents-events.md` - Console session viewer note; deep link `?event={event_id}` |
| Define agents/environments as version-controlled YAML; drive the API from the shell | `shared/anthropic-cli.md` - `ant beta:agents create < agent.yaml`, `--transform`, `@file` inlining |
| Store credentials (MCP auth, API keys for CLIs/SDKs) | `shared/managed-agents-tools.md` (Vaults section) - `mcp_oauth` / `static_bearer` / `environment_variable` |
| Call a non-MCP API / CLI that needs a secret | `shared/managed-agents-tools.md` (Vaults section) - `environment_variable` credential, substituted at egress. If that doesn't fit (e.g. self-hosted sandboxes), `shared/managed-agents-client-patterns.md` Pattern 9 keeps the secret host-side via a custom tool |
| Run an agent on a recurring cron schedule | `shared/managed-agents-scheduled-deployments.md` - deployments, deployment runs, pause/auto-pause |
| Cap a session's spend with a hard dollar budget | `shared/managed-agents-core.md` (§ Session budgets) - `budget` at session create, `budget_reached` pause, change/remove to resume. Deployments: `shared/managed-agents-scheduled-deployments.md` § Deployment budgets |
| Pin where model inference runs (data residency) | `shared/managed-agents-core.md` (§ Pinning inference geography) - `model.inference_geo` on the agent, per-session override, roster uniformity |
| Load skills from the codebase instead of uploading | `shared/managed-agents-tools.md` (§ Skills from a GitHub repository) - root `.claude/skills` discovery at session start |
| Give the session an advisor to consult mid-turn | `shared/managed-agents-multiagent.md` (§ Advisor) - `{type: "advisor", model}` roster entry, consultation threads, plaintext vs redacted delivery |
## Common Pitfalls
- **Agent FIRST, then session - NO EXCEPTIONS** - the session's `agent` field accepts **only** a string ID or `{type: "agent", id, version}`. `model`, `system`, `tools`, `mcp_servers`, `skills` are **top-level fields on `POST /v1/agents`**, never on `sessions.create()`. If the user hasn't created an agent, that is step zero of every example.
- **Agent ONCE, not every run** - `agents.create()` is a setup step. Store the returned `agent_id` and reuse it; don't call `agents.create()` at the top of your hot path. If the agent's config needs to change, `POST /v1/agents/{id}` - each update creates a new version, and sessions can pin to a specific version for reproducibility.
- **MCP auth goes through vaults** - the agent's `mcp_servers` array declares `{type, name, url}` only (no auth). Credentials live in vaults (`client.beta.vaults.credentials.create`) and attach to sessions via `vault_ids`. Anthropic auto-refreshes OAuth tokens using the stored refresh token. Vaults also hold `environment_variable` credentials for non-MCP services (CLIs, SDKs, direct API calls) - substituted at egress, never visible in the sandbox.
- **Reconcile resources before the first run** - a session with a clear ask but a missing tool, credential, data mount, or context will discover the gap mid-run, then flail and give up. Before creating the session, check that every action in the task maps to a configured tool/MCP server, every MCP server has a vault credential, and every referenced file/host is mounted/reachable. When helping a user set one up, run the reconciliation in `shared/managed-agents-onboarding.md` -> §3 Pre-flight viability check.
- **Stream to get events** - `GET /v1/sessions/{id}/events/stream` is the primary way to receive agent output in real-time.
- **SSE stream has no replay - reconnect with consolidation** - if the stream drops while a `agent.tool_use`, `agent.mcp_tool_use`, or `agent.custom_tool_use` is pending resolution (`user.tool_confirmation` for the first two, `user.custom_tool_result` for the last one), the session deadlocks (client disconnects -> session idles -> reconnect happens -> no client resolution happens). On every (re)connect: open stream with `GET /v1/sessions/{id}/events/stream` , fetch `GET /v1/sessions/{id}/events`, dedupe by event ID, then proceed. See `shared/managed-agents-events.md` -> Reconnecting after a dropped stream.
- **Don't trust HTTP-library timeouts as wall-clock caps** - `requests` `timeout=(c, r)` and `httpx.Timeout(n)` are *per-chunk* read timeouts; they reset every byte, so a trickling connection can block indefinitely. For a hard deadline on raw-HTTP polling, track `time.monotonic()` at the loop level and bail explicitly. Prefer the SDK's `sessions.events.stream()` / `sessions.events.list()` over hand-rolled HTTP. See `shared/managed-agents-events.md` -> Receiving Events.
- **Messages queue** - you can send events while the session is `running` or `idle`; they're processed in order. No need to wait for a response before sending the next message. Exception: a session paused at its budget (`stop_reason: budget_reached`) accepts only settle events - change or remove the budget to resume (`shared/managed-agents-core.md` § Session budgets).
- **Environment `config.type` is `"cloud"` or `"self_hosted"`** - `cloud` runs the container on Anthropic's infrastructure; `self_hosted` moves tool execution to your own (see `shared/managed-agents-self-hosted-sandboxes.md`).
- **Archive is permanent on every resource** - archiving an agent, environment, session, vault, credential, or memory store makes it read-only with no unarchive. For agents, environments, and memory stores specifically, archived resources cannot be referenced by new sessions (existing sessions continue). Do not call `.archive()` on a production agent, environment, or memory store as cleanup - **always confirm with the user before archiving**.
# Managed Agents - Scheduled Deployments
A **scheduled deployment** runs an agent on a recurring cron schedule - each firing creates a session autonomously. Use it for predictable-cadence work: nightly triage, weekly compliance scans, hourly monitors.
Requires the `managed-agents-2026-04-01` beta header (the SDK sets it automatically for `client.beta.deployments.*` / `client.beta.deployment_runs.*` calls).
## Create a deployment
A deployment bundles everything a session needs (agent, environment, optional files / GitHub / memory stores / vaults) plus a `schedule` and the `initial_events` that kick off each run:
- `agent` and `environment_id` are required - same shapes as `sessions.create` (see `shared/managed-agents-core.md`). A deployment targeting a **self-hosted** environment can attach `memory_store` resources (SDK worker required - `shared/managed-agents-self-hosted-sandboxes.md` § Memory stores); `file` and `github_repository` resources need a cloud environment. The Console deployment form doesn't offer memory stores for self-hosted environments - attach them via the API/SDK.
- `initial_events` must contain at least one starting event - a `user.message` **or** a `user.define_outcome`. (A deployment's `initial_events` also accepts `system.message`, which a session's does not.)
- `schedule` takes a cron `expression` and an IANA `timezone`. Minute-level granularity is the maximum.
```bash
curl -fsSL https://api.anthropic.com/v1/deployments \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @- <<EOF
{
"name": "Weekly compliance scan",
"agent": "$AGENT_ID",
"environment_id": "$ENVIRONMENT_ID",
"initial_events": [
{"type": "user.message", "content": [{"type": "text", "text": "Run the weekly compliance scan."}]}
],
"schedule": {
"type": "cron",
"expression": "0 20 * * 5",
"timezone": "America/New_York"
}
}
EOF
```
```python
deployment = client.beta.deployments.create(
name="Weekly compliance scan",
agent=agent.id,
environment_id=environment.id,
initial_events=[
{
"type": "user.message",
"content": [{"type": "text", "text": "Run the weekly compliance scan."}],
},
],
schedule={
"type": "cron",
"expression": "0 20 * * 5",
"timezone": "America/New_York",
},
)
```
The response is a deployment object (`depl_` ID prefix). Check `schedule.upcoming_runs_at` - the next fire times - to confirm the schedule parses the way you intended:
```json
{
"id": "depl_01xyz",
"status": "active",
"paused_reason": null,
"schedule": {
"type": "cron",
"expression": "0 20 * * 5",
"timezone": "America/New_York",
"last_run_at": null,
"upcoming_runs_at": ["2026-05-09T00:00:00Z", "2026-05-16T00:00:00Z", "2026-05-23T00:00:00Z"]
}
}
```
`upcoming_runs_at` reflects the exact configured schedule, but **execution is jittered to distribute load: up to 15% of the interval between runs, floored at 5 seconds and capped at 9 minutes.** An hourly deployment can therefore fire up to 9 minutes late; don't build a downstream deadline that assumes the listed timestamp. Maximum **1000 scheduled deployments per organization** (contact Anthropic support for more).
### Cron and timezone semantics
- **Expression:** standard POSIX cron (`minute hour day-of-month month day-of-week`).
- **Timezone:** IANA identifier (e.g. `"America/Los_Angeles"`).
- **DST:** literal wall-clock matching - `"0 20 * * *"` in `America/New_York` fires at 8:00 PM local regardless of EST/EDT.
> Warning: **DST edge:** wall-clock times that don't exist on a spring-forward day (e.g. 2AM) are **skipped**; times that occur twice on a fall-back day **fire twice**. Schedule outside the 1-3AM local window, or use UTC, when missed or duplicate executions are unacceptable.
## Deployment budgets
A deployment accepts the same `budget` object as a session (`{type: "limit", max_list_cost: {amount, currency}}` - minor-unit cents string, `USD` only; see `shared/managed-agents-core.md` § Session budgets). The cap is **copied onto each session at fire time**, and that session then behaves exactly like any budgeted session.
Deployment budget update semantics differ from a session's:
- `budget` is accepted on **create and update** - it is not create-only.
- `budget: null` on update **clears** it, and a cleared budget **can be re-added later** - there is no one-way door.
- A change applies **from the next fired session** - sessions already running keep the cap they were created with (change those via their own session update).
## Deployment runs
Every trigger attempt - successful or not - writes a **deployment run** record (`drun_` prefix), so you can audit failures independent of the session lifecycle. A successful run carries the created `session_id`; follow that session via the event stream (`shared/managed-agents-events.md`) or webhooks (`shared/managed-agents-webhooks.md`) as usual. A failed run carries an `error` whose `type` explains why session creation was rejected.
```python
# All runs for a deployment
for run in client.beta.deployment_runs.list(deployment_id=deployment.id):
print(run.created_at, run.session_id or run.error.type)
# Failures only
for run in client.beta.deployment_runs.list(deployment_id=deployment.id, has_error=True):
print(run.created_at, run.error.type, run.error.message)
```
```typescript
for await (const run of client.beta.deploymentRuns.list({
deployment_id: deployment.id,
has_error: true,
})) {
console.log(run.created_at, run.error?.type, run.error?.message);
}
```
Raw HTTP: `GET /v1/deployment_runs?deployment_id=...&has_error=true`. To retrieve a single run by ID, `GET /v1/deployment_runs/{deployment_run_id}` (SDK: `client.beta.deployment_runs.retrieve(run_id)`) - a `deployment_run.*` webhook event carries the run ID as its `data.id`.
A failed run looks like:
```json
{
"type": "deployment_run",
"id": "drun_01abc124",
"deployment_id": "depl_01xyz",
"trigger_context": { "type": "schedule", "scheduled_at": "2026-05-09T00:00:00Z" },
"session_id": null,
"error": { "type": "environment_archived", "message": "environment `env_01abc` is archived" },
"agent": { "type": "agent", "id": "agent_01ghi789", "version": 3 },
"created_at": "2026-05-09T00:00:01Z"
}
```
Error types include `environment_archived`, `agent_archived`, `vault_not_found`, `session_rate_limited`, and `service_unavailable`.
The outcome of each **scheduled** run (started/succeeded/failed) and each deployment lifecycle change (created/updated/paused/unpaused/archived/deleted) is also delivered as a webhook event - see `shared/managed-agents-webhooks.md` for the `deployment.*` and `deployment_run.*` event types - so you can react without polling. Manual runs do **not** emit `deployment_run.*` webhook events.
## Lifecycle: pause / unpause / archive
| Operation | SDK | Effect |
|---|---|---|
| Pause | `client.beta.deployments.pause(id)` | Suppresses scheduled triggers go-forward. Sessions already running continue. **Manual runs are still permitted while paused.** Sets `paused_reason: {"type": "manual"}`. |
| Unpause | `client.beta.deployments.unpause(id)` | Resumes from the next scheduled occurrence. **Missed triggers are not backfilled.** Clears `paused_reason`. |
| Archive | `client.beta.deployments.archive(id)` | **Terminal** - the schedule stops and the deployment can no longer be modified. Use pause for anything reversible. |
Raw HTTP: `POST /v1/deployments/{deployment_id}/pause` (likewise `/unpause`, `/archive`).
### Failure behavior
- **Rate-limited:** recorded immediately as a `session_rate_limited` run, **no retry** - the schedule simply tries again at the next occurrence. (Rate limits on API calls *inside* a session are handled by the session itself.)
- **Other failed runs** (e.g. `environment_archived`, `vault_not_found`, `service_unavailable`): the run records the `error.type` - monitor runs and fix the referenced resource, or pause the deployment.
- **Agent archived:** the deployment is automatically **archived** (terminal) in the same operation. **Agent deleted:** the next scheduled trigger detects the missing agent and archives the deployment then. Either way no deployment run is recorded, and no further sessions are created.
## Manual runs
`POST /v1/deployments/{deployment_id}/run` (SDK: `client.beta.deployments.run(id)`) creates a session immediately and writes a run with `trigger_context.type: "manual"`. Use it to **test a deployment before committing to the schedule** - and remember it works even while the deployment is paused.
# Managed Agents - Self-Hosted Sandboxes
With `config.type: "self_hosted"`, the **agent loop stays on Anthropic's orchestration layer** but **tool execution moves to infrastructure you control** - bash, file ops, and code run inside your container, so filesystem contents and the sandbox's network egress never leave your environment. (`web_search` / `web_fetch` are the exception: they run on Anthropic's servers in both environment types - restrict them with `allowed_domains` / `blocked_domains` in the agent toolset, `shared/managed-agents-tools.md` § Web search & web fetch settings.) Tool inputs/outputs still flow to Anthropic's control plane so the model can see results; the agent's skills and the contents of any attached memory stores are stored by Anthropic and copied into your sandbox for the session (memory changes sync back - see § Memory stores). Contrast with `config.type: "cloud"`, where Anthropic runs the container. Connectivity is **outbound-only**: your worker long-polls Anthropic's work queue; Anthropic never dials into your network.
## Flow
```
1. Create environment: config: {type: "self_hosted"} -> env_...
2. Generate environment key (Console, on the environment page) -> sk-ant-oat01-... as ANTHROPIC_ENVIRONMENT_KEY
3. Run a worker: EnvironmentWorker.run() or ant beta:worker poll
4. Sessions reference environment_id=env_... exactly as for cloud
```
## Create the environment
```python
client = anthropic.Anthropic()
environment = client.beta.environments.create(
name="self-hosted", config={"type": "self_hosted"}
)
```
`{"type": "self_hosted"}` is the entire config - there are no pool, capacity, or networking sub-fields; you control those on your side.
## Run a worker - SDK (primary path)
`EnvironmentWorker` wraps the poll -> dispatch -> tool-execute loop. `.run()` is the always-on loop (loops until cancelled). `.handle_item()` / `.handleItem()` / `.HandleItem()` services **one already-claimed** work item without polling - IDs fall back to `ANTHROPIC_WORK_ID` / `ANTHROPIC_ENVIRONMENT_ID` / `ANTHROPIC_SESSION_ID`, the key to the worker's own `environment_key` and then `ANTHROPIC_ENVIRONMENT_KEY`, and the per-session secret to `ANTHROPIC_WORK_SECRET`, so inside an `ant beta:worker poll --on-work` container it needs no arguments. It ignores (and force-stops) non-session work items itself. There is no `run_one()`; claiming is done by `.run()` or by the mid-level poller (below).
**Python - always-on:**
```python
import asyncio
import contextlib
import os
import signal
from anthropic import AsyncAnthropic
from anthropic.lib.environments import EnvironmentWorker
async def main() -> None:
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
async with AsyncAnthropic(auth_token=environment_key) as client:
worker = EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
)
task = asyncio.create_task(worker.run())
# Cancel the task (don't kill the process): the worker stops its in-flight
# work item and uploads changed memory files before exiting.
loop = asyncio.get_running_loop()
for signum in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(signum, task.cancel)
with contextlib.suppress(asyncio.CancelledError):
await task
asyncio.run(main())
```
**TypeScript - always-on:**
```typescript
import Anthropic from "@anthropic-ai/sdk";
import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
const client = new Anthropic({ authToken: environmentKey });
const ctrl = new AbortController();
process.once("SIGTERM", () => ctrl.abort());
process.once("SIGINT", () => ctrl.abort());
await new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
signal: ctrl.signal
}).run();
```
**Customizing tools.** `EnvironmentWorker` runs the built-in toolset by default. To add or replace tools, use `AgentToolContext(workdir=, client=, session_id=)` with `beta_agent_toolset(env)` / `betaAgentToolset(env)` and pass the resulting tools to the lower-level `tool_runner()`. Skills attached to the agent are downloaded into `{workdir}/skills/<name>/` before tool calls begin (`AgentToolContext` handles this when given `client` and `session_id`). Downloaded skill files are marked executable automatically by the CLI and SDK; if you implement skills download yourself, you set permissions.
> **Runtime deps:** the SDK helpers require `/bin/bash` at that exact path (not consulted via `PATH`). The TypeScript SDK additionally requires `unzip` and `tar` on `PATH` and Node.js 22+; Python and Go use their standard libraries for archive extraction. Memory stores additionally need a POSIX host (Linux or macOS - not Windows, the worker opens memory files with `O_NOFOLLOW`) with a writable `/mnt/memory` - see § Memory stores.
**File-tool confinement.** `AgentToolContext` confines `read`/`write`/`edit`/`glob`/`grep` to the working directory plus `allowed_roots` (`allowedRoots` / `AllowedRoots`); `write` and `edit` also refuse paths under `read_only_roots` (`readOnlyRoots` / `ReadOnlyRoots`). `EnvironmentWorker` adds the session's memory store directories to these lists itself. This is a guardrail for the file tools only - it does **not** constrain `bash`. The old `unrestricted_paths` option is no longer accepted (passing it raises); add directories to `allowed_roots` instead.
## Run a worker - `ant` CLI (fixed tools)
The `ant` CLI ships a worker with the fixed built-in toolset (`bash`, `read`, `write`, `edit`, `glob`, `grep`). Install per `shared/anthropic-cli.md`, then:
```sh
export ANTHROPIC_ENVIRONMENT_KEY=sk-ant-oat01-...
ant beta:worker poll --environment-id env_... --workdir /workspace
```
- `--workdir` is the directory tools operate in (default `.`); tool calls are sandboxed to it.
- `--environment-key` overrides the env var.
- `--on-work <script>` runs your script per work item (e.g. to spin a fresh container per session - see Container orchestration below).
- `--unrestricted-paths`, `--max-idle` (default `60s`), `--log-format` - see `ant beta:worker poll --help`.
- Flags fall back to env vars (`ANTHROPIC_ENVIRONMENT_ID`, `ANTHROPIC_ENVIRONMENT_KEY`).
- Exits cleanly on SIGTERM/SIGINT after draining in-flight work.
- **Fixed toolset** - for custom tools, use the SDK worker above.
- **Does not mount memory stores.** A session that attaches one still runs, but the agent finds nothing at the store's `/mnt/memory/<store-name>/` directory and nothing syncs back. To combine the CLI poller with memory stores, keep `ant beta:worker poll --on-work` on the host and run the **SDK** worker (`EnvironmentWorker.handle_item()`) inside the per-session sandbox - see § Memory stores -> Sandbox-per-session.
Inside an `--on-work` container, run `ant beta:worker run --workdir <dir>` as the entrypoint (or the SDK worker, if the session needs memory stores).
## Webhook-driven wake (instead of always-on)
Register a webhook for `session.status_run_started` (see `shared/managed-agents-webhooks.md`), verify the delivery, then **drain** the queue with the poller (`drain=True` stops when it's empty; `block_ms=None` is non-blocking; `auto_stop=False` because `handle_item` force-stops the item itself) and hand each claimed item to `handle_item()`. **Don't `await` the drain inside the HTTP handler** - a session run outlives the webhook delivery timeout, so acknowledge the delivery and run the drain as a background task (`asyncio.create_task` / a detached promise / a goroutine off `context.Background()`), keeping the process alive until it finishes:
```python
import asyncio
import os
import anthropic
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
client = anthropic.AsyncAnthropic(
auth_token=environment_key,
) # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env for webhooks.unwrap()
async def handle(raw: bytes, headers: dict[str, str]) -> dict:
event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
if event.data.type != "session.status_run_started":
return {"status": "ignored"}
asyncio.create_task(drain()) # keep a reference if your framework may GC it
return {"status": "accepted"}
async def drain() -> None:
async for work in client.beta.environments.work.poller(
environment_id=environment_id,
environment_key=environment_key,
block_ms=None,
reclaim_older_than_ms=2000,
drain=True,
auto_stop=False,
):
await client.beta.environments.work.worker(workdir="/workspace").handle_item(
work_id=work.id,
environment_id=environment_id,
session_id=work.data.id,
environment_key=environment_key,
work_secret=work.secret, # lets the worker mount the session's memory stores
)
```
TypeScript: same shape with `client.beta.webhooks.unwrap(body, {headers})`, `client.beta.environments.work.poller({environmentId, environmentKey, blockMs: null, reclaimOlderThanMs: 2000, drain: true, autoStop: false})`, and `client.beta.environments.work.worker({workdir}).handleItem({workId, environmentId, sessionId, environmentKey, workSecret: work.secret})`. Go: no `RunOne` convenience either - `environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{EnvironmentID, EnvironmentKey, BlockMs: param.Null[int64](), ReclaimOlderThanMs: param.NewOpt[int64](2000), Drain: true, AutoStop: param.NewOpt(false)})`, then `worker.HandleItem(ctx, environments.HandleItemOptions{WorkID: item.ID, EnvironmentID: item.EnvironmentID, SessionID: item.Data.ID, EnvironmentKey, WorkSecret: item.Secret})` per `poller.Next()` item, in a goroutine off `context.Background()`. Always pass the work item's `secret` through, or sessions with memory stores fail at claim time. `handle_item` skips non-session work items itself, so the drain loop needs no `work.data.type` check.
## Container orchestration (mid-level)
`EnvironmentWorker.run()` polls and executes tools in the same process. To run each session in its **own** container, use the mid-level poller in a thin orchestrator - Python `client.beta.environments.work.poller(environment_id=, environment_key=, drain=, block_ms=, reclaim_older_than_ms=, auto_stop=)`; TypeScript `new WorkPoller({client, environmentId, environmentKey, autoStop})` from `@anthropic-ai/sdk/helpers/beta/environments` - and, for each yielded `work` item, start a fresh container with these env vars injected, whose entrypoint runs `ant beta:worker run` or an `EnvironmentWorker(...).handle_item()` (required if the session attaches memory stores). `block_ms` is 1-999 (or `None` for non-blocking); `reclaim_older_than_ms` re-claims items leased to a dead worker; `drain` stops once the queue is empty; `auto_stop` posts a stop signal after the iterator exits (set `False` when the launched container owns the stop call). Go: `environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{EnvironmentID, EnvironmentKey, BlockMs, ReclaimOlderThanMs, Drain, AutoStop: param.NewOpt(false)})` with `poller.Next()` / `poller.Current()` / `poller.Err()`.
| Env var | Value |
|---|---|
| `ANTHROPIC_SESSION_ID` | `work.data.id` |
| `ANTHROPIC_WORK_ID` | `work.id` |
| `ANTHROPIC_ENVIRONMENT_ID` | `work.environment_id` |
| `ANTHROPIC_ENVIRONMENT_KEY` | pass through |
| `ANTHROPIC_BASE_URL` | pass through |
| `ANTHROPIC_WORK_SECRET` | `work.secret` - the per-session credential the worker inside needs to mount memory stores. `ant beta:worker poll --on-work` does **not** set it for the spawned script; read it from the work-item JSON on stdin (`jq -r '.secret // empty'`) and pass it in. Only into the sandbox serving that session; never log it. |
Skip items where `work.data.type != "session"` when you dispatch containers yourself (`handle_item` does this check for you).
## Memory stores
Sessions on a self-hosted environment attach memory stores exactly like cloud sessions - `resources=[{"type": "memory_store", "memory_store_id": ..., "access": ...}]` at session create, up to 8 per session (see `shared/managed-agents-memory.md`). The difference is *who materializes them*: on cloud, Anthropic mounts a live FUSE filesystem; on self-hosted, the **SDK worker** (`EnvironmentWorker`, or its `handle_item()` / `handleItem()` / `HandleItem()`) downloads a working copy and syncs it. Requires the Python, TypeScript, or Go SDK; the `ant` CLI worker and the C#/Java/PHP/Ruby SDKs don't mount stores. Not available on Claude Platform on AWS.
**What the worker does** when it claims a work item whose session has stores attached:
1. Downloads each store to its mount path under `/mnt/memory/` - derived from the store's name, not a settable field (e.g. `/mnt/memory/user-preferences/` for a store named "User Preferences"); the same path cloud sessions use, and the session's system prompt describes it to the agent. Authenticates with the work item's per-session `secret`.
2. Adds those directories to the file tools' `allowed_roots`, and `access: "read_only"` stores to `read_only_roots`, so the agent uses the ordinary `read`/`write`/`edit`/`glob`/`grep` tools on memories.
3. Reconciles after tool calls, at most once per sync interval (default 15 s): remote changes are written to disk, files the agent changed are uploaded.
4. On session end: final sync, flushes pending uploads for up to 30 s, removes the directories. A worker that is *cancelled* mid-session skips the final sync but still uploads changed files and removes the directories; a worker that is *killed* runs no teardown at all.
The store on Anthropic's side remains the source of truth - memory versions, redaction, and Console viewing/editing work as for cloud sessions, and the agent's memory reads/writes appear in the event stream as ordinary tool events. Because sync is interval-based, a change written by one self-hosted session is visible to another running session only after both have synced (typically well under a minute); cloud sessions see each other's changes almost immediately. Each store directory holds a marker file `.anthropic-memory-store` - leave it alone; the worker won't sync a directory whose marker is missing or altered.
**Prepare the host.** POSIX (Linux/macOS) only; a case-sensitive filesystem is recommended. Before starting the worker:
```bash
sudo mkdir -p /mnt/memory && sudo chown "$USER" /mnt/memory
```
Do **not** create the per-store directories yourself - the worker creates each store's directory when a session starts, **refuses the work item if something already exists at that path**, and removes it at session end. Two rules follow: (a) two sessions can't mount the same store on one host simultaneously (they need the same path) - give each session its own sandbox; (b) stop workers gracefully. `EnvironmentWorker` installs no signal handlers: wire SIGTERM/SIGINT to cancellation yourself (abort the `signal` in TypeScript, cancel the context in Go, cancel the task running `run()` / `handle_item()` in Python), send SIGTERM, and allow >= 30 s before any hard kill. If a worker is killed before teardown, remove the leftover directory under `/mnt/memory/` before the next session that attaches that store - unsynced edits in it are lost.
**Sandbox-per-session** (the pattern from § Container orchestration) satisfies rule (a) automatically. Keep `ant beta:worker poll --on-work` (or the SDK poller) on the host; build the per-session image around the SDK worker instead of `ant beta:worker run` - its entrypoint constructs `EnvironmentWorker` and calls `handle_item()`, which reads the session/work/environment IDs from the `ANTHROPIC_*` vars and the per-session secret from `ANTHROPIC_WORK_SECRET` (or pass `work_secret=` / `workSecret` / `WorkSecret` explicitly). `--on-work` does not set `ANTHROPIC_WORK_SECRET` for the spawn script, so read it from the work-item JSON on stdin:
```bash
#!/bin/bash
# spawn.sh - called once per claimed work item; the work item arrives as JSON on stdin
ANTHROPIC_WORK_SECRET="$(jq -r '.secret // empty')"
export ANTHROPIC_WORK_SECRET
exec docker run --rm \
-e ANTHROPIC_SESSION_ID -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID \
-e ANTHROPIC_ENVIRONMENT_KEY -e ANTHROPIC_BASE_URL -e ANTHROPIC_WORK_SECRET \
my-sdk-worker-image
```
The per-session entrypoint is a few lines - no arguments needed, `handle_item()` reads the forwarded `ANTHROPIC_*` vars including `ANTHROPIC_WORK_SECRET`; wire signals to cancellation so a stopped container still uploads:
```python
import asyncio, contextlib, os, signal
from anthropic import AsyncAnthropic
from anthropic.lib.environments import EnvironmentWorker
async def main() -> None:
async with AsyncAnthropic(auth_token=os.environ["ANTHROPIC_ENVIRONMENT_KEY"]) as client:
task = asyncio.create_task(EnvironmentWorker(client, workdir="/workspace").handle_item())
loop = asyncio.get_running_loop()
for signum in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(signum, task.cancel)
with contextlib.suppress(asyncio.CancelledError):
await task
asyncio.run(main())
```
TypeScript: `new EnvironmentWorker({ client, workdir: "/workspace", signal: controller.signal }).handleItem()` with `process.once("SIGTERM"/"SIGINT", () => controller.abort())`. Go: `signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)` then `environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{Workdir: "/workspace"}).HandleItem(ctx, environments.HandleItemOptions{})`.
The image needs a writable `/mnt/memory`; the memory directories need **not** be bind-mounted to the host - the worker uploads before the sandbox exits, and a discarded sandbox leaves nothing to clean up. Stop a container early with a signal the entrypoint turns into cancellation, not a kill, so that upload still runs.
**Configure sync** - two `EnvironmentWorker` options (constructor or `client.beta.environments.work.worker()` factory in Python; the options object in TypeScript; `environments.EnvironmentWorkerOptions` in Go):
| Option | Python / TypeScript / Go | Behavior |
|---|---|---|
| Sync interval | `memory_sync_interval` (seconds) / `memorySyncIntervalMs` (ms) / `MemorySyncInterval` (duration) | Default 15 s, minimum 5 s. Shorter narrows the stale window at the cost of more memory-store requests. `None` / `null` / negative duration **disables memory support entirely** - stores are neither downloaded nor synced, and a session with stores attached runs without them even though its system prompt still describes them. Only disable on workers whose sessions never attach stores. While enabled, a work item that arrives without a `secret` for a session with stores **fails** rather than running memory-less. |
| Delete propagation | `memory_sync_deletes` / `memorySyncDeletes` / `MemorySyncDeletes` | `"enabled"` (default - deletes from the store once a later sync confirms the file is still gone), `"log_only"` (same checks, only logs what it would delete - use to audit before trusting `enabled`), `"disabled"` (never deletes from the store). Go: `environments.MemorySyncDeletesEnabled` (zero value) / `LogOnly` / `Disabled`. Uploads/downloads are unaffected. |
For example, sync every 10 s and only *log* would-be deletes: Python `EnvironmentWorker(client, environment_id=..., environment_key=..., workdir="/workspace", memory_sync_interval=10, memory_sync_deletes="log_only")`; TypeScript `new EnvironmentWorker({ client, environmentId, environmentKey, workdir: "/workspace", memorySyncIntervalMs: 10_000, memorySyncDeletes: "log_only" })`; Go `environments.EnvironmentWorkerOptions{..., MemorySyncInterval: 10 * time.Second, MemorySyncDeletes: environments.MemorySyncDeletesLogOnly}`.
**Read-only stores and conflicts.** For `access: "read_only"`, `write`/`edit` refuse changes under the directory (the only memory errors that reach the agent, as tool errors) and nothing uploads; the memory-store endpoints also reject writes made with the session's `secret`. `bash` edits aren't blocked locally - they never sync and the next remote change overwrites them. Conflicts resolve **in favor of the store**: if the agent changes a file that also changed remotely since the last sync, the worker keeps the store's version at the next sync, overwrites the local file, and logs a warning - `write`/`edit` still succeed and no error reaches the agent; it can re-read and re-apply.
**Troubleshooting.** Mount and background-sync failures are *logged*, not reported to the session. If a store can't be mounted at claim time the worker fails the work item - the session emits no error event and sits `idle` (`requires_action` stop reason).
| Log line / symptom | Cause | Fix |
|---|---|---|
| `the work item carried no sessions token` (Go: `ErrSessionMemoryNoToken`), work item fails | The per-session `secret` didn't reach the worker - memory on self-hosted isn't enabled for your org, or your spawn script didn't forward it | Forward `ANTHROPIC_WORK_SECRET` into the sandbox. If the in-process worker (poll + run in one process) still logs this, contact support |
| `something already exists at the memory store's path` | Leftover directory from a killed worker | Remove the named directory (unsynced edits are lost) |
| `cannot create the memory store's folder` + `the worker host must make this mount path writable` | Worker user can't create dirs under `/mnt/memory` | `mkdir -p /mnt/memory && chown <worker-user> /mnt/memory` |
| Session `idle` with `requires_action`, no error event, shortly after a claim | Worker failed the work item on a mount error above | Fix the host, then send `user.interrupt` - the work is re-queued and the next claim retries the mount |
## Monitoring & control
These are **control-plane** calls - authenticate with `x-api-key` (not the environment key); `managed-agents-2026-04-01` beta header. **Call them from outside the worker host** - setting `ANTHROPIC_API_KEY` on the worker host exposes an organization-scoped credential to agent tool calls.
| SDK (`client.beta.environments.work.*`) | REST | CLI | Returns |
|---|---|---|---|
| `stats(environment_id)` | `GET /v1/environments/{id}/work/stats` | `ant beta:environments:work stats` | `{type:"work_queue_stats", depth, pending, oldest_queued_at, workers_polling}` |
| `stop(work_id, environment_id=)` | `POST /v1/environments/{id}/work/{work_id}/stop` | `ant beta:environments:work stop` | `work.state` |
## What changes vs `cloud`
| Concern | `cloud` | `self_hosted` |
|---|---|---|
| Container lifecycle, hardening, networking | Anthropic | **You** - run non-root, read-only rootfs, drop caps; egress is whatever your VPC/firewall allows - except `web_search` / `web_fetch`, which run on Anthropic's servers either way (restrict them per tool with `allowed_domains` / `blocked_domains`) |
| `file` / `github_repository` resource mounting | Anthropic mounts into the container | **You** - pass pointers via `sessions.create(metadata={...})` and have your orchestrator fetch/clone before dispatch |
| `memory_store` resources | Mounted by Anthropic at `/mnt/memory/<name>/` (live FUSE mount) | **Supported via the SDK worker** (Python / TypeScript / Go `EnvironmentWorker`), which downloads each store to `/mnt/memory/<store-name>/` and syncs on an interval - see § Memory stores. Not mounted by the `ant` CLI worker; not available in the C#, Java, PHP, or Ruby SDKs. `memory_store` is the **only** resource type self-hosted environments accept - `file` / `github_repository` are still rejected with the 400 message "Environment env_... is a self-hosted environment. `resources` are not supported with self-hosted environments." (deployments targeting a self-hosted environment follow the same rule; the Console deployment form doesn't offer memory stores for them - use the API/SDK). |
| Vault `environment_variable` credentials | Supported (substituted at Anthropic-managed egress) | **Not yet supported** - egress is yours, so there's nowhere to substitute the secret. Use MCP credentials or a host-side custom tool (`shared/managed-agents-client-patterns.md` Pattern 9) |
| Built-in tools | Via `agent_toolset_20260401` | Supplied by your worker (`EnvironmentWorker` default / `beta_agent_toolset(env)` / `ant` CLI fixed set) |
| Skills download | Automatic | `EnvironmentWorker` / `AgentToolContext` fetch into `{workdir}/skills/` (needs `client` + `session_id`) |
| Claude Platform on AWS | Supported | Supported - the worker authenticates with AWS IAM (SigV4) or an AWS-Console-generated API key (Console-generated environment keys don't work against the AWS endpoint); attach the `AnthropicSelfHostedEnvironmentAccess` managed policy to the worker's principal. **Memory stores cannot be attached** to sessions on self-hosted environments there (rejected at session create); cloud environments attach them as usual. |
| SDK worker helpers | All SDKs | **Python, TypeScript, Go only** (`EnvironmentWorker` / poller not in Java, Ruby, PHP, or C#) - use one of those three or the `ant` CLI |
## Credentials
| Credential | Format | Scope |
|---|---|---|
| `ANTHROPIC_ENVIRONMENT_KEY` | `sk-ant-oat01-...` | One environment's work queue. Generate in Console ("Generate environment key"). Pass as `auth_token=` / `authToken` on the client **and** as `environment_key=` / `environmentKey` on `EnvironmentWorker`. Store in a secrets manager; rotate on exposure. |
| `ANTHROPIC_WEBHOOK_SIGNING_KEY` | `whsec_...` | Webhook signature verification (if using webhook-driven wake). The SDK reads this env var automatically for `client.beta.webhooks.unwrap()`. |
| Work-item `secret` (`ANTHROPIC_WORK_SECRET`) | per-session, issued by Anthropic on the claimed work item | Posts that session's events and reads/writes the memory stores attached to it. You don't generate it; the in-process worker picks it up from the work item, and in the sandbox-per-session pattern you forward it into the sandbox yourself (or pass `work_secret=` / `workSecret` / `WorkSecret` explicitly). Treat like the environment key: only into the sandbox serving that session, never in images, shared volumes, or logs. |
## Security - what you own
Container hardening; egress restriction for the sandbox (there is no default; the server-side `web_search` / `web_fetch` are governed only by their `allowed_domains` / `blocked_domains`); `ANTHROPIC_ENVIRONMENT_KEY` custody and rotation; one workspace + environment per trust boundary when running untrusted code; least-privilege for the tool process; log retention and redaction. **Anthropic cannot**: fast-revoke a leaked environment key, verify your image or supply chain, sandbox tool execution inside your container, or enforce retention after tool output reaches your infrastructure. **Memory stores** stay hosted by Anthropic (with version history), but the working copy under `/mnt/memory/` is yours for the session's duration: the worker deletes it on teardown, a killed worker leaves it behind, and permissions/isolation between sessions sharing a filesystem are your responsibility. A `read_only` store is protected from *upload*, not from local modification - `bash` can still change the local copy (later tool calls in that session read the changed copy until the store next changes that memory); disable `bash` or mount the path read-only if the agent must not alter even its local view. See the Self-Hosted Sandboxes Security page in `shared/live-sources.md` for the full checklist.
# Managed Agents - Tools & Skills
## Tools
### Server tools vs client tools
| Type | Who runs it | How it works |
|---|---|---|
| **Prebuilt Claude Agent tools** (`agent_toolset_20260401`) | Anthropic, on the session's container (for `cloud` envs; for `self_hosted`, **your** worker supplies and runs the file/bash tools - see `shared/managed-agents-self-hosted-sandboxes.md`). `web_search` / `web_fetch` always run on Anthropic's servers, in both environment types. | File ops, bash, web search, etc. Enable all at once or configure individually with `enabled: true/false`; restrict the web tools with `allowed_domains` / `blocked_domains`. |
| **MCP tools** (`mcp_toolset`) | Anthropic's orchestration layer | Capabilities exposed by connected MCP servers. Grant access per-server via the toolset. |
| **Custom tools** | **You** - your application handles the call and returns results | Agent emits a `agent.custom_tool_use` event, session goes `idle`, you send back a `user.custom_tool_result` event. |
**Recommendation:** Enable all prebuilt tools via `agent_toolset_20260401`, then disable individually as needed.
**Versioning:** The toolset is a versioned, static resource. When underlying tools change, a new toolset version is created (hence `_20260401`) so you always know exactly what you're getting.
### Agent Toolset
The `agent_toolset_20260401` provides these built-in tools:
| Tool | Description |
| ---------------------- | ---------------------------------------- |
| `bash` | Execute bash commands in a shell session |
| `read` | Read a file from the local filesystem, including text, images, PDFs, and Jupyter notebooks |
| `write` | Write a file to the local filesystem |
| `edit` | Perform string replacement in a file |
| `glob` | Fast file pattern matching using glob patterns |
| `grep` | Text search using regex patterns |
| `web_fetch` | Fetch content from a URL |
| `web_search` | Search the web for information |
Enable the full toolset:
```json
{
"tools": [
{ "type": "agent_toolset_20260401" }
]
}
```
### Per-Tool Configuration
Override defaults for individual tools. This example enables everything except bash:
```json
{
"tools": [
{
"type": "agent_toolset_20260401",
"default_config": { "enabled": true },
"configs": [
{ "name": "bash", "enabled": false }
]
}
]
}
```
| Field | Required | Description |
|---|---|---|
| `type` | Yes | `"agent_toolset_20260401"` |
| `default_config` | No | Applied to all tools. `{ "enabled": bool, "permission_policy": {...} }` |
| `configs` | No | Per-tool overrides: `[{ "name": "...", "type": "...", "enabled": bool, "permission_policy": {...} }]`. `name` identifies the tool (values from the table above); `type` is optional in requests (same value as `name`; the server infers it) and always present in responses. `web_search` / `web_fetch` entries also accept web settings - see § Web search & web fetch settings below. |
> **Typed SDKs:** each `configs` entry is a member of a union with one member per built-in tool (eight: `BetaManagedAgentsWebFetchToolConfigParams`, `...WebSearchToolConfigParams`, `...BashToolConfigParams`, ...), discriminated by `type`. Python/TypeScript/Ruby dicts and hashes with just `name` + `enabled` + `permission_policy` are unchanged. In Go, Java, C#, and PHP, `configs` is the union itself - build each entry from its per-tool type (Go: `BetaManagedAgentsAgentToolConfigUnionParamsUnion{OfWebFetch: &anthropic.BetaManagedAgentsWebFetchToolConfigParams{...}}` - the arms are `OfBash` / `OfRead` / `OfWrite` / `OfEdit` / `OfGlob` / `OfGrep` / `OfWebFetch` / `OfWebSearch`; Java: `.addConfig(BetaManagedAgentsWebFetchToolConfigParams.builder()...build())`; C#: `new BetaManagedAgentsWebFetchToolConfigParams { Enabled = false }`; PHP: `BetaManagedAgentsWebFetchToolConfigParams::with(enabled: false)`). Code written against an SDK where all tools shared one config type must update how it constructs entries.
### Permission Policies
Control when server-executed tools (agent toolset + MCP) run automatically vs wait for approval. Does not apply to custom tools.
| Policy | Behavior |
|---|---|
| `always_allow` | Tool executes automatically (default) |
| `always_ask` | Session emits `session.status_idle` and pauses until you send a `user.tool_confirmation` event |
```json
{
"type": "agent_toolset_20260401",
"default_config": {
"enabled": true,
"permission_policy": { "type": "always_allow" }
},
"configs": [
{ "name": "bash", "permission_policy": { "type": "always_ask" } }
]
}
```
**Responding to `always_ask`:** Send a `user.tool_confirmation` event with `tool_use_id` from the triggering `agent_tool_use`/`mcp_tool_use` event:
```json
{ "type": "user.tool_confirmation", "tool_use_id": "sevt_abc123", "result": "allow" }
{ "type": "user.tool_confirmation", "tool_use_id": "sevt_def456", "result": "deny", "message": "Read .env.example instead" }
```
The optional `message` on a deny is delivered to the agent so it can adjust its approach.
To enable only specific tools, flip the default off and opt-in per tool:
```json
{
"tools": [
{
"type": "agent_toolset_20260401",
"default_config": { "enabled": false },
"configs": [
{ "name": "bash", "enabled": true },
{ "name": "read", "enabled": true }
]
}
]
}
```
### Web search & web fetch settings (domain filters)
`web_search` and `web_fetch` run on Anthropic's servers regardless of environment type, so an environment's `networking` policy **does not** govern them (see `shared/managed-agents-environments.md` -> Networking). To control what they can reach, set `allowed_domains` (only these hosts) **or** `blocked_domains` (never these hosts) - never both on one entry - on the tool's `configs` entry. Each tool carries its own list. Organization-level web search/fetch settings in the Console apply to the Messages API only, not to Managed Agents sessions.
```json
{
"type": "agent_toolset_20260401",
"configs": [
{
"type": "web_search",
"name": "web_search",
"allowed_domains": ["docs.example.com", "arxiv.org"],
"user_location": { "type": "approximate", "country": "US", "timezone": "America/Los_Angeles" }
},
{
"type": "web_fetch",
"name": "web_fetch",
"blocked_domains": ["ads.example.com"],
"max_content_tokens": 50000
}
]
}
```
| Setting | Applies to | Description |
|---|---|---|
| `allowed_domains` | `web_search`, `web_fetch` | The only hosts the tool can reach. Mutually exclusive with `blocked_domains` on the same entry. |
| `blocked_domains` | `web_search`, `web_fetch` | Hosts the tool cannot reach. |
| `max_content_tokens` | `web_fetch` | Positive integer cap on fetched *text* content entering context (binary content such as PDFs is not capped). |
| `user_location` | `web_search` | `{ "type": "approximate", city?, region?, country? (2-letter uppercase ISO 3166-1), timezone? (IANA) }` - at least one of the optional fields. |
**Run-time behavior:** a `web_fetch` call outside its list returns an error result to the agent (`is_error: true` on `agent.tool_result`, content names `url_not_allowed`); `web_search` silently omits results outside its list. In the Console, the agent form has allow/block-list controls for the web tools; `user_location` and `max_content_tokens` are set in the agent's **Raw** view.
**Domain list rules** (violations -> 400 `invalid_request_error` on agent create/update and on session create/update that supplies `tools`; messages name the list and zero-based index, e.g. `allowed_domains.0: IP addresses are not supported...`):
- 1-64 domains per list, each 1-255 chars. Empty list is rejected - omit the field or send `null` for "no restriction". Duplicates within a list are rejected.
- Plain hostname only: `example.com`, not `https://example.com`, `example.com:443`, or `*.example.com`. Case-insensitive; a single trailing `/` is ignored.
- A listed domain covers itself **and its subdomains** (`example.com` covers `docs.example.com`; `docs.example.com` does not cover `example.com` or `api.example.com`). `www.` is an ordinary subdomain - list the bare domain to cover both.
- Rejected: IP addresses in any form; bare TLDs/registry suffixes (`com`, `co.uk`); single-label names (`intranet`); `localhost` and hosts ending in `.localhost`, `.local`, `.internal`, `.localdomain`, `.invalid`; non-ASCII (use `xn--` Punycode).
- `web_fetch` domains cannot carry a path. `web_search` domains may carry a path suffix (`example.com/blog`, no spaces / `?` / `#` / `$ , | ^ !`), but the provider matches it as a URL pattern - prefer plain hostnames.
- Provider-dependent rejections at the same time: a domain Anthropic's crawler may not access, an unsupported `user_location.country` (message ends `not a country the search provider supports`), an invalid IANA `timezone`.
The session re-checks the config when it first initializes the tool; if a previously accepted setting is no longer valid it emits `session.error` and goes `idle` without retrying. Fix via a session tools update (`shared/managed-agents-core.md` -> Updating the agent configuration mid-session), update the agent too so new sessions get the fix, then send a new `user.message`.
**Multiagent layering** (see `shared/managed-agents-multiagent.md`): every list on the path to a thread applies at once - a roster agent is bound by its own lists, by those of every agent that called it, and by the coordinator's *current* lists. Allow-lists intersect and block-lists union, so a roster agent can narrow but never widen. Disjoint allow-lists leave the tool available but every call fails `url_not_allowed` (the tool description tells the model) - keep roster allow-lists inside the coordinator's. `max_content_tokens` and `user_location` are **not** combined: own value -> caller's -> coordinator's. `{"type": "self"}` entries follow the coordinator. The outcome grader (`shared/managed-agents-outcomes.md`) runs without the web tools. Updating an idle session's tools changes the coordinator's lists for every thread from its next turn; a roster agent's own lists stay as defined at session create.
**vs. the Messages API `web_search_20260209` / `web_fetch_20260209` tools:** same `allowed_domains` / `blocked_domains` vocabulary, but 64-entry cap, no path on `web_fetch` domains, and no `max_uses`, `citations`, or `cache_control`. If migrating from Messages API, these move from per-request to once-on-the-agent.
### Custom Tools (Client-Side)
Custom tools are executed by **your application**, not Anthropic. The flow:
1. Agent decides to use the tool -> session emits a `agent.custom_tool_use` event with inputs
2. Session goes `idle` waiting for you
3. Your application executes the tool
4. You send back a `user.custom_tool_result` event with the output
5. Session resumes `running`
No permission policy needed - you're the one executing.
```json
{
"tools": [
{
"type": "custom",
"name": "get_weather",
"description": "Fetch current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" }
},
"required": ["city"]
}
}
]
}
```
### MCP Servers
MCP (Model Context Protocol) servers expose standardized third-party capabilities (e.g. Asana, GitHub, Linear). **Configuration is split across agent and vault:**
1. **Agent creation** declares which servers to connect to (`type`, `name`, `url` - no auth). The agent's `mcp_servers` array has no auth field.
2. **Vault** stores the OAuth credentials. Attach via `vault_ids` on session create.
This keeps secrets out of reusable agent definitions. Each vault credential is tied to one MCP server URL; Anthropic matches credentials to servers by URL.
**Agent side - declare servers (no auth):**
| Field | Required | Description |
|---|---|---|
| `type` | Yes | `"url"` |
| `name` | Yes | Unique name - referenced by `mcp_toolset.mcp_server_name` |
| `url` | Yes | The MCP server's endpoint URL (Streamable HTTP transport) |
```json
{
"mcp_servers": [
{ "type": "url", "name": "linear", "url": "https://mcp.linear.app/mcp" }
],
"tools": [
{ "type": "mcp_toolset", "mcp_server_name": "linear" }
]
}
```
**Session side - attach vault:**
```json
{
"agent": "agent_abc123",
"environment_id": "env_abc123",
"vault_ids": ["vlt_abc123"]
}
```
> Tip: **Per-tool enablement:** `mcp_toolset` accepts `default_config: {enabled: false}` + `configs: [{name, enabled: true}]` for an allowlist pattern. MCP `configs` entries take **only** `name` (the bare tool name as the server reports it), `enabled`, and `permission_policy` - no `type` field and none of the web settings that `web_search` / `web_fetch` accept in the agent toolset.
> Tip: **Changing tools/MCP servers on a running session:** `sessions.update()` can replace `agent.tools` and `agent.mcp_servers` while the session is `idle` - a session-local override that doesn't touch the agent object. `vault_ids` is create-only. See `shared/managed-agents-core.md` -> Updating the agent configuration mid-session.
**Large tool outputs.** If a tool returns more than **100,000 characters (roughly 25,000 tokens)**, the output is automatically offloaded to a file in the sandbox - the agent receives a truncated preview plus the file path and can `read` the full content. No configuration required. The threshold is in *characters*, not tokens, and applies to built-in agent tools as well as MCP tools.
**Invalid vault credentials don't block session creation.** If a vault credential is invalid for a declared MCP server, the session still creates successfully; a `session.error` event describes the MCP auth failure, and auth retries on the next `session.status_idle` -> `session.status_running` transition.
> Warning: **MCP auth tokens != REST API tokens.** Hosted MCP servers (`mcp.notion.com`, `mcp.linear.app`, etc.) typically require **OAuth bearer tokens**, not the service's native API keys. A Notion `ntn_` integration token authenticates against Notion's REST API but will **not** work as a vault credential for the Notion MCP server. These are different auth systems.
### Vaults - the credential store
**Vaults** store credentials that Anthropic manages on your behalf. Two credential categories:
- **MCP credentials** (`mcp_oauth`, `static_bearer`) - keyed by `mcp_server_url`. When the agent connects to a server at that URL, the token is injected automatically. **Matching is normalized, not byte-exact:** scheme and host are lowercased, and default ports and trailing slashes are stripped, so host casing, an explicit default port, or a trailing slash won't break the match. A different path, subdomain, or *non-default* port will. If nothing matches, the connection is attempted unauthenticated. `mcp_oauth` tokens are auto-refreshed via the standard OAuth 2.0 `refresh_token` grant. This is the only way to authenticate MCP servers.
- **Environment variables** (`environment_variable`) - keyed by `secret_name` (the env var name). The sandbox sees only an **opaque placeholder**; the real secret is substituted into the outbound request **at egress**. Use this for any service that authenticates through an environment variable: CLIs (`aws`, `gcloud`, `stripe`), SDKs, or direct `curl` calls from the `bash` tool.
Secret fields you supply (`token`, `access_token`, `refresh_token`, `client_secret`, `secret_value`) are write-only - never returned in API responses.
#### Credentials and the sandbox
Vaults store credentials; those credentials **never enter the sandbox**. This is a deliberate security boundary - code running in the sandbox (including anything the agent writes) cannot read or exfiltrate a vaulted credential, even under prompt injection. Instead, credentials are injected by Anthropic-side proxies **after** a request leaves the sandbox:
- **MCP tool calls** are routed through an Anthropic-side proxy that fetches the credential from the vault and adds it to the outbound request.
- **Git operations on attached GitHub repositories** (`git pull`, `git push`, GitHub REST calls) are routed through a git proxy that injects the `github_repository` resource's `authorization_token` the same way.
- **Environment-variable credentials** appear in the sandbox as an opaque placeholder; the real value replaces the placeholder at egress, on requests to the credential's allowed hosts only. Substitution covers request **headers and body only** - a secret embedded in the **URL path** is never substituted, so path-secret endpoints (e.g. Slack incoming-webhook URLs) can't be vaulted; use header-based auth instead (for Slack: a bot token in `Authorization` via `chat.postMessage`).
**When vault credentials don't fit** (e.g. self-hosted sandboxes - `environment_variable` is not yet supported there), **register a custom tool:** the agent emits `agent.custom_tool_use`, your orchestrator (which already holds the credential) executes the call and returns `user.custom_tool_result` over the same authenticated event stream. No public endpoint is exposed; the sandbox never sees the secret. See `shared/managed-agents-client-patterns.md` -> Pattern 9.
**Do not put API keys in the system prompt or user messages as a workaround** - they persist in the session's event history.
> Formerly known internally as TATs (Tool/Tenant Access Tokens).
**Flow:**
1. Create a vault (`client.beta.vaults.create(...)`) - one per tenant/user, or one shared, depending on your model
2. Add credentials to it (`client.beta.vaults.credentials.create(...)`) - MCP credentials are keyed by MCP server URL; environment-variable credentials by `secret_name`
3. Reference the vault on session create via `vault_ids: ["vlt_..."]`
4. Anthropic auto-refreshes OAuth tokens before they expire and substitutes secrets at runtime
**MCP OAuth credential shape**:
```json
{
"display_name": "Notion (workspace-foo)",
"auth": {
"type": "mcp_oauth",
"mcp_server_url": "https://mcp.notion.com/mcp",
"access_token": "<current access token>",
"expires_at": "2026-04-02T14:00:00Z",
"refresh": {
"refresh_token": "<refresh token>",
"client_id": "<your OAuth client_id>",
"token_endpoint": "https://api.notion.com/v1/oauth/token",
"token_endpoint_auth": { "type": "none" }
}
}
}
```
The `refresh` block is what enables auto-refresh - `token_endpoint` is where Anthropic posts the `refresh_token` grant. `token_endpoint_auth` is a discriminated union:
| `type` | Shape | Use when |
|---|---|---|
| `"none"` | `{type: "none"}` | Public OAuth client (no secret) |
| `"client_secret_basic"` | `{type: "client_secret_basic", client_secret: "..."}` | Confidential client, secret via HTTP Basic auth |
| `"client_secret_post"` | `{type: "client_secret_post", client_secret: "..."}` | Confidential client, secret in request body |
Omit `refresh` entirely if you only have an access token with no refresh capability - it'll work until it expires, then the agent loses access.
> Tip: **Getting an OAuth token.** How you obtain the initial access and refresh tokens depends on the MCP server - consult its documentation. Once you have them, store them in a vault credential using the shape above; Anthropic auto-refreshes via the `refresh.token_endpoint` from there.
**Environment-variable credential shape**:
```json
{
"display_name": "Twilio API key for sandbox",
"auth": {
"type": "environment_variable",
"secret_name": "TWILIO_API_KEY",
"secret_value": "sk-your-secret-here",
"networking": {
"type": "limited",
"allowed_hosts": ["api.twilio.com", "*.twilio.com"]
}
}
}
```
`networking.allowed_hosts` controls which outbound hosts the secret can be substituted for - `{"type": "limited", "allowed_hosts": [...]}` or `{"type": "unrestricted"}` if you can't enumerate the domains in advance. Limiting is strongly recommended: it prevents the key from ever being sent to unauthorized hosts.
**`injection_location`** (optional, sibling of `networking`) controls **where** in the outbound request the secret is substituted - `{header: bool, body: bool}`. The two are independent: `allowed_hosts` scopes *which hosts* a substituted request can target; `injection_location` scopes *which parts of the request* the secret is substituted into across all of those hosts. Most services read an API key from a request header, so `{"header": true}` is the narrower configuration - request bodies are often assembled from content the agent is working with, making the body the broader exposure surface. A placeholder in a disabled location is **neither substituted nor stripped** - the literal opaque placeholder string is sent to the third party in that location.
| Operation | `injection_location` semantics |
|---|---|
| Create credential | Omit the field entirely -> both locations enabled. Provide the object -> any field you omit defaults to `false` (`{"header": true}` creates a header-only credential). |
| Update credential | Fields **merge individually** - `{"body": false}` disables body substitution and leaves `header` unchanged. For a running session, the update takes effect on the session's next operation. |
A credential must have at least one location enabled; a create or update that would disable both returns 400, as does explicit `null` for the object or either field (omit instead). The response always returns both fields with their resolved values.
> Warning: **Credentials created in the Console are header-only by default** - unlike the API, where omitting the field enables both. If your client sends the secret in the request body (a form-encoded token request, for example), the placeholder passes through literally and the service rejects it with its own authentication error. Tick body injection in the Console form, or `POST` the credential with `{"injection_location": {"body": true}}`.
> Warning: **Two networking layers, both required.** `networking.allowed_hosts` on the credential controls which requests *use the secret*, not which requests are *allowed*. The agent must also be able to reach the domain at the **environment level** (`unrestricted`, or the host listed in the environment's `allowed_hosts` - see `shared/managed-agents-environments.md`). A domain missing from either layer means the secret-substituted request fails.
> Warning: **Client-side validation caveat.** Substitution happens at egress, not inside the sandbox - clients that validate the credential *format* locally before making a network request (e.g. a CLI that checks the key starts with `sk-`) will see the opaque placeholder and may fail at startup. If a client rejects the credential before any network call, that's why.
> Tip: **Scope the key minimally.** The agent can do anything the key allows; a key with broader permissions than the task needs increases the blast radius if the agent behaves unexpectedly.
**Not supported with self-hosted sandboxes** - `environment_variable` credentials require Anthropic-managed egress. See `shared/managed-agents-self-hosted-sandboxes.md`.
**Constraints (all credential types):**
- **Unique key per vault.** `mcp_server_url` (MCP credentials) and `secret_name` (environment-variable credentials) must be unique among active credentials in a vault; duplicates return a 409.
- **Keys are immutable.** Secret values, `display_name`, and (on environment-variable credentials) `injection_location` can be updated; to change `mcp_server_url`, `secret_name`, `token_endpoint`, or `client_id`, archive the credential and create a new one. Archiving purges the secret and frees the key for a replacement.
- **Maximum 20 credentials per vault.**
- Credentials are stored as provided and **not validated until session runtime** - an invalid credential surfaces as an authentication or downstream error during the session, which is emitted but does not block the session from continuing.
**Scoping:** Vaults are workspace-scoped. Anyone with developer+ role in the API workspace can create, read (metadata only - secrets are write-only), and attach vaults. `vault_ids` can be set at session **create** time but not via session update (the SDK docstring says "Not yet supported; requests setting this field are rejected").
---
## Skills
Skills are reusable, filesystem-based resources that provide your agent with domain-specific expertise: workflows, context, and best practices that transform general-purpose agents into specialists. Unlike prompts (conversation-level instructions for one-off tasks), skills load on-demand and eliminate the need to repeatedly provide the same guidance across multiple conversations.
Skills reach the agent two ways: **attached** through the agent's `skills` array, or **loaded from a GitHub repository** mounted on the session (see § Skills from a GitHub repository below). The agent automatically uses them when relevant to the task at hand:
| Type | What it is |
|---|---|
| **Pre-built Anthropic skills** | Common document tasks (PowerPoint, Excel, Word, PDF). Reference by name (e.g. `xlsx`). |
| **Custom skills** | Skills you've created in your organization via the Skills API. Reference by `skill_id` + optional `version`. |
**Max 20 skills per agent.** Agent creation uses `managed-agents-2026-04-01`; the separate Skills API (for managing custom skill definitions) uses `skills-2025-10-02`.
### Enabling skills on a session
Skills are attached to the **agent** definition via `agents.create()`:
```ts
const agent = await client.beta.agents.create(
{
name: "Financial Agent",
model: "claude-opus-5",
system: "You are a financial analysis agent.",
skills: [
{ type: "anthropic", skill_id: "xlsx" },
{ type: "custom", skill_id: "skill_abc123", version: "latest" },
],
}
);
```
Python:
```python
agent = client.beta.agents.create(
name="Financial Agent",
model="claude-opus-5",
system="You are a financial analysis agent.",
skills=[
{"type": "anthropic", "skill_id": "xlsx"},
{"type": "custom", "skill_id": "skill_abc123", "version": "latest"},
]
)
```
**Skill reference fields:**
| Field | Anthropic skill | Custom skill |
|---|---|---|
| `type` | `"anthropic"` | `"custom"` |
| `skill_id` | Skill name (e.g. `"xlsx"`, `"docx"`, `"pptx"`, `"pdf"`) | Skill ID from Skills API (e.g. `"skill_abc123"`) |
| `version` | `"latest"` or a specific version number | `"latest"` or a specific version number |
`version` is optional on **both** kinds and defaults to `"latest"` - it is not custom-skill-only.
### Skills from a GitHub repository
Skills can also live in your codebase. When a session mounts a repository via the `github_repository` resource (see `shared/managed-agents-environments.md` -> GitHub Repositories), the repository's root `.claude/skills` directory is scanned at session start, and each skill found becomes available to the agent: it sees each discovered skill's name, description, and sandbox path, and reads the skill's `SKILL.md` (plus any scripts/resources it ships) when a task matches.
**The agent can discover any skill in `.claude/skills/<skill-name>/`** - one directory level deep at the repository root. Skills in the following locations are not discoverable: a bare `.claude/skills/SKILL.md` (no skill directory), anything nested deeper (`.claude/skills/tools/code-review/SKILL.md`), a `skills/` directory outside `.claude`, or a `.claude/skills` inside a package subdirectory (though those can still surface when the agent reads files under that subtree). The `SKILL.md` format is the same as uploaded custom skills.
> Warning: **Repository skills are agent instructions - treat them as part of your trust boundary.** Anyone who can commit to a mounted repository (a merged external PR, a compromised dependency, a contributor) can add or edit `.claude/skills/` content, and the platform loads it at session start with no review step - where session tools like `bash` and `web_fetch` give injected instructions real capability. Only mount repositories you trust, and audit `.claude/skills/` before mounting one with external contributors.
Rules:
- **Cloud sandboxes only** - self-hosted sandboxes don't support `github_repository` resources, so they can't load repository skills.
- **Scanned once, at session start**, from the repository state checked out then (the resource's `checkout` branch/commit, else the default branch). Commits pushed mid-session are not picked up - start a new session for updated skills. Repositories added to a *running* session are not scanned either.
- **Coexists with attached skills.** If a repository skill shares a name with an attached skill (or a skill from another mounted repo), both are available, each announced with its own path.
### Skills API
| Operation | Method | Path |
| --------------------- | -------- | ----------------------------------------------- |
| Create Skill | `POST` | `/v1/skills` |
| List Skills | `GET` | `/v1/skills` |
| Get Skill | `GET` | `/v1/skills/{id}` |
| Delete Skill | `DELETE` | `/v1/skills/{id}` |
| Create Version | `POST` | `/v1/skills/{id}/versions` |
| List Versions | `GET` | `/v1/skills/{id}/versions` |
| Get Version | `GET` | `/v1/skills/{id}/versions/{version}` |
| Delete Version | `DELETE` | `/v1/skills/{id}/versions/{version}` |
# Managed Agents - Webhooks
Anthropic can POST to your HTTPS endpoint when a Managed Agents resource changes state - an alternative to holding an SSE stream or polling. Payloads are **thin** (event type + resource IDs only); on receipt, fetch the resource for current state. Every delivery is HMAC-signed.
> **Direction matters.** This page covers *Anthropic -> you* notifications about session/vault state. It does **not** cover *third-party -> you* webhooks that *trigger* a session (e.g. a GitHub push handler that calls `sessions.create()`) - that's ordinary application code on your side with no Anthropic-specific wire format.
---
## Register an endpoint (Console only)
Console -> **Manage -> Webhooks**. There is no programmatic endpoint-management API yet. Secret rotation is supported from the same page.
| Field | Constraint |
|---|---|
| URL | HTTPS on port 443, publicly resolvable hostname |
| Event types | Subscribe per `data.type` - an endpoint receives only the types it is subscribed to |
| Signing secret | `whsec_`-prefixed, 32 bytes, **shown once at creation** - store it |
---
## Verify the signature
Every delivery carries the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. **Use the SDK's `client.beta.webhooks.unwrap()`** - it verifies the signature, rejects payloads more than ~5 minutes old, and returns the parsed event. It reads the `whsec_` secret from `ANTHROPIC_WEBHOOK_SIGNING_KEY`. Pass the headers through untouched; don't hand-roll verification against a single `X-Webhook-Signature` header, which is not the wire format.
```python
import anthropic
from flask import Flask, request
client = anthropic.Anthropic() # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
try:
event = client.beta.webhooks.unwrap(
request.get_data(as_text=True),
headers=dict(request.headers),
)
except Exception:
return "invalid signature", 400
if event.id in seen_event_ids: # dedupe retries - id is per-event, not per-delivery
return "", 204
seen_event_ids.add(event.id)
match event.data.type:
case "session.status_idled":
session = client.beta.sessions.retrieve(event.data.id)
notify_user(session)
case "vault_credential.refresh_failed":
alert_oncall(event.data.id)
return "", 204
```
Pass the **raw request body** to `unwrap()` - frameworks that re-serialize JSON (Express `.json()`, Flask `.get_json()`) change the bytes and break the MAC. For other languages, look up the `beta.webhooks.unwrap` binding in the SDK repo (`shared/live-sources.md`); don't hand-roll verification.
---
## Payload envelope
```json
{
"type": "event",
"id": "whe_9d5c1f7e...",
"created_at": "2026-03-18T14:05:22Z",
"data": {
"type": "session.status_idled",
"id": "session_01XYZ...",
"organization_id": "8a3d2f1e-...",
"workspace_id": "c7b0e4d9-..."
}
}
```
Switch on `data.type`, fetch the resource by `data.id`, return any **2xx** to acknowledge. `created_at` is when the *event occurred*, not when the delivery was attempted - the `webhook-timestamp` header is the clock for the attempt (see Delivery behavior).
The top-level `id` is the same value as the `webhook-id` header, and it is per *event*, not per delivery - every retry carries it unchanged. Dedupe on it.
---
## Supported `data.type` values
| `data.type` | Fires when |
|---|---|
| `session.status_scheduled` | Session created and ready to accept events |
| `session.status_run_started` | Agent execution kicked off (every transition to `running`) |
| `session.status_idled` | Agent awaiting input (tool approval, custom tool result, or next message) - or paused at its session budget. The webhook payload is thin - list the session's events and check the latest `session.status_idle` event's `stop_reason` (the session object itself has no `stop_reason` field): if it is `budget_reached`, further `user.message` events return a 400 and only a budget change/removal resumes the session (`shared/managed-agents-core.md` § Session budgets) |
| `session.status_rescheduled` | A transient error occurred; the session is retrying automatically |
| `session.status_terminated` | Session ended - **on completion or on error**, not error-only |
| `session.thread_created` | Multiagent: coordinator opened a new subagent thread, or the session's advisor is being consulted (`shared/managed-agents-multiagent.md` -> Advisor) |
| `session.thread_idled` | Child threads only: a subagent thread is waiting for input - or paused because the session reached its budget cap. When the whole session pauses at the cap, a `session.status_idled` webhook also fires and the stream's `session.status_idle` event carries `stop_reason: budget_reached` - unless another thread is waiting on a tool ask, which outranks the cap at the session level (`shared/managed-agents-core.md` § Session budgets). |
| `session.thread_terminated` | A thread ended - child completed its work, or the thread was archived. **Child threads only**; the primary thread's end surfaces as `session.status_terminated` |
| `session.outcome_evaluation_ended` | Outcome grader finished one iteration |
| `session.updated` | Session properties changed (name, configuration) |
| `session.deleted` | Session permanently deleted - no object left to fetch; treat the event itself as final |
| `vault.archived` | Vault was archived |
| `vault.created` | Vault was created |
| `vault.deleted` | Vault was deleted - a `vault_credential.deleted` also fires per underlying credential. No object left to fetch; treat the event itself as final |
| `vault_credential.archived` | Credential archived, directly or via vault archival |
| `vault_credential.created` | Vault credential was created |
| `vault_credential.deleted` | Credential deleted, directly or via vault deletion. No object left to fetch; treat the event itself as final |
| `vault_credential.refresh_failed` | MCP OAuth vault credential failed to refresh |
| `agent.created` | Agent created |
| `agent.updated` | A new agent version was published. Updates that do not create a new version do **not** fire this. |
| `agent.archived` | Agent archived |
| `agent.deleted` | Agent permanently deleted - no object left to fetch; treat the event itself as final |
| `deployment.created` | Scheduled deployment created |
| `deployment.updated` | Deployment properties changed (e.g. schedule edited) |
| `deployment.paused` | Deployment paused - by request, or automatically when a scheduled run fails with a **non-recoverable** error (archived agent, missing environment). Recoverable failures, including rate limits, do **not** auto-pause. |
| `deployment.unpaused` | Deployment unpaused; schedule resumes |
| `deployment.archived` | Deployment archived - directly, or as a result of agent archival/deletion |
| `deployment.deleted` | Deployment permanently deleted - no object left to fetch; treat the event itself as final |
| `deployment_run.started` | A **scheduled** run started. Manual runs do **not** emit `deployment_run.*` events. |
| `deployment_run.succeeded` | Scheduled run created its session. Same `data.id` (the run ID) as the run's `.started` event - fetch the deployment run for its `session_id`, then subscribe to the session events to follow the work. |
| `deployment_run.failed` | Scheduled run did not create a session. Same `data.id` as the run's `.started` event - fetch the deployment run for `error.type` / `error.message`. |
| `environment.created` | Environment created |
| `environment.updated` | Environment updated with at least one changed field. A no-op update emits nothing. |
| `environment.archived` | Environment archived. Re-archiving an already-archived environment emits nothing. |
| `environment.deleted` | Environment deleted, including delete of an already-archived one. No object left to fetch; treat the event itself as final |
| `memory_store.created` | Memory store created - by you, or by an Anthropic-operated process that clones one of your stores |
| `memory_store.archived` | Memory store archived. Re-archiving an already-archived store emits nothing. |
| `memory_store.deleted` | Memory store deleted, including delete of an already-archived one. Cascades to its memories and versions **without** per-memory events - this single event is the signal. No object left to fetch; treat it as final |
> **There is deliberately no `memory_store.updated`.** Individual memories and memory versions emit no webhook events at all, and neither do an environment's self-hosted work items. If you need per-memory change tracking, poll the memory-versions endpoints (`shared/managed-agents-memory.md`).
> These are **webhook** `data.type` values - a separate namespace from SSE event types (`session.status_idle`, `span.outcome_evaluation_end`, etc. in `shared/managed-agents-events.md`). Don't reuse SSE constants in webhook handlers.
---
## Delivery behavior & pitfalls
- **Duplicates.** An endpoint can receive the same event more than once; every attempt carries the same top-level `event.id` (= the `webhook-id` header). Dedupe on it.
- **Subscription scope.** An event reaches only endpoints subscribed to its type **at the moment it is emitted**. An event emitted while nothing was subscribed is never delivered, and subscribing later does not backfill - subscribe before you need the type.
- **No ordering guarantee.** Events are not delivered in occurrence order: `session.status_idled` may arrive before `session.outcome_evaluation_ended`, and a `.deleted` can arrive before the `.archived` for the same resource. **Drive state from the resource you fetch, not from arrival order.**
- **Retries: up to three attempts** per endpoint per event, with jittered exponential backoff between 5 and 120 seconds. A response that triggers auto-disable is never retried. **After the last attempt fails the event is dropped** - not queued, and with no signal that it was lost. Webhooks are not a durable log: if you must observe every transition, reconcile by listing or fetching the resource.
- **`webhook-timestamp` is re-stamped on every attempt**, so retries don't fail the SDK's five-minute freshness check. It times the *delivery attempt*; use the payload's `created_at` for when the event occurred.
- **Auto-disable - three triggers**, each setting `disabled_reason`, all reversible from Console (events emitted while disabled are **not** replayed):
- A `3xx` response. Redirects are never followed; disables immediately, on the first attempt. Reason: `auto-disabled: endpoint URL returned a redirect (3xx)`.
- The URL resolves to a non-public IP at connect time. Disables immediately. Reason: `auto-disabled: endpoint URL resolved to an invalid address`.
- Continuous failure for a sustained period. Reason: `auto-disabled after sustained delivery failures`. **The trigger is duration, not a delivery count** - a single `2xx` resets the window, so one flaky event can't disable the endpoint.
- **Thin payload is intentional.** Don't expect `stop_reason` (list the session's events for that - the session object has no `stop_reason` field), `outcome_evaluations`, credential secrets, etc. on the webhook body - fetch the resource.
# Model Migration Guide
> **If you arrived via `/claude-api migrate`:** this is the right file. Execute the steps below in order - do not summarize them back to the user. Start with Step 0 (confirm scope) before touching any file.
How to move existing code to newer Claude models. Covers breaking changes, deprecated parameters, and drop-in replacements for retired models.
For the latest, authoritative version (with code samples in every supported language), WebFetch the **Migration Guide** URL from `shared/live-sources.md`. Use this file for the consolidated, skill-resident reference; fall back to the live docs whenever a model launch or breaking change may have shifted the picture.
**This file is large.** Use the section names below to jump (or `Grep` this file for the heading text). Read Step 0 and Step 1 first - they apply to every migration. Then read only the per-target section for the model you are migrating to.
| Section | When you need it |
|---|---|
| Step 0: Confirm the migration scope | Always - before any edits |
| Step 1: Classify each file | Always - decides whether to swap, add-alongside, or skip |
| Per-SDK Syntax Reference | Translate the Python examples in this guide to TypeScript / Go / Ruby / Java / C# / PHP |
| Destination Models / Retired Model Replacements | Picking a target model |
| Breaking Changes by Source Model | Migrating to Opus 4.6 / Sonnet 4.6 |
| Migrating to Opus 4.7 | Migrating to Opus 4.7 (breaking changes, silent defaults, behavioral shifts) |
| Opus 4.7 Migration Checklist | The required vs optional items for 4.7, tagged `[BLOCKS]` / `[TUNE]` |
| Migrating to Opus 4.8 | Migrating to Opus 4.8 (no new breaking changes; mid-session system prompts; behavioral re-tuning) |
| Opus 4.8 Migration Checklist | The required vs optional items for 4.8, tagged `[BLOCKS]` / `[TUNE]` |
| Migrating to Claude Opus 5 | Migrating Opus 4.8 -> Claude Opus 5 (thinking-disabled effort-gated; mid-conversation tool changes; per-turn effort and task budget; verbosity, over-verification, and scope re-tuning) |
| Claude Opus 5 Migration Checklist | The required vs optional items for Claude Opus 5, tagged `[BLOCKS]` / `[TUNE]` |
| Migrating to Claude Sonnet 5 | Migrating Sonnet 4.6 -> Claude Sonnet 5 (adaptive thinking on by default; non-default sampling params 400; new tokenizer; `xhigh` effort for coding/agentic; high-res vision; behavioral re-tuning) |
| Claude Sonnet 5 Migration Checklist | The required vs optional items, tagged `[BLOCKS]` / `[TUNE]` |
| Migrating to Claude Fable 5.1 | Migrating to Claude Fable 5.1 or Claude Mythos 5.1 (always-on thinking, raw chain of thought never returned, refusal handling, data retention, behavioral shifts + prompting guidance) |
| Claude Fable 5.1 Migration Checklist | The required vs optional items for Claude Fable 5.1, tagged `[BLOCKS]` / `[TUNE]` |
| Migrating to Claude Fable 5.1 from Claude Fable 5 | Migrating Claude Fable 5 / Claude Opus 5 / Claude Mythos 5 -> Claude Fable 5.1 or Claude Mythos 5.1 (forced `tool_choice` 400s; "preserved thinking" - model-bound blocks and the history-editing check; per-message effort; append-only per-turn reminders; `display: "updates"` progress updates; cheaper cache reads; behavioral re-tuning) |
| Claude Fable 5.1 from Claude Fable 5 Migration Checklist | The required vs optional items for the Claude Fable 5 -> Claude Fable 5.1 move, tagged `[BLOCKS]` / `[TUNE]` |
| Verify the Migration | After edits - runtime spot-check |
**TL;DR:** Change the model ID string. If you were using `budget_tokens`, switch to `thinking: {type: "adaptive"}`. If you were using assistant prefills, they 400 on both Opus 4.6 and Sonnet 4.6 - switch to one of the prefill replacements (most often `output_config.format`; see the table in Breaking Changes by Source Model). If you're moving from Sonnet 4.5 to Sonnet 4.6, set `effort` explicitly - 4.6 defaults to `high`. Remove the `effort-2025-11-24` and `fine-grained-tool-streaming-2025-05-14` beta headers (GA on 4.6); remove `interleaved-thinking-2025-05-14` once you're on adaptive thinking (keep it only while using the transitional `budget_tokens` escape hatch). Then drop back from `client.beta.messages.create` to `client.messages.create`. Dial back any aggressive "CRITICAL: YOU MUST" tool instructions; 4.6 follows the system prompt much more closely.
---
## Step 0: Confirm the migration scope
**Before any Write, Edit, or MultiEdit call, confirm the scope.** If the user's request does not explicitly name a single file, a specific directory, or an explicit file list, **ask first - do not start editing**. This is non-negotiable: even imperative-sounding requests like "migrate my codebase", "move my project to X", "upgrade to Sonnet 4.6", or bare "migrate to Opus 4.7" leave the scope ambiguous and require a clarifying question. Phrases like "my project", "my code", "my codebase", "the whole thing", "everywhere", or "across the repo" are **ambiguous, not directive** - they tell you *what* to do but not *where*. Ask before doing.
Offer the common scopes explicitly and wait for the answer before touching any file:
1. The entire working directory
2. A specific subdirectory (e.g. `src/`, `app/`, `services/billing/`)
3. A specific file or a list of files
Surface this as a single clarifying question so the user can answer in one turn. **Proceed without asking only when the scope is already unambiguous** - the user named an exact file ("migrate `extract.py` to Sonnet 4.6"), pointed at a specific directory ("migrate everything under `services/billing/` to Opus 4.6"), listed specific files ("update `a.py` and `b.py`"), or already answered the scope question in an earlier turn. If you can answer the question "which files is this change going to touch?" with a precise list from the prompt alone, proceed. If not, ask.
**Worked example.** If the user says *"Move my project to Opus 4.6. I want adaptive thinking everywhere it makes sense."* you do not know whether "my project" means the whole working directory, just `src/`, just the production code, or something else - the `everywhere` makes the intent clear (update every call site *within scope*) but the scope itself is still not defined. Do not start editing. Respond with:
> Before I start editing, can you confirm the scope? I can migrate:
> 1. Every `.py` file in the working directory
> 2. Just the files under `src/` (production code)
> 3. A specific subdirectory or list of files you name
>
> Which one?
Then wait for the answer. The same applies to *"Migrate to Opus 4.7"* and bare *"Help me upgrade to Sonnet 4.6"* - ask before editing.
**Sizing the scope question (large repos).** Before asking, get a per-directory count so the user can pick concretely:
```sh
rg -l "<old-model-id>" --type-not md | cut -d/ -f1 | sort | uniq -c | sort -rn
```
Present the breakdown in your scope question (e.g. *"Found 217 references across 3 directories: api/ (130), api-go/ (62), routing/ (25). Which to migrate?"*). Also confirm `git status` is clean before surveying - unexpected modifications mean a concurrent process; stop and investigate before proceeding.
---
## Step 1: Classify each file
Not every file that contains the old model ID is a **caller** of the API. Before editing, classify each file into one of these buckets - the right action differs:
| # | Bucket | What it looks like | Action |
|---|---|---|---|
| 1 | **Calls the API/SDK** | `client.messages.create(model=...)`, `anthropic.Anthropic()`, request payloads | Swap the model ID **and** apply the breaking-change checklist for the target version (below). |
| 2 | **Defines or serves the model** | Model registries, OpenAPI specs, routing/queue configs, model-policy enums, generated catalogs | The old entry **stays** (the model is still served). Ask whether to (a) add the new model alongside, (b) leave alone, or (c) retire the old model - never blind-replace. **If you can't ask, default to (a): add the new model alongside and flag it** - replacing would de-register a model that's still in production. |
| 3 | **References the ID as an opaque string** | UI fallback constants, capability-gate substring checks, generic test fixtures, label parsers, env defaults | Usually swap the string and verify any parser/regex/substring match handles the new ID - but check the sub-cases below first. |
| 4 | **Suffixed variant ID** | `claude-<model>-<suffix>` like `-fast`, `-1024k`, `-200k`, `[1m]`, dated snapshots | These are deployment/routing identifiers, not the public model ID. **Do not assume a new-model equivalent exists.** Verify in the registry first; if absent, leave the string alone and flag it. **Exception: `-fast` strings (e.g. `claude-opus-4-6-fast`) are handled by the Fast Mode section below**, which rewrites them to Opus 4.8 plus `speed="fast"` and the `fast-mode-2026-02-01` beta rather than leaving them in place. |
**Bucket 3 sub-cases - before swapping a string reference, check:**
- **Capability gate** (e.g. `if 'opus-4-6' in model_id:` enables a feature) -> **add the new ID alongside**, don't replace. The old model is still served and still has the capability, so replacing would silently disable the feature for any old-model traffic that still flows through. If you know no old-model traffic will hit this gate (single-caller codebase fully migrating), replacing is fine; if unsure, add alongside.
- **Registry-assert test** (e.g. `assert "claude-X" in supported_models`, `test_X_has_N_clusters`) -> **add an assertion for the new model alongside; keep the old one.** The old model is still served, so its assertion stays valid - but the registry should also include the new model, so assert that too. Heuristic: if the test references multiple model versions in a list, it's a registry test; if one model in a struct compared only to itself, it's a generic fixture.
- **Frozen / generated snapshot** -> **regenerate**, don't hand-edit.
- **Coupled to a definer** (e.g. an integration test that passes model authorization via a shared `conftest` seed list, or asserts on a billing-tier / rate-limit-group enum or a generated SKU/pricing catalog) -> **verify the definer has a new-model entry first.** If not, add a seed entry (reusing the nearest existing tier as a placeholder); if you can't confidently do that, ask the user how to populate the definer. **Do not skip the test.** Swapping without populating the definer will make the test fail at runtime.
When migrating tests specifically: breaking parameters (`temperature`, `top_p`, `budget_tokens`) are usually absent - test fixtures rarely set sampling params on placeholder models. The breaking-change scan is still required, but expect mostly clean results.
**Find intentionally-flagged sync points first.** Many codebases tag spots that must change at every model launch with comment markers like `MODEL LAUNCH`, `KEEP IN SYNC`, `@model-update`, or similar. Grep for whatever convention the repo uses *before* the broad model-ID grep - those markers point at the load-bearing changes.
---
## Per-SDK Syntax Reference
Code examples in this guide are Python. **The same fields exist in every official Anthropic SDK** - Stainless generates all 7 from the same OpenAPI spec, so JSON field names map 1:1 with only case-convention differences. Use the rows below to translate the Python examples to the SDK you are migrating.
> **Verify type and method names against the SDK source before writing them into customer code.** WebFetch the relevant repository from the SDK source-code table in `shared/live-sources.md` (one row per SDK) and confirm the exact symbol - particularly for typed SDKs (Go, Java, C#) where union/builder names can differ from the JSON shape. Do not guess type names that aren't in the table below or in `<lang>/claude-api/README.md`.
### `thinking` - `budget_tokens` -> adaptive
| SDK | Before | After |
|---|---|---|
| Python | `thinking={"type": "enabled", "budget_tokens": N}` | `thinking={"type": "adaptive"}` |
| TypeScript | `thinking: { type: 'enabled', budget_tokens: N }` | `thinking: { type: 'adaptive' }` |
| Go | `Thinking: anthropic.ThinkingConfigParamOfEnabled(N)` | `Thinking: anthropic.ThinkingConfigParamUnion{OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}}` |
| Ruby | `thinking: { type: "enabled", budget_tokens: N }` | `thinking: { type: "adaptive" }` |
| Java | `.thinking(ThinkingConfigEnabled.builder().budgetTokens(N).build())` | `.thinking(ThinkingConfigAdaptive.builder().build())` |
| C# | `Thinking = new ThinkingConfigEnabled { BudgetTokens = N }` | `Thinking = new ThinkingConfigAdaptive()` |
| PHP | `thinking: ['type' => 'enabled', 'budget_tokens' => N]` | `thinking: ['type' => 'adaptive']` |
### Sampling parameters - `temperature` / `top_p` / `top_k`
(Remove the field entirely on Opus 4.7; on Claude 4.x keep at most one of `temperature` or `top_p`.)
| SDK | Field(s) to remove |
|---|---|
| Python | `temperature=...`, `top_p=...`, `top_k=...` |
| TypeScript | `temperature: ...`, `top_p: ...`, `top_k: ...` |
| Go | `Temperature: anthropic.Float(...)`, `TopP: anthropic.Float(...)`, `TopK: anthropic.Int(...)` |
| Ruby | `temperature: ...`, `top_p: ...`, `top_k: ...` |
| Java | `.temperature(...)`, `.topP(...)`, `.topK(...)` |
| C# | `Temperature = ...`, `TopP = ...`, `TopK = ...` |
| PHP | `temperature: ...`, `topP: ...`, `topK: ...` |
### Prefill replacement - structured outputs via `output_config.format`
| SDK | Remove (last assistant turn) | Add |
|---|---|---|
| Python | `{"role": "assistant", "content": "..."}` | `output_config={"format": {"type": "json_schema", "schema": SCHEMA}}` |
| TypeScript | `{ role: 'assistant', content: '...' }` | `output_config: { format: { type: 'json_schema', schema: SCHEMA } }` |
| Go | trailing `anthropic.MessageParam{Role: "assistant", ...}` | `OutputConfig: anthropic.OutputConfigParam{Format: anthropic.JSONOutputFormatParam{...}}` |
| Ruby | `{ role: "assistant", content: "..." }` | `output_config: { format: { type: "json_schema", schema: SCHEMA } }` |
| Java | trailing `Message.builder().role(ASSISTANT)...` | `.outputConfig(OutputConfig.builder().format(JsonOutputFormat.builder()...build()).build())` |
| C# | trailing `new Message { Role = "assistant", ... }` | `OutputConfig = new OutputConfig { Format = new JsonOutputFormat { ... } }` |
| PHP | trailing `['role' => 'assistant', 'content' => '...']` | `outputConfig: ['format' => ['type' => 'json_schema', 'schema' => $SCHEMA]]` |
### `thinking.display` - opt back into summarized reasoning (Opus 4.7)
| SDK | Add |
|---|---|
| Python | `thinking={"type": "adaptive", "display": "summarized"}` |
| TypeScript | `thinking: { type: 'adaptive', display: 'summarized' }` |
| Go | `Thinking: anthropic.ThinkingConfigParamUnion{OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized}}` |
| Ruby | `thinking: { type: "adaptive", display: "summarized" }` (or `display_:` when constructing the model class directly) |
| Java | `.thinking(ThinkingConfigAdaptive.builder().display(ThinkingConfigAdaptive.Display.SUMMARIZED).build())` |
| C# | `Thinking = new ThinkingConfigAdaptive { Display = Display.Summarized }` |
| PHP | `thinking: ['type' => 'adaptive', 'display' => 'summarized']` |
For any field not in these tables, the JSON key in the Python example translates directly: `snake_case` for Python/TypeScript/Ruby, `camelCase` named args for PHP, `PascalCase` struct fields for Go/C#, `camelCase` builder methods for Java.
---
## Explain every change you make
Migration edits often look arbitrary to a user who hasn't read the release notes - a removed `temperature`, a deleted prefill, a rewritten system-prompt sentence. **For each edit, tell the user what you changed and why**, tied to the specific API or behavioral change that motivates it. Do this in your summary as you work, not just at the end.
Be especially explicit about **system-prompt edits**. Users are rightly protective of their prompts, and prompt-tuning changes are judgment calls (not hard API requirements). For any prompt edit:
- Quote the before and after text.
- State the behavioral shift that motivates it (e.g. *"Opus 4.7 calibrates response length to task complexity, so I added an explicit length instruction"*, or *"4.6 follows instructions more literally, so 'CRITICAL: YOU MUST use the search tool' will now overtrigger - softened to 'Use the search tool when...'"*).
- Make clear which prompt edits are **optional tuning** (tone, length, subagent guidance) versus which code edits are **required to avoid a 400** (sampling params, `budget_tokens`, prefills). Never present an optional prompt change as mandatory.
If you're applying several prompt-tuning edits at once, offer them as a short list the user can accept or decline item-by-item rather than silently rewriting their system prompt.
---
## Before You Migrate
1. **Confirm the target model ID.** Use only the exact strings from `shared/models.md` - do not append date suffixes to aliases (`claude-opus-4-6`, not `claude-opus-4-6-20251101`). Guessing an ID will 404.
2. **Check which features your code uses** with this checklist:
- `thinking: {type: "enabled", budget_tokens: N}` -> migrate to adaptive thinking on Opus 4.6 / Sonnet 4.6 (still functional but deprecated)
- Assistant-turn prefills (`messages` ending with `role: "assistant"`) -> must change on Opus 4.6 / Sonnet 4.6 (returns 400)
- `output_format` parameter on `messages.create()` -> must change on all models (deprecated API-wide)
- `max_tokens > ~16000` -> must stream on any model (above ~16K risks SDK HTTP timeouts). When streaming, every current model reaches 128K except Haiku 4.5, which caps at 64K
- Beta headers `effort-2025-11-24`, `fine-grained-tool-streaming-2025-05-14`, `interleaved-thinking-2025-05-14` -> GA on 4.6, remove them and switch from `client.beta.messages.create` to `client.messages.create`
- Moving Sonnet 4.5 -> Sonnet 4.6 with no `effort` set -> 4.6 defaults to `high`, which may change your latency/cost profile
- System prompts with `CRITICAL`, `MUST`, `If in doubt, use X` language -> likely to overtrigger on 4.6 (see Prompt-Behavior Changes)
- Coming from 3.x / 4.0 / 4.1: also check sampling params (`temperature` + `top_p`), tool versions (`text_editor_20250728`), `refusal` + `model_context_window_exceeded` stop reasons, trailing-newline tool-param handling
3. **Test on a single request first.** Run one call against the new model, inspect the response, then roll out.
---
## Destination Models (recommended targets)
| If you're on... | Migrate to | Why |
| ------------------------------------- | ------------------ | ------------------------------------------------- |
| Claude Mythos Preview (`claude-mythos-preview`) | `claude-mythos-5-1` (Project Glasswing successor) or `claude-fable-5-1` (GA) | Same tokenizer family - mostly a model-ID swap; remove `thinking` config and prefill; see Migrating to Claude Fable 5.1 |
| Claude Fable 5 (`claude-fable-5`) | `claude-fable-5-1` | Same tier, same per-token price, same tokenizer; three breaking changes (forced `tool_choice` 400s, "preserved thinking") - see Migrating to Claude Fable 5.1 from Claude Fable 5 |
| Claude Mythos 5 (`claude-mythos-5`) | `claude-mythos-5-1` | Same path as claude-fable-5 -> claude-fable-5-1; see § Claude Mythos 5.1 under Migrating to Claude Fable 5.1 from Claude Fable 5 |
| Opus 4.8 | `claude-opus-5` | The current Opus. Two breaking changes (thinking on by default; disabling thinking capped at `high` effort) plus prompt re-tuning - see Migrating to Claude Opus 5 |
| Opus 4.7 | `claude-opus-5` | Apply the Opus 4.8 section (prompt re-tuning, no new breaking changes), then the Claude Opus 5 section |
| Opus 4.6 | `claude-opus-5` | Apply the Opus 4.7 breaking changes, then 4.8 re-tuning, then the Claude Opus 5 section |
| Opus 4.0 / 4.1 / 4.5 / Opus 3 | `claude-opus-5` | Apply 4.6 -> 4.7 -> 4.8 -> Claude Opus 5 in order (adaptive thinking, drop sampling params, then re-tune) |
| Sonnet 4.6 | `claude-sonnet-5` | Near-Opus quality on agentic and coding work at Sonnet cost; adaptive thinking on by default; see Migrating to Claude Sonnet 5 |
| Sonnet 4.0 / 4.5 / 3.7 / 3.5 | `claude-sonnet-5` | Apply the Sonnet 4.6 changes first, then the Claude Sonnet 5 section |
| Haiku 3 / 3.5 | `claude-haiku-4-5` | Fastest and most cost-effective |
Default to the latest Opus for the caller's tier unless they explicitly chose otherwise. The Opus migrations layer: if you're on Opus 4.6 or older, apply each version's section in order up to your target (e.g. 4.5 -> 4.8 means the 4.6, 4.7, and 4.8 sections in sequence). A 4.7 -> 4.8 move has no new breaking changes - see Migrating to Opus 4.8 below.
---
## Retired Model Replacements
These models return 404 - update immediately:
| Retired model | Retired | Drop-in replacement |
| ----------------------------- | ------------- | -------------------- |
| `claude-3-7-sonnet-20250219` | Feb 19, 2026 | `claude-sonnet-5` |
| `claude-3-5-haiku-20241022` | Feb 19, 2026 | `claude-haiku-4-5` |
| `claude-3-opus-20240229` | Jan 5, 2026 | `claude-opus-4-8` |
| `claude-3-5-sonnet-20241022` | Oct 28, 2025 | `claude-sonnet-5` |
| `claude-3-5-sonnet-20240620` | Oct 28, 2025 | `claude-sonnet-5` |
| `claude-3-sonnet-20240229` | Jul 21, 2025 | `claude-sonnet-5` |
| `claude-2.1`, `claude-2.0` | Jul 21, 2025 | `claude-sonnet-5` |
## Deprecated Models (retiring soon)
| Model | Retires | Replacement |
| ----------------------------- | ------------- | -------------------- |
| `claude-3-haiku-20240307` | Apr 19, 2026 | `claude-haiku-4-5` |
| `claude-opus-4-20250514` | June 15, 2026 | `claude-opus-4-8` |
| `claude-sonnet-4-20250514` | June 15, 2026 | `claude-sonnet-5` |
---
## Breaking Changes by Source Model
### Migrating from Sonnet 4.5 to Sonnet 4.6 (effort default change)
Sonnet 4.5 had no `effort` parameter; Sonnet 4.6 defaults to `high`. If you just switch the model string and do nothing else, you may see noticeably higher latency and token usage. Set `effort` explicitly.
**Recommended starting points:**
| Workload | Start at | Notes |
| ------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------- |
| Chat, classification, content generation | `low` | With `thinking: {"type": "disabled"}` you'll see similar or better performance vs. Sonnet 4.5 no-thinking |
| Most applications (balanced) | `medium` | The default sweet spot for quality vs. cost |
| Agentic coding, tool-heavy workflows | `medium` | Pair with adaptive thinking and a generous `max_tokens` (up to 128K with streaming - Sonnet 4.6's ceiling) |
| Autonomous multi-step agents, long-horizon loops | `high` | Scale down to `medium` if latency/tokens become a concern |
| Computer-use agents | `high` + adaptive | Sonnet 4.6's best computer-use accuracy is on adaptive + high |
For non-thinking chat workloads specifically:
```python
client.messages.create(
model="claude-sonnet-4-6",
max_tokens=8192,
thinking={"type": "disabled"},
output_config={"effort": "low"},
messages=[{"role": "user", "content": "..."}],
)
```
**When to use Opus 4.6 instead:** hardest and longest-horizon problems - large code migrations, deep research, extended autonomous work. Sonnet 4.6 wins on fast turnaround and cost efficiency.
### Migrating to Opus 4.6 / Sonnet 4.6 (from any older model)
**1. Manual extended thinking is deprecated - use adaptive thinking.**
`thinking: {type: "enabled", budget_tokens: N}` (manual extended thinking with a fixed token budget) is deprecated on Opus 4.6 and Sonnet 4.6. Replace it with `thinking: {type: "adaptive"}`, which lets Claude decide when and how much to think. Adaptive thinking also enables interleaved thinking automatically (no beta header needed).
```python
# Old (still works on older models, deprecated on 4.6)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=16000,
thinking={"type": "enabled", "budget_tokens": 8000},
messages=[...]
)
# New (Opus 4.6 / Sonnet 4.6)
response = client.messages.create(
model="claude-opus-4-6", # or "claude-sonnet-4-6"
max_tokens=16000,
thinking={"type": "adaptive"},
output_config={"effort": "high"}, # optional: low | medium | high | max
messages=[...]
)
```
Adaptive thinking is the long-term target, and on internal evaluations it outperforms manual extended thinking. Move when you can.
**Transitional escape hatch:** manual extended thinking is still *functional* on Opus 4.6 and Sonnet 4.6 (deprecated, will be removed in a future release). If you need a hard ceiling while migrating - for example, to bound token spend on a runaway workload before you've tuned `effort` - you can keep `budget_tokens` around alongside an explicit `effort` value, then remove it in a follow-up. `budget_tokens` must be strictly less than `max_tokens`:
```python
# Transitional only - deprecated, plan to remove
client.messages.create(
model="claude-sonnet-4-6",
max_tokens=16384,
thinking={"type": "enabled", "budget_tokens": 8192}, # must be < max_tokens
output_config={"effort": "medium"},
messages=[...],
)
```
If the user asks for a "thinking budget" on 4.6, the preferred answer is `effort` - use `low`, `medium`, `high`, or `max` rather than a token count.
**2. Effort parameter (Opus 4.5, Opus 4.6, Sonnet 4.6 only).**
Controls thinking depth and overall token spend. Goes inside `output_config`, not top-level. Default is `high`. `max` is supported on Fable 5, Opus 4.6 and later, Sonnet 5, and Sonnet 4.6 - it errors on Sonnet 4.5 and Haiku 4.5.
```python
output_config={"effort": "medium"} # often the best cost / quality balance
```
### Migrating to the 4.6 family (Opus 4.6 and Sonnet 4.6)
**3. Assistant-turn prefills return 400 (Opus 4.6 and Sonnet 4.6).**
Prefilled responses on the final assistant turn are no longer supported on either Opus 4.6 or Sonnet 4.6 - both return a 400. Adding assistant messages *elsewhere* in the conversation (e.g., for few-shot examples) still works. Pick the replacement that matches what the prefill was doing:
| Prefill was used for | Replacement |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Forcing JSON / YAML / schema output | `output_config.format` with a `json_schema` - see example below |
| Forcing a classification label | Tool with an enum field containing valid labels, or structured outputs |
| Skipping preambles (`Here is the summary:\n`) | System prompt instruction: *"Respond directly without preamble. Do not start with phrases like 'Here is...' or 'Based on...'."* |
| Steering around bad refusals | Usually no longer needed - 4.6 refuses far more appropriately. Plain user-turn prompting is sufficient. |
| Continuing an interrupted response | Move continuation into the user turn: *"Your previous response was interrupted and ended with `[last text]`. Continue from there."* |
| Injecting reminders / context hydration | Inject into the user turn instead. For complex agent harnesses, expose context via a tool call or during compaction. |
```python
# Old (fails on Opus 4.6 / Sonnet 4.6) - prefill forcing JSON shape
messages=[
{"role": "user", "content": "Extract the name."},
{"role": "assistant", "content": "{\"name\": \""},
]
# New - structured outputs replace the prefill
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
output_config={"format": {"type": "json_schema", "schema": {...}}},
messages=[{"role": "user", "content": "Extract the name."}],
)
```
**4. Stream for `max_tokens > ~16K` (all models); only Haiku 4.5 caps lower, at 64K.**
Non-streaming requests hit SDK HTTP timeouts at high `max_tokens`, regardless of model - stream for anything above ~16K output. The streamable ceiling is 128K for every current model except Haiku 4.5, which caps at 64K.
```python
with client.messages.stream(model="claude-opus-4-6", max_tokens=64000, ...) as stream:
message = stream.get_final_message()
```
**5. Tool-call JSON escaping may differ (Opus 4.6 and Sonnet 4.6).**
Both 4.6 models can produce tool call `input` fields with Unicode or forward-slash escaping. Always parse with `json.loads()` / `JSON.parse()` - never raw-string-match the serialized input.
### All models
**6. `output_format` -> `output_config.format` (API-wide).**
The old top-level `output_format` parameter on `messages.create()` is deprecated. Use `output_config.format` instead. This is not 4.6-specific - applies to every model.
---
## Beta Headers to Remove on 4.6
Several beta headers that were required on 4.5 are now GA on 4.6 and should be removed. Leaving them in is harmless but misleading; removing them also lets you move from `client.beta.messages.create(...)` back to `client.messages.create(...)`.
| Header | Status on 4.6 | Action |
| ----------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------- |
| `effort-2025-11-24` | Effort parameter is GA | Remove |
| `fine-grained-tool-streaming-2025-05-14` | GA | Remove |
| `interleaved-thinking-2025-05-14` | Adaptive thinking enables interleaved thinking automatically | Remove when using adaptive thinking; still functional on Sonnet 4.6 *with* manual extended thinking, but that path is deprecated |
| `token-efficient-tools-2025-02-19` | Built in to all Claude 4+ models | Remove (no effect) |
| `output-128k-2025-02-19` | Built in to Claude 4+ models | Remove (no effect) |
Once you remove all of these and finish moving to adaptive thinking, you can switch the SDK call site from the beta namespace back to the regular one:
```python
# Before
response = client.beta.messages.create(
model="claude-opus-4-5",
betas=["interleaved-thinking-2025-05-14", "effort-2025-11-24"],
...
)
# After
response = client.messages.create(
model="claude-opus-4-6",
thinking={"type": "adaptive"},
output_config={"effort": "high"},
...
)
```
---
## Additional Changes When Coming from 3.x / 4.0 / 4.1 -> 4.6
If you're jumping from Opus 4.1, Sonnet 4, Sonnet 3.7, or an older Claude 3.x model directly to 4.6, apply everything above *plus* the items in this section. Users already on Opus 4.5 / Sonnet 4.5 can skip this.
**1. Sampling parameters: `temperature` OR `top_p`, not both.**
Passing both will error on every Claude 4+ model:
```python
# Old (3.x only - errors on 4+)
client.messages.create(temperature=0.7, top_p=0.9, ...)
# New
client.messages.create(temperature=0.7, ...) # or top_p, not both
```
**2. Update tool versions.**
Legacy tool versions are not supported on 4+. **Both the `type` and the `name` field change** - `text_editor_20250728` and `str_replace_based_edit_tool` are a pair; updating one without the other 400s. Also remove the `undo_edit` command from your text-editor integration:
| Old | New |
| ------------------------------------------------- | ------------------------------------------------------- |
| `text_editor_20250124` + `str_replace_editor` | `text_editor_20250728` + `str_replace_based_edit_tool` |
| `code_execution_*` (earlier versions) | `code_execution_20260521` |
| `undo_edit` command | *(no longer supported - delete call sites)* |
```python
# Before
tools = [{"type": "text_editor_20250124", "name": "str_replace_editor"}]
# After - BOTH fields change
tools = [{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}]
```
**3. Handle the `refusal` stop reason.**
Claude 4+ can return `stop_reason: "refusal"` on the response. If your code only handles `end_turn` / `tool_use` / `max_tokens`, add a branch:
```python
if response.stop_reason == "refusal":
# Surface the refusal to the user; do not retry with the same prompt
...
```
**4. Handle the `model_context_window_exceeded` stop reason (4.5+).**
Distinct from `max_tokens`: it means the model hit the *context window* limit, not the requested output cap. Handle both:
```python
if response.stop_reason == "model_context_window_exceeded":
# Context window exhausted - compact or split the conversation
...
elif response.stop_reason == "max_tokens":
# Requested output cap hit - retry with higher max_tokens or stream
...
```
**5. Trailing newlines preserved in tool call string parameters (4.5+).**
4.5 and 4.6 preserve trailing newlines that older models stripped. If your tool implementations do exact string matching against tool-call `input` values (e.g., `if name == "foo"`), verify they still match when the model sends `"foo\n"`. Normalizing with `.rstrip()` on the receiving side is usually the simplest fix.
**6. Haiku: rate limits reset between generations.**
Haiku 4.5 has its own rate-limit pool separate from Haiku 3 / 3.5. If you're ramping traffic as you migrate, check your tier's Haiku 4.5 limits at [API rate limits](https://platform.claude.com/docs/en/api/rate-limits) - a quota that comfortably served Haiku 3.5 traffic may need a tier bump for the same volume on 4.5.
---
## Prompt-Behavior Changes (Opus 4.5 / 4.6, Sonnet 4.6)
These don't break your code, but prompts that worked on 4.5-and-earlier may over- or under-trigger on 4.6. Tune as needed. For a standing, model-general audit of dated prompt text beyond this migration - skills and tool descriptions included - read `shared/prompt-audit.md` (or invoke `/claude-api prompt-audit`).
**1. Aggressive instructions cause overtriggering.** Opus 4.5 and 4.6 follow the system prompt much more closely than earlier models. Prompts written to *overcome* the old reluctance are now too aggressive:
| Before (worked on 4.0 / 4.5) | After (use on 4.6) |
| ------------------------------------------- | ----------------------------------------- |
| `CRITICAL: You MUST use this tool when...` | `Use this tool when...` |
| `Default to using [tool]` | `Use [tool] when it would improve X` |
| `If in doubt, use [tool]` | *(delete - no longer needed)* |
If the model is now overtriggering a tool or skill, the fix is almost always to dial back the language, not to add more guardrails.
**2. Overthinking and excessive exploration (Opus 4.6).** At higher `effort` settings, Opus 4.6 explores more before answering. If that burns too many thinking tokens, lower `effort` first (`medium` is often the sweet spot) before adding prose instructions to constrain reasoning.
**3. Overeager subagent spawning (Opus 4.6).** Opus 4.6 has a strong preference for delegating to subagents. If you see it spawning a subagent for something a direct `grep` or `read` would solve, add guidance: *"Use subagents only for parallel or independent workstreams. For single-file reads or sequential operations, work directly."*
**4. Overengineering (Opus 4.5 / 4.6).** Both models may add extra files, abstractions, or defensive error handling beyond what was asked. If you want minimal changes, prompt for it explicitly: *"Only make changes directly requested. Don't add helpers, abstractions, or error handling for scenarios that can't happen."*
**5. LaTeX math output (Opus 4.6).** Opus 4.6 defaults to LaTeX (`\frac{}{}`, `$...$`) for math and technical content. If you need plain text, instruct it explicitly: *"Format all math as plain text - no LaTeX, no `$`, no `\frac{}{}`. Use `/` for division and `^` for exponents."*
**6. Skipped verbal summaries (4.6 family).** The 4.6 models are more concise and may skip the summary paragraph after a tool call, jumping straight to the next action. If you rely on those summaries for visibility, add: *"After completing a task that involves tool use, provide a brief summary of what you did."*
**7. "Think" as a trigger word (Opus 4.5 with thinking disabled).** When `thinking` is off, Opus 4.5 is particularly sensitive to the word *think* and may reason more than you want. Use `consider`, `evaluate`, or `reason through` instead.
---
## Model-ID Rename Quick Reference
| Old string (migration source) | New string |
| ------------------------------ | ------------------ |
| `claude-opus-4-8` | `claude-opus-5` |
| `claude-opus-4-7` | `claude-opus-5` |
| `claude-opus-4-6` | `claude-opus-5` |
| `claude-opus-4-5` | `claude-opus-5` |
| `claude-opus-4-1` | `claude-opus-5` |
| `claude-opus-4-0` | `claude-opus-5` |
| `claude-mythos-preview` | `claude-mythos-5-1` (Project Glasswing) or `claude-fable-5-1` |
| `claude-fable-5` | `claude-fable-5-1` |
| `claude-mythos-5` | `claude-mythos-5-1` |
| `claude-sonnet-4-6` | `claude-sonnet-5`|
| `claude-sonnet-4-5` | `claude-sonnet-5`|
| `claude-sonnet-4-0` | `claude-sonnet-5`|
Older aliases (`claude-opus-4-7`, `claude-opus-4-6`, `claude-opus-4-5`, `claude-sonnet-4-6`, `claude-sonnet-4-5`, etc.) are still active and can be pinned if you need time before upgrading - see `shared/models.md` for the full legacy list.
### Amazon Bedrock model IDs
If the code uses the `AnthropicBedrockMantle` client (Python `anthropic[bedrock]`, TypeScript `@anthropic-ai/bedrock-sdk`, Java `BedrockMantleBackend`, Go `bedrock.NewMantleClient`, etc.) or targets `https://bedrock-mantle.{region}.api.aws/anthropic`, it is running on **Claude in Amazon Bedrock**. All breaking changes in this guide apply unchanged there - it serves the same Messages API shape - but model IDs carry an `anthropic.` provider prefix:
| First-party ID | Bedrock ID |
|---|---|
| `claude-opus-4-8` | `anthropic.claude-opus-4-8` |
| `claude-opus-5` | `anthropic.claude-opus-5` |
| `claude-fable-5-1` | `anthropic.claude-fable-5-1` |
| `claude-fable-5` | `anthropic.claude-fable-5` |
| `claude-mythos-5-1` | `anthropic.claude-mythos-5-1` (us-east-1 only, not publicly listed) |
| `claude-opus-4-7` | `anthropic.claude-opus-4-7` |
| `claude-sonnet-5` | `anthropic.claude-sonnet-5` |
| `claude-haiku-4-5` | `anthropic.claude-haiku-4-5` |
When migrating a Bedrock file, apply the same rename-table row as first-party, then keep/add the `anthropic.` prefix. Do **not** generate a first-party `claude-*` ID for a Bedrock client - it will 400.
**Skip for Bedrock:** the `code_execution_*` tool-version checklist item and the **Task Budgets** section - neither is available on Bedrock (see `shared/platform-availability.md` for the per-feature table). Everything else in this guide - `effort`, adaptive/extended thinking, `output_config.format`, `thinking.display`, fine-grained tool streaming, token counting - is available on Bedrock.
> **Out of scope:** the legacy Amazon Bedrock integration (`InvokeModel` / `Converse` APIs with ARN-versioned IDs like `anthropic.claude-3-5-sonnet-20241022-v2:0`) uses a different request shape and model-ID format. This guide does not cover it; WebFetch the Bedrock page in `shared/live-sources.md` if the user is migrating between the two Bedrock integrations.
### Claude Platform on AWS
If the code uses `AnthropicAWS` / `AnthropicAws` / `anthropicaws.NewClient` / `AnthropicAwsClient` (or targets `https://aws-external-anthropic.{region}.api.aws`), it is running on **Claude Platform on AWS** - Anthropic-operated, same-day API parity. Model IDs are **bare first-party** strings; apply the rename table above **verbatim** and every breaking-change section in this guide unchanged. There is nothing to skip. Do **not** add an `anthropic.` prefix (that's Amazon Bedrock, a separate offering). See `shared/claude-platform-on-aws.md` for client/auth details.
---
## Migration Checklist
Every item is tagged: **`[BLOCKS]`** items cause a 400 error, infinite loop, silent timeout, or wrong tool selection if missed - apply these as code edits, not as suggestions. **`[TUNE]`** items are quality/cost adjustments.
For each file that calls `messages.create()` / equivalent SDK method:
- [ ] **[BLOCKS]** Update the `model=` string to the new alias
- [ ] **[BLOCKS]** Replace `budget_tokens` with `thinking={"type": "adaptive"}` (deprecated on Opus 4.6 / Sonnet 4.6)
- [ ] **[BLOCKS]** Move `format` from top-level `output_format` into `output_config.format`
- [ ] **[BLOCKS]** Remove any assistant-turn prefills if targeting Opus 4.6 or Sonnet 4.6 (see the prefill replacement table)
- [ ] **[BLOCKS]** Switch to streaming if `max_tokens > ~16000` (otherwise SDK HTTP timeout)
- [ ] **[TUNE]** Verify tool-input handling parses JSON rather than raw-string-matching the serialized input (4.6 may escape Unicode / forward slashes differently; most SDKs already expose `block.input` as a parsed object)
- [ ] **[TUNE]** Set `output_config={"effort": "..."}` explicitly - especially when moving Sonnet 4.5 -> Sonnet 4.6 (4.6 defaults to `high`)
- [ ] **[TUNE]** Remove GA beta headers: `effort-2025-11-24`, `fine-grained-tool-streaming-2025-05-14`, `token-efficient-tools-2025-02-19`, `output-128k-2025-02-19`; remove `interleaved-thinking-2025-05-14` once on adaptive thinking
- [ ] **[TUNE]** Switch `client.beta.messages.create(...)` -> `client.messages.create(...)` once all betas are removed
- [ ] **[TUNE]** Review system prompt for aggressive tool language (`CRITICAL:`, `MUST`, `If in doubt`) and dial it back
**Extra items when coming from 3.x / 4.0 / 4.1:**
- [ ] **[BLOCKS]** Remove either `temperature` or `top_p` (passing both 400s on Claude 4+)
- [ ] **[BLOCKS]** Update text-editor tool `type` to `text_editor_20250728`
- [ ] **[BLOCKS]** Update text-editor tool `name` to `str_replace_based_edit_tool` - **changing only the `type` and keeping `name: "str_replace_editor"` returns a 400**
- [ ] **[BLOCKS]** Update code-execution tool to `code_execution_20260521`
- [ ] **[BLOCKS]** Delete any `undo_edit` command call sites
- [ ] **[TUNE]** Add handling for `stop_reason == "refusal"`
- [ ] **[TUNE]** Add handling for `stop_reason == "model_context_window_exceeded"` (4.5+)
- [ ] **[TUNE]** Verify tool-param string matching tolerates trailing newlines (preserved on 4.5+)
- [ ] **[TUNE]** If moving to Haiku 4.5: review rate-limit tier (separate pool from Haiku 3.x)
**Verification:**
- [ ] Run one test request and inspect `response.stop_reason`, `response.usage`, and whether tool-use / thinking behavior matches expectations
For cached prompts: the render order and hash inputs did not change, so existing `cache_control` breakpoints keep working. However, **changing the model string invalidates the existing cache** - the first request on the new model will write the cache fresh.
---
## Migrating to Opus 4.7
> **Model ID `claude-opus-4-7` is authoritative as written here.** When the user asks to migrate to Opus 4.7, write `model="claude-opus-4-7"` exactly. Do **not** WebFetch to verify - this guide is the source of truth for migration target IDs. The corresponding entry exists in `shared/models.md`.
Claude Opus 4.7 was Anthropic's most capable model at its launch and is now the previous-generation Opus (Opus 4.8 is current - see Migrating to Opus 4.8 below). It is highly autonomous and performs exceptionally well on long-horizon agentic work, knowledge work, vision tasks, and memory tasks. This section summarizes everything that was new at the 4.7 launch and remains the layered breaking-change path for callers coming from Opus 4.6 or older. It is layered on top of the 4.6 migration above - if the caller is jumping from Opus 4.5 or older, apply the 4.6 changes first, then this section, then the 4.8 section.
**TL;DR for someone already on Opus 4.6:** update the model ID to `claude-opus-4-7`, strip any remaining `budget_tokens` and sampling parameters (both 400 on Opus 4.7), give `max_tokens` extra headroom and re-baseline with `count_tokens()` against the new model, opt back into `thinking.display: "summarized"` if reasoning is surfaced to users, and re-tune `effort` - it matters more on 4.7 than on any prior Opus.
### Breaking changes (will 400 on Opus 4.7)
**Extended thinking removed.**
`thinking: {type: "enabled", budget_tokens: N}` is no longer supported on Claude Opus 4.7 or later models and returns a 400 error. Switch to adaptive thinking (`thinking: {type: "adaptive"}`) and use the effort parameter to control thinking depth. Adaptive thinking is **off by default** on Claude Opus 4.7: requests with no `thinking` field run without thinking, matching Opus 4.6 behavior. Set `thinking: {type: "adaptive"}` explicitly to enable it.
```python
# Before (Opus 4.6)
client.messages.create(
model="claude-opus-4-6",
max_tokens=64000,
thinking={"type": "enabled", "budget_tokens": 32000},
messages=[{"role": "user", "content": "..."}],
)
# After (Opus 4.7)
client.messages.create(
model="claude-opus-4-7",
max_tokens=64000,
thinking={"type": "adaptive"},
output_config={"effort": "high"}, # or "max", "xhigh", "medium", "low"
messages=[{"role": "user", "content": "..."}],
)
```
If the caller wasn't using extended thinking, no change is required - thinking is off by default, or can be set explicitly with `thinking={"type": "disabled"}`.
Delete `budget_tokens` plumbing entirely. For the replacement `effort` value, see **Choosing an effort level on Opus 4.7** below - there is no exact 1:1 mapping from `budget_tokens`.
**Sampling parameters removed.**
The `temperature`, `top_p`, and `top_k` parameters are no longer accepted on Claude Opus 4.7. Requests that include them return a 400 error. Remove these fields from your request payloads. Prompting is the recommended way to guide model behavior on Claude Opus 4.7. If you were using `temperature = 0` for determinism, note that it never guaranteed identical outputs on prior models.
```python
# Before - errors on Opus 4.7
client.messages.create(temperature=0.7, top_p=0.9, ...)
# After
client.messages.create(...) # no sampling params
```
- **If the intent was determinism** - use `effort: "low"` with a tighter prompt.
- **If the intent was creative variance** - the prompt replacement depends on the use case; **ask the user** how they want variance elicited. If you can't ask, add a use-case-appropriate instruction along the lines of *"choose something off-distribution and interesting"* - e.g. for text generation, *"Vary your phrasing and structure across responses"*; for frontend/design, use the propose-4-directions approach under **Design and frontend coding** below.
### Choosing an effort level on Opus 4.7
`budget_tokens` controlled how much to *think*; `effort` controls how much to think *and* act, so there is no exact 1:1 mapping. **Use `xhigh` for best results in coding and agentic use cases, and a minimum of `high` for most intelligence-sensitive use cases.** Experiment with other levels to further tune token usage and intelligence:
| Level | Use when | Notes |
| --- | --- | --- |
| `max` | Intelligence-demanding tasks worth testing at the ceiling | Can deliver gains in some use cases but may show diminishing returns from increased token usage; can be prone to overthinking |
| `xhigh` | **Most coding and agentic use cases** | The best setting for these; used as the default in Claude Code |
| `high` | Intelligence-sensitive use cases generally | Balances token usage and intelligence; recommended minimum for most intelligence-sensitive work |
| `medium` | Cost-sensitive use cases that need to reduce token usage while trading off intelligence | |
| `low` | Short, scoped tasks and latency-sensitive workloads that are not intelligence-sensitive | |
### Silent default changes (no error, but behavior differs)
**Thinking content omitted by default.**
Thinking blocks still appear in the response stream on Claude Opus 4.7, but their `thinking` field is empty unless you explicitly opt in. This is a silent change from Claude Opus 4.6, where the default was to return summarized thinking text. To restore summarized thinking content on Claude Opus 4.7, set `thinking.display` to `"summarized"`. **The block-field name is unchanged** - it is still `block.thinking` on a `thinking`-type block; do not rename it.
**Detect this:** any code that reads `block.thinking` (or equivalent) from a `thinking`-type block and renders it in a UI, log, or trace. **The fix is the request parameter, not the response handling** - add `display: "summarized"` to the `thinking` parameter:
```python
thinking={"type": "adaptive", "display": "summarized"} # "display" is new on Opus 4.7; values: "omitted" (default) | "summarized"
```
The default is `"omitted"` on Claude Opus 4.7. If thinking content was never surfaced anywhere, no change needed. If your product streams reasoning to users, the new default appears as a long pause before output begins; set `display: "summarized"` to restore visible progress during thinking.
**Updated token counting.**
Claude Opus 4.7 and Claude Opus 4.6 count tokens differently. The same input text produces a higher token count on Claude Opus 4.7 than on Claude Opus 4.6, and `/v1/messages/count_tokens` will return a different number of tokens for Claude Opus 4.7 than it did for Claude Opus 4.6. The token efficiency of Claude Opus 4.7 can vary by workload shape. Prompting interventions, `task_budget`, and `effort` can help control costs and ensure appropriate token usage. Keep in mind that these controls may trade off model intelligence. **Update your `max_tokens` parameters to give additional headroom, including compaction triggers.** Claude Opus 4.7 provides a 1M context window at standard API pricing with no long-context premium.
What else to check:
- Client-side token estimators (tiktoken-style approximations) calibrated against 4.6
- Cost calculators that multiply tokens by a fixed per-token rate
- Rate-limit retry thresholds keyed to measured token counts
Re-baseline by re-running `client.messages.count_tokens()` against `claude-opus-4-7` on a representative sample of the caller's prompts. Do not apply a blanket multiplier. For cost-sensitive workloads, consider reducing `effort` by one level (e.g. `high` -> `medium`). For agentic loops, consider adopting Task Budgets (below).
### New feature: Task Budgets (beta)
Opus 4.7 introduces **task budgets** - tell Claude how many tokens it has for a full agentic loop (thinking + tool calls + final output). The model sees a running countdown and uses it to prioritize work and wrap up gracefully as the budget is consumed.
This is a **suggestion the model is aware of**, not a hard cap. It is distinct from `max_tokens`, which remains the enforced per-response limit and is *not* surfaced to the model. Use `task_budget` when you want the model to self-moderate; use `max_tokens` as a hard ceiling to cap usage.
Requires beta header `task-budgets-2026-03-13`:
```python
client.beta.messages.create(
betas=["task-budgets-2026-03-13"],
model="claude-opus-4-7",
max_tokens=64000,
thinking={"type": "adaptive"},
output_config={
"effort": "high",
"task_budget": {"type": "tokens", "total": 128000},
},
messages=[...],
)
```
Set a generous budget for open-ended agentic tasks and tighten it for latency-sensitive ones. **Minimum `task_budget.total` is 20,000 tokens.** If the budget is too restrictive for the task, the model may complete it less thoroughly, referencing its budget as the constraint. **Do not add `task_budget` during a migration unless you are sure the budget value is right** - if you can run the workload and measure, do so; otherwise ask the user for the value rather than guessing. This is the primary lever for offsetting the token-counting shift on agentic workloads.
### Capability improvements
**High-resolution vision.** Opus 4.7 is the first Claude model with high-resolution image support. Maximum image resolution is **2576 pixels on the long edge** (up from 1568px on Opus 4.6 and prior). This unlocks gains on vision-heavy workloads, especially computer use and screenshot/artifact/document understanding. Coordinates returned by the model now map 1:1 to actual image pixels, so no scale-factor math is needed.
High-res support is **automatic on Opus 4.7** - no beta header, no client-side opt-in required. The model accepts larger inputs and returns pixel-accurate coordinates out of the box.
**Token cost.** Full-resolution images on Opus 4.7 can use up to ~3× more image tokens than on prior models (up to ~4784 tokens per image, vs. the previous ~1,600-token cap). If the extra fidelity isn't needed, downsample client-side before sending to control cost - but **do not add downsampling by default during a migration**. If you're not sure whether the pipeline needs the fidelity, ask the user rather than guessing. Use `count_tokens()` on representative images on Opus 4.7 to re-baseline before reacting to any measured cost shift.
Beyond resolution, Opus 4.7 also improves on low-level perception (pointing, measuring, counting) and natural-image bounding-box localization and detection.
**Knowledge work.** Meaningful gains on tasks where the model visually verifies its own output - `.docx` redlining, `.pptx` editing, and programmatic chart/figure analysis (e.g. pixel-level data transcription via image-processing libraries). If prompts have scaffolding like *"double-check the slide layout before returning"*, try removing it and re-baselining.
**Memory.** Opus 4.7 is better at writing and using file-system-based memory. If an agent maintains a scratchpad, notes file, or structured memory store across turns, that agent should improve at jotting down notes to itself and leveraging its notes in future tasks.
**User-facing progress updates.** Opus 4.7 provides more regular, higher-quality interim updates during long agentic traces. If the system prompt has scaffolding like *"After every 3 tool calls, summarize progress"*, try removing it to avoid excessive user-facing text. If the length or contents of Opus 4.7's updates are not well-calibrated to your use case, explicitly describe what these updates should look like in the prompt and provide examples.
### Real-time cybersecurity safeguards
Requests that involve prohibited or high-risk topics may lead to refusals.
### Fast Mode: Claude Opus 5 / Opus 4.8 only
Fast mode is available on Claude Opus 5 and Opus 4.8. Only surface this if the caller's code actually uses fast mode (e.g. `model="claude-opus-4-6-fast"`, or `speed="fast"` on an unsupported model); if the word "fast" does not appear in the code, say nothing about Fast Mode.
When you see `model="claude-opus-4-6-fast"` (or any retired `-fast` model string), **the migration edit is** to move the fast-mode traffic onto Claude Opus 5, the current fast-capable default (Opus 4.8 also works if the caller is staying on that tier):
```python
# Request fast mode on Claude Opus 5.
client.beta.messages.create(
model="claude-opus-5", max_tokens=4096,
speed="fast", betas=["fast-mode-2026-02-01"],
messages=[...],
)
```
That is: switch the model to Claude Opus 5 (or Opus 4.8) and request fast mode the supported way, using the beta `client.beta.messages....` endpoint, the `fast-mode-2026-02-01` beta flag, and `speed="fast"` as a top-level request parameter (per-language form in SKILL.md § Fast Mode). Opus 4.7 fast mode has also been removed, so do not land on Opus 4.7 either. Do **not** leave the code on a retired `-fast` model string - the failure mode differs by version: `claude-opus-4-6-fast` is retired and the API **silently falls back** to standard Opus 4.6 (no error - the caller loses fast-mode speed without noticing); `claude-opus-4-7-fast` and `speed="fast"` on Opus 4.7 instead return an **API error** (hard failure - requests break outright rather than degrading). Either way, migrate to Opus 4.8 fast mode now.
### Behavioral shifts (prompt-tunable)
These don't break anything, but prompts tuned for Opus 4.6 may land differently. Opus 4.7 is more steerable than 4.6, so small prompt nudges usually close the gap.
**More literal instruction following.** Claude Opus 4.7 interprets prompts more literally and explicitly than Claude Opus 4.6, particularly at lower effort levels. It will not silently generalize an instruction from one item to another, and it will not infer requests you didn't make. The upside of this literalism is precision and less thrash. It generally performs better for API use cases with carefully tuned prompts, structured extraction, and pipelines where you want predictable behavior. A prompt and harness review may be especially helpful for migration to Claude Opus 4.7.
**Verbosity calibrates to task complexity.** Opus 4.7 scales response length to how complex it judges the task to be, rather than defaulting to a fixed verbosity - shorter answers on simple lookups, much longer on open-ended analysis. If the product depends on a particular length or style, tune the prompt explicitly. To reduce verbosity:
> *"Provide concise, focused responses. Skip non-essential context, and keep examples minimal."*
If you see specific kinds of over-verbosity (e.g. over-explaining), add instructions targeting those. Positive examples showing the desired level of concision tend to be more effective than negative examples or instructions telling the model what not to do. Do **not** assume existing "be concise" instructions should be removed - test first.
**Tone and writing style.** Opus 4.7 is more direct and opinionated, with less validation-forward phrasing and fewer emoji than Opus 4.6's warmer style. As with any new model, prose style on long-form writing may shift. If the product relies on a specific voice, re-evaluate style prompts against the new baseline. If a warmer or more conversational voice is wanted, specify it:
> *"Use a warm, collaborative tone. Acknowledge the user's framing before answering."*
**`effort` matters more than on any prior Opus.** Opus 4.7 respects `effort` levels more strictly, especially at the low end. At `low` and `medium` it scopes work to what was asked rather than going above and beyond - good for latency and cost, but on moderate tasks at `low` there is some risk of under-thinking.
- If shallow reasoning shows up on complex problems, raise `effort` to `high` or `xhigh` rather than prompting around it.
- If `effort` must stay `low` for latency, add targeted guidance: *"This task involves multi-step reasoning. Think carefully through the problem before responding."*
- **At `xhigh` or `max`, set a large `max_tokens`** so the model has room to think and act across tool calls and subagents. Start at 64K and tune from there. (`xhigh` is a new effort level on Opus 4.7, between `high` and `max`.)
Adaptive-thinking triggering is also steerable. If the model thinks more often than wanted - which can happen with large or complex system prompts - add: *"Thinking adds latency and should only be used when it will meaningfully improve answer quality - typically for problems that require multi-step reasoning. When in doubt, respond directly."*
**Uses tools less often by default.** Opus 4.7 tends to use tools less often than 4.6 and to use reasoning more. This produces better results in most cases, but for products that rely on tools (search/retrieval, function-calling, computer-use steps), it can drop tool-use rate. Two levers:
- **Raise `effort`** - `high` or `xhigh` show substantially more tool usage in agentic search and coding, and are especially useful for knowledge work.
- **Prompt for it** - be explicit in tool descriptions or the system prompt about when and how to use the tool, and encourage the model to err on the side of using it more often:
> *"When the answer depends on information not present in the conversation, you MUST call the `search` tool before answering - do not answer from prior knowledge."*
**Fewer subagents by default.** Opus 4.7 tends to spawn fewer subagents than 4.6. This is steerable - give explicit guidance on when delegation is desirable. For a coding agent, for example:
> *"Do NOT spawn a subagent for work you can complete directly in a single response (e.g. refactoring a function you can already see). Spawn multiple subagents in the same turn when fanning out across items or reading multiple files."*
**Design and frontend coding.** Opus 4.7 has stronger design instincts than 4.6, with a consistent default house style: warm cream/off-white backgrounds (around `#F4F1EA`), serif display type (Georgia, Fraunces, Playfair), italic word-accents, and a terracotta/amber accent. This reads well for editorial, hospitality, and portfolio briefs, but will feel off for dashboards, dev tools, fintech, healthcare, or enterprise apps - and it appears in slide decks as well as web UIs.
The default is persistent. Generic instructions ("don't use cream," "make it clean and minimal") tend to shift the model to a different fixed palette rather than producing variety. Two approaches work reliably:
1. **Specify a concrete alternative.** The model follows explicit specs precisely - give exact hex values, typefaces, and layout constraints.
2. **Have the model propose options before building.** This breaks the default and gives the user control:
> *"Before building, propose 4 distinct visual directions tailored to this brief (each as: bg hex / accent hex / typeface - one-line rationale). Ask the user to pick one, then implement only that direction."*
If the caller previously relied on `temperature` for design variety, use approach (2) - it produces meaningfully different directions across runs.
Opus 4.7 also requires less frontend-design prompting than previous models to avoid generic "AI slop" aesthetics. Where earlier models needed a lengthy anti-slop snippet, Opus 4.7 generates distinctive, creative frontends with a much shorter nudge. This snippet works well alongside the variety approaches above:
> *"NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white or dark backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. Use unique fonts, cohesive colors and themes, and animations for effects and micro-interactions."*
**Interactive coding products.** Opus 4.7's token usage and behavior can differ between autonomous, asynchronous coding agents with a single user turn and interactive, synchronous coding agents with multiple user turns. Specifically, it tends to use more tokens in interactive settings, primarily because it reasons more after user turns. This can improve long-horizon coherence, instruction following, and coding capabilities in long interactive coding sessions, but also comes with more token usage. To maximize both performance and token efficiency in coding products, use `effort: "xhigh"` or `"high"`, add autonomous features (like an auto mode), and reduce the number of human interactions required from users.
When limiting required user interactions, specify the task, intent, and relevant constraints upfront in the first human turn. Well-specified, clear, and accurate task descriptions upfront help maximize autonomy and intelligence while minimizing extra token usage after user turns - because Opus 4.7 is more autonomous than prior models, this usage pattern helps to maximize performance. In contrast, ambiguous or underspecified prompts conveyed progressively over multiple user turns tend to reduce token efficiency and sometimes performance.
**Code review.** Opus 4.7 is meaningfully better at finding bugs than prior models, with both higher recall and precision. However, if a code-review harness was tuned for an earlier model, it may initially show *lower* recall - this is likely a harness effect, not a capability regression. When a review prompt says "only report high-severity issues," "be conservative," or "don't nitpick," Opus 4.7 follows that instruction more faithfully than earlier models did: it investigates just as thoroughly, identifies the bugs, and then declines to report findings it judges to be below the stated bar. Precision rises, but measured recall can fall even though underlying bug-finding has improved.
Recommended prompt language:
> *"Report every issue you find, including ones you are uncertain about or consider low-severity. Do not filter for importance or confidence at this stage - a separate verification step will do that. Your goal here is coverage: it is better to surface a finding that later gets filtered out than to silently drop a bug. For each finding, include your confidence level and an estimated severity so a downstream filter can rank them."*
This can be used without an actual second step, but moving confidence filtering out of the finding step often helps. If the harness has a separate verification/dedup/ranking stage, tell the model explicitly that its job at the finding stage is coverage, not filtering. If single-pass self-filtering is wanted, be concrete about the bar rather than using qualitative terms like "important" - e.g. *"report any bugs that could cause incorrect behavior, a test failure, or a misleading result; only omit nits like pure style or naming preferences."* Iterate on prompts against a subset of evals to validate recall or F1 gains.
**Computer use.** Computer use works across resolutions up to the new 2576px / 3.75MP maximum. Sending images at **1080p** provides a good balance of performance and cost. For particularly cost-sensitive workloads, **720p** or **1366×768** are lower-cost options with strong performance. Test to find the ideal settings for the use case; experimenting with `effort` can also help tune behavior.
---
## Opus 4.7 Migration Checklist
Every item is tagged: **`[BLOCKS]`** items cause a 400 error, infinite loop, silent truncation, or empty output if missed - apply these as code edits, not as suggestions. **`[TUNE]`** items are quality/cost adjustments - surface them to the user as recommendations.
`[BLOCKS]` items prefixed with **"If..."** or **"At..."** are conditional. Before working through the list, **scan the file** for the conditions: does it surface thinking text to a UI/log? Does it set `output_config.effort` to `"x-high"` or `"max"`? Is it a security workload? Is it a multi-turn agentic loop? Apply only the items whose condition matches.
- [ ] **[BLOCKS]** Replace `thinking: {type: "enabled", budget_tokens: N}` with `thinking: {type: "adaptive"}` + `output_config.effort`; delete `budget_tokens` plumbing entirely
- [ ] **[BLOCKS]** Strip `temperature`, `top_p`, `top_k` from request construction
- [ ] **[BLOCKS]** If thinking content is surfaced to users or stored in logs: add `thinking.display: "summarized"` (otherwise the rendered text is empty)
- [ ] **[BLOCKS]** At `output_config.effort` of `xhigh` or `max`: set `max_tokens` >= 64000 (otherwise output truncates mid-thought)
- [ ] **[TUNE]** Give `max_tokens` and compaction triggers extra headroom; re-run `count_tokens()` against `claude-opus-4-7` on representative prompts to re-baseline (no blanket multiplier)
- [ ] **[TUNE]** Re-baseline cost and rate-limit dashboards *before* reacting to measured shifts
- [ ] **[TUNE]** Re-evaluate `effort` per route - use `xhigh` for coding/agentic and a minimum of `high` for most intelligence-sensitive work; it matters more on 4.7 than any prior Opus
- [ ] **[TUNE]** Multi-turn agentic loops: adopt the API-native Task Budgets (`output_config.task_budget`, beta `task-budgets-2026-03-13`, minimum 20k tokens) - this is for capping *cumulative* spend across a loop; per-turn depth is `effort`
- [ ] **[TUNE]** Check for ambiguous or underspecified instructions that relied on 4.6 generalizing intent, and update them to be clearer or more precise - 4.7 follows them literally
- [ ] **[TUNE]** Tool-use workloads: add explicit when/how-to-use guidance to tool descriptions (4.7 reaches for tools less often)
- [ ] **[TUNE]** Verbosity: test existing length instructions before changing them - 4.7 calibrates length to task complexity, so tune for the desired output rather than assuming a direction
- [ ] **[TUNE]** Remove forced-progress-update scaffolding (*"after every N tool calls..."*)
- [ ] **[TUNE]** Remove knowledge-work verification scaffolding (*"double-check the slide layout..."*) and re-baseline
- [ ] **[TUNE]** Add tone instruction if a warmer / more conversational voice is needed; re-evaluate style prompts on writing-heavy routes
- [ ] **[TUNE]** Subagent tool present: add explicit spawn / don't-spawn guidance
- [ ] **[TUNE]** Frontend/design output: specify a concrete palette/typeface, or have the model propose 4 visual directions before building (the default cream/serif house style is persistent)
- [ ] **[TUNE]** Interactive coding products: use `effort: "xhigh"` or `"high"`, add autonomous features (e.g. an auto mode) to reduce human interactions, and specify task/intent/constraints upfront in the first turn
- [ ] **[TUNE]** Code-review harnesses: remove or loosen "only report high-severity" / "be conservative" filters and have the model report every finding with confidence + severity; move filtering to a downstream step (4.7 follows severity filters more literally, which can depress measured recall)
- [ ] **[TUNE]** Vision-heavy pipelines (screenshots, charts, document understanding): leave images at native resolution up to 2576px long edge for the accuracy gain; remove any scale-factor math from coordinate handling (coords are now 1:1 with pixels). No beta header / opt-in needed - high-res is automatic on Opus 4.7.
- [ ] **[TUNE]** Computer-use pipelines: send screenshots at 1080p for a good performance/cost balance (720p or 1366×768 for cost-sensitive workloads); experiment with `effort` to tune behavior
- [ ] **[TUNE]** Cost-sensitive image pipelines: full-res images on 4.7 use up to ~4784 tokens vs ~1,600 on prior models (~3×). Downsampling client-side before upload avoids the increase, but **do not downsample by default** - if you're unsure whether fidelity is needed, ask the user. Re-baseline with `count_tokens()` on representative images before reacting to cost shifts.
---
## Migrating to Opus 4.8
> **Model ID `claude-opus-4-8` is authoritative as written here.** When the user asks to migrate to Opus 4.8, write `model="claude-opus-4-8"` exactly. Do **not** WebFetch to verify - this guide is the source of truth for migration target IDs. The corresponding entry exists in `shared/models.md`.
Claude Opus 4.8 is our most capable Opus-tier model - highly autonomous, with state-of-the-art long-horizon agentic execution, knowledge work, and memory. It is layered on top of the Opus 4.7 migration above. If the caller is jumping from Opus 4.6 or older, apply the 4.6 and 4.7 sections first, then this one.
**No new breaking changes.** Opus 4.8 keeps the same request surface as Opus 4.7. The same calls that already work on 4.7 work unchanged on 4.8 - adaptive thinking only (`thinking: {type: "enabled", budget_tokens: N}` still 400s; use `{type: "adaptive"}`), sampling parameters (`temperature`, `top_p`, `top_k`) still rejected, last-assistant-turn prefills still 400, `thinking.display` still defaults to `"omitted"`, and the `low`/`medium`/`high`/`xhigh`/`max` effort levels, Task Budgets (beta), and high-resolution vision all behave as on 4.7. A 4.7 -> 4.8 migration is therefore **the model-ID swap plus prompt re-tuning** - there is no required code edit beyond the model string.
**TL;DR for someone already on Opus 4.7:** swap the model ID to `claude-opus-4-8`. Nothing else is required to avoid an error. Then re-tune prompts for the behavioral shifts: 4.8 narrates *more* than 4.7 (add a silence-default if you want 4.7-like terseness), writes in a warmer, less hedged voice, is more deliberate and asks more often (add autonomy guidance to claw back ask-rate), and is more conservative about reaching for search, subagents, file-based memory, and custom tools (add explicit "when to use this" triggering). For long-horizon agentic work, give the full task specification up front in one well-specified turn and run at high effort.
### No new API breaking changes (inherited from 4.7)
These all carry over from Opus 4.7 unchanged - apply them only if the caller is coming from Opus 4.6 or earlier (see the **Migrating to Opus 4.7** section above for the before/after and the SDK-specific syntax):
- `thinking: {type: "enabled", budget_tokens: N}` -> 400. Use `thinking: {type: "adaptive"}` + `output_config.effort`.
- `temperature`, `top_p`, `top_k` -> 400. Remove them; steer with prompting.
- Last-assistant-turn prefills -> 400. Use `output_config.format` (structured outputs) or a system-prompt instruction.
- `thinking.display` defaults to `"omitted"`; set `"summarized"` if you surface reasoning to users.
If the caller is already on Opus 4.7 and these are clean, there is nothing to change here.
### New API feature: mid-session system prompts
You can deliver trusted instructions partway through a session by placing `{"role": "system", ...}` entries directly in the `messages` array - without editing the top-level system prompt and invalidating your prompt cache. Use it for things the application learns mid-session: the user delivered async context, a mode toggled (auto-approve enabled), files changed on disk, the remaining token budget dropped.
```python
messages=[
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "...", "content": "..."}]},
{"role": "system", "content": "This project's codebase is Go. Write code in Go."},
]
```
Phrase these as **context, not commands**. State the fact and let Claude act on it; avoid override-style language ("ignore what the user said", "regardless of the user's request", "disregard the previous instruction"). Claude is trained to protect users from instructions that appear to work against them, and that protection applies to the system role too. No beta header is required; available on Claude Opus 4.8. For cache-placement details and the older-model `<system-reminder>` fallback, see `shared/prompt-caching.md` and `shared/agent-design.md`.
### Capability improvements
**Long-horizon agentic execution.** Opus 4.8 is state-of-the-art at long, autonomous agentic work - complex refactors and overnight coding runs that complete without human correction. To get the most out of it, **give the full task specification up front in a single well-specified initial turn and run at high effort** (`effort: "high"` or `"xhigh"`). Its long-horizon coherence comes partly from reasoning more at each step; combined with a clear up-front goal, that more-intelligent planning often produces more efficient *and* more accurate output than prior frontier models. The "clear goal up front" principle maps to two product surfaces: in Claude Code, `/goal` sets direction for the run; with **Managed Agents (CMA)**, state what "done" looks like via an **Outcome** (`user.define_outcome` with a gradeable rubric - the harness runs an iterate -> grade -> revise loop), see `shared/managed-agents-outcomes.md`.
**Effort is a dimension to test, not a fixed setting.** On prior models many reached for `xhigh` reflexively to maximize intelligence. Opus 4.8 has a higher intelligence ceiling, so **start at `high` as the default and iterate** rather than defaulting to `xhigh`. Sweep `medium`, `high`, and `xhigh` on your own eval set and weigh the intelligence <-> latency <-> cost tradeoff per route - the relationship isn't monotonic: higher effort up front often *reduces* turn count and total cost on agentic work, while for some tasks `medium` delivers equally good results in less time. Reserve `max` for extremely hard, latency-insensitive cases. The per-level effort table in the **Migrating to Opus 4.7** section above applies unchanged on 4.8.
**Writing voice and clarity.** Testers consistently describe 4.8's prose as clearer, warmer, and less hedged than prior models, with fewer measurable AI vocal tics - especially at higher effort, where it approaches expert-level prose and structure. This is roughly the **opposite** direction from the 4.7 shift (4.7 was more clipped, direct, and less validation-forward). If you added style prompts to counter 4.7's terseness or to inject warmth, re-evaluate them against the new baseline before keeping them - they may now overcorrect. 4.8 is also a stronger thought partner: more thoughtful, more willing to push back, and more likely to infer the right answer from context.
**Code review and debugging.** Stronger real-bug finding and clearer explanations than 4.7 - one-shot fixes where 4.7 needed more, and correctly identifying intermittent flakes rather than declaring "fixed" after one clean run. The 4.7 caveat still applies: if a review harness says "only report high-severity issues" or "be conservative", 4.8 follows it literally and measured recall can drop even though underlying bug-finding improved. Tell the model to report everything and filter downstream (or review a second time) - see the **Code review** guidance in the 4.7 section for the recommended prompt.
### Behavioral shifts (prompt-tunable)
None of these break code, but prompts tuned for Opus 4.7 may land differently. 4.8 follows instructions well, so small, explicit nudges close the gap.
**Tool triggering is surface-dependent (search & knowledge).** 4.8's tool-triggering is more surface-dependent than in prior models: with a system prompt present it is high-precision / low-recall - web search triggers slightly more often but runs fewer rounds per trigger, while knowledge-retrieval tools (Drive, project knowledge, connected files) trigger *less* often. It searches when it's confident search is needed and otherwise answers from context, which can lower research depth on tasks that need it. Recover should-search rate with an explicit search-first instruction:
> ```
> <search_first>
> For questions where current information would change the answer (recent events, current roles or prices, version-specific behavior, or anything the user flags as time-sensitive) search before answering rather than answering from memory. For open-ended research requests, begin searching immediately; do not ask a scoping question first unless the request is genuinely ambiguous about what to research.
> </search_first>
> ```
**Under-utilization of subagents, memory, and custom tools.** Separately from search, 4.8 is conservative about reaching for capabilities that need an explicit "decide to use this" step - file-based memory, subagent delegation, custom tools. It won't reach for complex or expensive capabilities unless reasonably sure they're needed. This is steerable since 4.8 follows instructions well - say *when* each capability applies, not just that it exists:
> *"Before any task longer than a few turns, check your memory file for relevant prior context and write new findings to it as you go. When a task fans out across independent items (many files to read, many tests to run, many candidates to check), delegate to subagents rather than iterating serially."*
The same lever works at the **tool-description** level, not just the system prompt: prescriptive descriptions that state *when* to call a tool (e.g. "Call this when the user asks about current prices or recent events") give meaningful lift on 4.8 over descriptions that only state what the tool does. Make the trigger condition part of each capability's own `description`.
**More user-facing narration.** 4.8 narrates more than 4.7 - more text between tool calls in long tool-calling sessions, and longer, more detailed end-of-task wrap-ups by default. If you previously added scaffolding to force interim status ("after every 3 tool calls, summarize progress"), **remove it** - 4.8 does this on its own. If the narration is too verbose for a coding agent, an explicit silence-default makes it behave like 4.7 with no loss of quality:
> *"Default to silence between tool calls. Only write text when you find something, change direction, or hit a blocker - one sentence each. Do not narrate routine actions ('Now I'll...', 'Let me check...', 'Looking at...'). When done: one or two sentences on the outcome. Do not recap every file or test - the user has been following along."*
For knowledge-work deliverables (reports, analysis readouts), verbosity responds very well to instructions in user preferences or the user turn - expose a verbosity preference rather than hard-coding a length.
**More deliberate - asks more often.** 4.8 is more deliberate than prior Opus models. On minor decisions it would previously just make (a variable name, a default value, which of two equivalent approaches), it tends to pause and ask, and it often closes a completed task with "Want me to also...?" rather than doing the obvious next step or stopping cleanly. This is preferred for high-stakes or unfamiliar codebases, but bugs users when uncalibrated. Grant autonomy on the small stuff while keeping caution where it matters (in Claude Code testing this cut ask-rate by ~12 percentage points with no increase in over-reach):
> *"For minor choices (naming, formatting, default values, which approach among equivalents), pick a reasonable option and note it rather than asking. For scope changes or destructive actions, still ask first."*
**Verbose reasoning when thinking is disabled.** With `thinking: {type: "disabled"}`, 4.8 occasionally writes longer explanations of its reasoning into the visible response, which reads as verbose when the user wants a fast, quick answer. The simplest fix is to leave adaptive thinking on - set `thinking: {type: "adaptive"}` (the recommended setting; it adjusts how much to think per task). Note adaptive is **not** on when the field is omitted - like Opus 4.7, a request with no `thinking` field runs without thinking, so set it explicitly. If you need thinking off for latency or cost, scope it in the system prompt:
> *"Respond only with your final answer. Do not include exploratory reasoning, intermediate drafts, diffs you considered but rejected, or meta-commentary about your process."*
### Opus 4.8 Migration Checklist
Every item is tagged: **`[BLOCKS]`** items cause a 400 error if missed; **`[TUNE]`** items are quality/cost adjustments - surface them to the user as recommendations.
For a caller **already on Opus 4.7**, only the first item is required; everything else is `[TUNE]`. The conditional `[BLOCKS]` item applies only when coming from Opus 4.6 or earlier.
- [ ] **[BLOCKS]** Update the `model=` string to `claude-opus-4-8`
- [ ] **[BLOCKS]** *(only if coming from Opus 4.6 or earlier)* Apply the **Migrating to Opus 4.7** breaking changes first - `budget_tokens` -> adaptive thinking, strip `temperature`/`top_p`/`top_k`, remove last-assistant-turn prefills. These already 400 on 4.7 and continue to 400 on 4.8.
- [ ] **[TUNE]** Long-horizon / agentic work: put the full task spec in one well-specified first turn and run at `high` or `xhigh` effort (Claude Code: `/goal`; Managed Agents: an Outcome with a gradeable rubric)
- [ ] **[TUNE]** Effort: sweep `medium` / `high` / `xhigh` on your eval set and pick per route by the intelligence <-> latency <-> cost tradeoff (default `high`, `xhigh` for coding/agentic)
- [ ] **[TUNE]** Research depth & tool use: add a search-first instruction; add explicit triggering guidance for subagents, file-based memory, and custom tools (4.8 under-reaches for these by default) - in the system prompt *and* in each tool's own `description` (prescriptive "call this when..." descriptions give measurable lift)
- [ ] **[TUNE]** Narration: remove forced-progress scaffolding (*"after every N tool calls..."*); add a silence-default if a coding agent is too chatty
- [ ] **[TUNE]** Autonomy: add small-decisions-don't-ask guidance to cut ask-rate, while keeping caution on scope changes / destructive actions
- [ ] **[TUNE]** Writing voice: re-evaluate style prompts added to counter 4.7's directness - 4.8 is warmer and less hedged by default; re-baseline before keeping them
- [ ] **[TUNE]** Code-review harnesses: keep the report-everything-filter-downstream pattern (4.8 follows "only high-severity" / "be conservative" filters literally, which can depress measured recall)
- [ ] **[TUNE]** Thinking-disabled paths: add a final-answer-only instruction if reasoning leaks into the visible response
- [ ] **[TUNE]** Consider mid-session system messages (`role:"system"` in `messages`; no beta header) for context the app learns mid-session, instead of rebuilding the top-level system prompt and invalidating the cache
---
## Migrating to Claude Opus 5
> **Model ID `claude-opus-5` is authoritative as written here.** When the user asks to migrate to Claude Opus 5, write `model="claude-opus-5"` exactly. Do **not** WebFetch to verify - this guide is the source of truth for migration target IDs. The corresponding entry exists in `shared/models.md`.
Claude Opus 5 is the successor to Claude Opus 4.8 in the Opus line, and is strongest on long-horizon agentic work and coding. It is layered on top of the Opus 4.8 migration above; if the caller is coming from Opus 4.7 or older, apply those sections first. Like Claude Fable 5.1, it ships with **elevated cybersecurity safeguards, and its safety classifiers can decline a request**: you get a normal HTTP 200 with `stop_reason: "refusal"` and a `stop_details` category, not an error. Benign security and life-sciences work occasionally trips them, so **check `stop_reason` before reading `response.content`** - code that indexes `content[0]` unconditionally breaks on a refusal. Cyber-category refusals route to Opus 4.8 as the recommended fallback, so a fallback strategy genuinely recovers the request rather than just relabelling the failure. The full refusal semantics (pre-output vs mid-stream billing, retry strategies, fallback credit) are in the Claude Fable 5.1 section below and apply here unchanged.
Existing prompts and evals should carry over with strong out-of-the-box performance. **It is a drop-in upgrade at Opus 4.8's pricing** - $5 per million input tokens, $25 per million output - with the same feature set: 1M context (default, no beta header), 128K max output, adaptive thinking, prompt caching, batch processing, the Files API, PDF support, vision, and the full server-side and client-side tool set. `claude-opus-5` is a fixed ID with no date suffix, same scheme as `claude-opus-4-8`.
The migration is **the model-ID swap plus prompt re-tuning**, with two breaking changes covered below.
**Availability at launch:** Claude API (`claude-opus-5`), Amazon Bedrock (`anthropic.claude-opus-5`), Google Cloud (`claude-opus-5`), and Microsoft Foundry. Opus 4.8 stays available on all four.
**Rate limits are a separate bucket.** Opus 4.8/4.7/4.6/4.5 share one combined Opus limit; Claude Opus 5 does **not** draw from it. Shifting traffic over neither frees headroom on the old bucket nor inherits it - check your tier's Claude Opus 5 limits before moving volume.
**TL;DR for someone already on Claude Opus 4.8:** swap the model ID. Then re-tune: Claude Opus 5 writes longer user-facing responses and longer files on disk (add explicit conciseness and deliverable-length instructions - `effort` does not reliably shorten visible output), verifies its own work without being told (**delete** your verification instructions and harness verification steps), and can expand task scope (add a scope-discipline instruction). Run a fresh effort sweep - `low` and `medium` are unusually strong here and are the primary cost/latency lever.
### Breaking change 1: thinking is on by default
A request that omits the `thinking` parameter **thinks** on Claude Opus 5, unlike Claude Opus 4.8 and Opus 4.7 where omitting it meant no thinking. `thinking: {type: "adaptive"}` remains valid and is equivalent to the default - the wire value didn't change, the default did.
This is a silent cost and truncation change, not just a behavior one: **`max_tokens` is a hard cap on thinking *plus* response text.** A workload that ran without thinking on Opus 4.8 and sized `max_tokens` tightly around its answer can now truncate mid-response. Revisit `max_tokens` on every route that never set `thinking`. To keep the old behavior, pass `thinking: {type: "disabled"}` - subject to the effort cap below.
Raw thinking tokens are **never returned** on Claude Opus 5; `display` defaults to `"omitted"`, and `display: "summarized"` gets you a summary. This also means a fallback model cannot read Claude Opus 5's thinking.
### Breaking change 2: disabling thinking is capped at `high` effort
Disabling thinking is available only at effort **`high` or lower**; `thinking: {type: "disabled"}` combined with `xhigh` or `max` returns a 400. Opus 4.8 accepts that combination, so audit any route that disables thinking before migrating.
**The check is per request.** Effort and thinking are validated independently on every call, so a later request that raises effort to `xhigh` while thinking is still disabled is rejected even though earlier requests in the same conversation succeeded.
```python
# 400 on Claude Opus 5 - disabled thinking above `high`
client.messages.create(
model="claude-opus-5",
max_tokens=4096,
thinking={"type": "disabled"},
output_config={"effort": "xhigh"},
messages=[...],
)
```
**Migrating:** either enable thinking at `xhigh`/`max`, or lower effort to `high` or below. Given how well Claude Opus 5 performs at `low` and `medium`, a latency-sensitive route that previously ran `xhigh` + disabled thinking is usually better served by `medium` with thinking on than by keeping the disabled path.
Everything else from the Opus 4.7/4.8 request surface is unchanged: `budget_tokens` still 400s (use `output_config.effort`), sampling parameters (`temperature`, `top_p`, `top_k`) are still rejected, last-assistant-turn prefills still 400, and `thinking.display` still defaults to `"omitted"`.
### Two failure modes when thinking is disabled
**Are you affected?** Only if you explicitly set `thinking: {type: "disabled"}`. Thinking is on by default on Claude Opus 5 (see Breaking change 1 above), so an unmodified request never hits either of these - but code carrying a disabled-thinking setting forward from Opus 4.8, where it was the default behaviour, does.
Both are specific to `thinking: {type: "disabled"}` on Claude Opus 5, and for both the **primary recommendation is the same: turn thinking back on and use a lower `effort` to control cost and verbosity instead.** Disabling thinking is the more expensive lever in every sense - it is what triggers these, and `low`/`medium` effort already gets you most of the token and latency saving (see § Effort below).
**1. Tool calls can arrive as plain text.** The model occasionally writes a tool call into its user-facing text rather than emitting a structured `tool_use` block. **The turn completes normally and the call never runs** - there is no error and no `tool_use` block to catch, so a harness sees a successful turn that silently did nothing. Worse in an agentic loop: the bogus text stays in conversation history and skews later turns. Most common on tool-heavy workloads such as search.
**2. `<thinking>` tags can leak into the visible response.** The model may emit `<thinking>` or other internal XML in its user-facing output.
If you cannot enable thinking, one instruction covers both failure modes - give the model explicit permission to talk before a tool call (the tool-as-text failure appears to come from suppressing the preamble it wants to write), and forbid internal tags generically:
> *"When you use a tool, you may say a brief sentence first. If no tool can express what the user asked for, say so instead of guessing. Do not include internal or system XML tags in your response."*
Two counterintuitive rules for that instruction:
- **Delete any instruction telling the model not to think or not to reason.** That kind of rule *increases* tag leakage rather than suppressing it.
- **Do not name thinking tags in the prompt.** Calling out `<thinking>` by name is measurably less effective than the generic "internal or system XML tags" wording above.
### New API features
Two additions, each behind its own beta header. Both are optional - a migrated request works without them.
**1. `fallbacks: "default"` - recommended for every caller.** Claude Opus 5's safety classifiers can decline a request; the `fallbacks` parameter re-runs a declined request on another model server-side instead of returning the refusal to you. Previously you named the substitute yourself (`"fallbacks": [{"model": "claude-opus-4-8"}]`). The new `"default"` mode picks Anthropic's recommended fallback automatically, routed **by refusal category** - cyber-category refusals go to Claude Opus 4.8.
```http
POST /v1/messages
anthropic-beta: server-side-fallback-2026-07-01
{"model": "claude-opus-5", "fallbacks": "default", "max_tokens": 1024,
"messages": [{"role": "user", "content": "Say OK."}]}
```
**Prefer `"default"` over pinning a model.** Different fallback models carry different classifiers, so the right substitute depends on *why* the request was declined - and `"default"` removes the migration you would otherwise owe when a pinned fallback model is deprecated. Note the header is `server-side-fallback-2026-07-01`, distinct from the `-2026-06-01` header that gates the array form; the array form's semantics (content blocks, `usage.iterations`, sticky routing) are unchanged and documented in the Claude Fable 5.1 refusal section below.
**2. Mid-conversation tool changes (beta `mid-conversation-tool-changes-2026-07-01`).** Change a conversation's tool set between turns without invalidating the prompt cache. Previously `tools` was fixed for the conversation's lifetime and any edit re-billed the whole prefix. Append a `{"role": "system", "content": [...]}` message carrying a `tool_addition` or `tool_removal` block:
```python
messages = [
{"role": "user", "content": "What tools do you have for weather in Paris?"},
{"role": "system", "content": [
{"type": "tool_addition", "tool": {"type": "tool_reference", "name": "get_forecast"}},
]},
]
```
The added tool must already be declared in `tools[]` with `"defer_loading": True` - declared up front, but not loaded into context until a `tool_addition` surfaces it. A `tool_removal` block must sit either immediately before an assistant message or at the end of `messages`. To *change* a tool's definition, remove the old one on one request, then send the updated entry in `tools[]` on the next. See `shared/tool-use-concepts.md` § Mid-conversation tool changes.
> Warning: Earlier previews of this feature used a different beta header and different block shapes. Both are deprecated - if the code you're migrating carries anything other than `mid-conversation-tool-changes-2026-07-01` with `tool_addition` / `tool_removal` / `tool_reference`, update the header and the shapes together.
> **SDK typings lag these blocks.** Pass them as plain dicts in Python (the SDK forwards unknown keys unchanged) or add a `@ts-expect-error` in TypeScript until the types catch up. `extra_body` / `extra_headers` work on `.stream()` exactly as on `.create()`.
### Capability improvements
**Agentic coding.** Claude Opus 5 is a workhorse for agentic coding and is strongest on *difficult* tasks - multi-file features, larger refactors, end-to-end feature work. It completes tasks rather than leaving stubs or placeholders. The gap over prior models is smaller on easy single-turn edits, so evaluate it on the hard end of your workload. To get the most out of it, give the complete task specification up front and let it run; longer autonomous sessions with more parallel agents show the strongest results, short interactive edits the least.
**Code review and bug-finding.** High precision *and* high recall - a high rate of real bugs per pass, with the extra findings mostly real rather than false positives. It stays accurate at lower effort, which makes a cheap fast pass at review time plus a thorough pass later a practical pattern.
**Effort: the full ladder, and where to start.** Claude Opus 5 supports all five levels - `low`, `medium`, `high`, `xhigh`, `max` - with no beta header. The API default is `high`.
- **Start at `high` (the API default), then sweep down.** `low` and `medium` are unusually effective on this model - strong quality at a fraction of the tokens and latency on many workloads - so treat them as the primary cost/latency lever and reserve `high` and above for tasks where your evals show a quality difference. Effort defaults carried over from a prior model are usually not the right setting here; run a fresh sweep.
- **`xhigh` and `max` are for measured wins, not a starting point.** `max` is the top tier for the deepest reasoning and worth testing where capability matters more than spend, but it can show diminishing returns and overthink simpler tasks.
At `xhigh` or `max`, **set a large `max_tokens`** so the model has room to think and act across tool calls and subagents. Start at 64K and tune.
**Lower prompt-cache minimum.** The minimum cacheable prompt is **512 tokens** on Claude Opus 5, down from 1024 on Opus 4.8. Prompts previously too short to cache now create entries with no code change - worth re-checking any prompt you'd written off as uncacheable. See `shared/prompt-caching.md`.
**Fast mode.** `speed: "fast"` (beta header `fast-mode-2026-02-01`) is supported on Claude Opus 5, priced at $10 / $50 per MTok. It is a research preview on the **Claude API only** - including Managed Agents - and is **not** available on Amazon Bedrock, Google Cloud, or Microsoft Foundry. Fast mode draws on dedicated rate limits separate from the standard Opus pools.
**Vision - give it tools, not more thinking.** Stronger on chart, document, and diagram understanding, and on UI and frontend visual replication. The highest-leverage change is **giving it tools to iteratively analyze, crop, and visually verify its own work**: on this model tool use is a markedly more cost-effective lever than raising thinking alone. Claude Opus 5 sits in the high-resolution tier alongside Opus 4.8 - 2576 px on the long edge, up to 4784 visual tokens per image - so coordinates map 1:1 to pixels and no scale-factor math is needed. Any prompt-side workaround you added for a prior model's vision limitations should be re-validated; several are now counterproductive.
**Long context.** 1M-token context window as both the default *and* the maximum. Instruction following, tool calling, and reasoning stay strong across the full window.
**Office and document tasks.** Generates and edits complex multi-sheet Excel files with non-trivial formulas, and visually strong PowerPoint decks that follow slide-design best practices. It can be prompted to adhere to a specific style or template when one is required.
**Multi-agent coordination.** Coordinates teams of subagents well - few cases of agents overwriting each other's work, and effective use of writer-verifier patterns. Workloads that benefit from multi-agent patterns are good fits. **Cost-sensitive workloads should cap multi-agent usage** - see the delegation section below, because this model reaches for subagents more readily than its predecessors.
### Behavioral shifts (prompt-tunable)
**Longer user-facing responses.** Default response text is longer than on prior models. **`effort` is not the lever here** - changing it may move thinking volume without reliably changing visible output length. Prompting is: in testing, a short conciseness instruction cut user-facing response length by ~20%.
> *"Keep responses focused, brief, and concise to avoid overwhelming the person. Disclaimers and caveats are brief, with most of the response on the main answer; when asked to explain something, give a high-level summary unless an in-depth one is specifically requested."*
For a long system prompt, pair that with a one-line reminder near the end:
> ```
> <tone_preference>
> Keep outputs reasonably concise.
> </tone_preference>
> ```
**More narration in agentic sessions** (the lever runs both ways - the same explicit-description technique tunes narration *up* or restyles it, if your product wants more). Claude Opus 5 narrates what it is about to do, and its per-message output in agentic sessions is longer than prior models'. It responds well to explicit guidance on *how* to communicate during a task rather than just *how much*. For coding agents, this block calibrates it:
> ```
> # Communicating with the user
> Your text output is what the user reads between tool calls; they usually can't see your thinking or the raw tool results. Write it for a teammate who stepped away and is catching up, not for a log file: they don't know the codenames or shorthand you created along the way, and they didn't watch your process unfold. Before your first tool call, say in a sentence what you're about to do; while working, give brief updates when you find something load-bearing or change direction.
>
> Lead with the outcome. Your first sentence after finishing should answer "what happened" or "what did you find" - the thing the user would ask for if they said "just give me the TLDR." Supporting detail and reasoning should come after, for readers who want them.
>
> Being readable and being concise are different things, and readable matters more. If the user has to reread your summary or ask you to explain, any time saved by brevity is gone. The way to keep output short is to be selective about what you include (drop details that don't change what the reader would do next), not to compress the writing into fragments, abbreviations, arrow chains like `A -> B -> fails`, or jargon. What you do include, write in complete sentences with the technical terms spelled out. Don't make the reader cross-reference labels or numbering you invented earlier; say what you mean in place.
>
> Match the response to the question: a simple question should be answered with a direct answer in prose, not headers and sections. Use tables only for short enumerable facts, with explanations in the surrounding prose rather than the cells. Calibrate to the user - a bit tighter for an expert, more explanatory for someone newer.
>
> Write code that reads like the surrounding code: match its comment density, naming, and idiom.
>
> Only write a code comment to state a constraint the code itself can't show - never to say where it came from, what the next line does, or why your change is correct; that's you talking to the reviewer, not the next reader, and it's noise the moment the PR merges.
> ```
**Longer written deliverables.** Separate from conversational verbosity: files Claude Opus 5 writes to disk - reports, Markdown documents, summaries - are often longer than on prior models. If your product ships Claude-authored documents, calibrate length explicitly:
> *"Match the length of written deliverables (especially Markdown files) to what the task needs: cover the substance, but do not pad documents with filler sections, redundant summaries, or boilerplate."*
**Self-check instructions are the same trap.** Beyond harness scaffolding, per-prompt re-check phrasing - *"double-check your answer"*, *"re-verify before responding"* - triggers the same extra work. Note this **inverts a standard prompting best practice**: "ask Claude to self-check" is generally sound advice and is wrong here, so a prompt library that applies it uniformly needs a carve-out for this model rather than a global rule.
**Over-verification - delete your verification scaffolding.** Claude Opus 5 verifies its own work without being asked. Instructions that *tell* it to verify ("include a final verification step for virtually any non-trivial task", "use a subagent to verify") now cause over-verification. **Removing them reduces over-verification with no capability regression** - this is a delete, not a rewrite. The same applies to harness-level scaffolding: separate verification steps carried over from prior models are likely redundant now.
**Task scope expansion.** It can add steps the user didn't request, or apply its own judgment about what the task should be without making that clear. In testing, this instruction reduced scope changes to nearly zero without producing excessive clarifying questions:
> *"Deliver what the user asked for, at the scope they intended. Interpret ambiguity the way a careful colleague would: make routine judgment calls yourself, and check in only when different readings would lead to materially different work. If you conclude the ask is mistaken or a better approach exists, say so in a sentence and keep going with the task as asked - don't quietly narrow, widen, or transform it. Finish the whole task, not just the easy part of it - only report completion when it's fully done. If you genuinely can't complete something, do the rest and state plainly what's missing and why. Stop short of actions or changes that are clearly beyond what the user's ask implies."*
The revised wording adds a **finish-the-whole-task** clause - report completion only when the work is actually done, and if something genuinely can't be finished, do the rest and say plainly what is missing. That covers premature "done" claims, which scope-discipline wording alone did not.
**Delegates to subagents more readily - the opposite of Opus 4.8.** This is a direction change worth flagging: Opus 4.8 *under*-reached for subagents and needed prompting to delegate. Claude Opus 5 reaches for them freely, which multiplies cost and latency - each subagent re-establishes context, re-explores, reports back, and then the coordinator re-reads the report. If your harness supports subagents, **any "delegate more" guidance you added for Opus 4.8 should come out**, and you likely want an explicit cap. A deterministic ceiling on spawn count is the reliable lever; this block reduces delegation and token spend:
> ```
> ## Delegating to subagents
> Subagents multiply cost and time: each one re-establishes context, re-explores, and reports back, and you then re-read its report. Delegate rarely and only when the payoff clearly exceeds that overhead.
>
> Do use subagents for:
> - Large tasks that are genuinely independent and parallelizable. For example, wide multi-file investigations.
>
> Do NOT use subagents for:
> - Work you could finish yourself in a handful of tool calls. For example: a few file reads, a handful of edits, a simple search task, relatively simple verification.
> - Review, verification, or to double check your work. Verification belongs in your main agent loop.
>
> Use of parallel or multiple subagents:
> - Do not use multiple subagents on a single small task. Parallel subagents are for genuinely independent, sizeable tracks (unrelated modules, a wide multi-file investigation), not for splitting one modest job into pieces.
> - If the task can be completed with one subagent, choose one subagent over multiple subagents. Keep spawn counts low.
> - Never use more than 20 parallel agents unless the user explicitly requests it.
>
> When delegating to subagents:
> - Brief the subagent precisely the first time. Avoid launching, waiting, and re-briefing.
> - If you delegate, commit to the delegation. Never redo the subagent's work and do not re-derive its findings once it reports back.
> - If you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
> ```
Note the interaction with over-verification below: "do not use subagents to verify" and "delete your verification scaffolding" are the same underlying fix seen from two angles.
**Narrates self-corrections more than prior models.** It flags and explains its own earlier mistakes at length, which reads as thrash in a user-facing product. Scope corrections to the ones that actually change the user's outcome:
> ```
> # Corrections
> Avoid unnecessary or excessive self-correction. Only correct an earlier statement in your user-facing text when the error would change the user's code, conclusions, or decisions. State corrections plainly and concisely, and continue the task; combine multiple corrections rather than enumerating them all. For slips that change nothing for the user, simply make the correction and move on - no need to note it explicitly. Don't add apologies or preambles, don't be overly self-critical, and don't ruminate or give a detailed account of the mistake or tally past errors. Sometimes, other agents will report incorrect or misleading results - don't always take them at face value immediately. If other agents correct your statements and they are right, then simply update your approach without narrating too much about the correction to the user. This instruction does not apply to thinking blocks.
>
> A follow-up question about your earlier work is not, by itself, a signal that you got something wrong - answer what was asked. A statement that was accurate needs no correction: don't re-audit how you phrased it, how you verified it, or limits you already stated. When the user does point to a real error, correct it plainly as above.
> ```
The second paragraph matters as much as the first: a plain follow-up question can otherwise trigger a re-audit of work that was correct.
**Time to first token (TTFT).** Claude Opus 5 sometimes thinks before its first visible block, which raises TTFT - a problem for user-facing chat and voice, where the pause reads as latency. This one-line instruction reduces pre-first-block thinking significantly:
> *"Latency-sensitive; begin your visible answer immediately."*
Apply it only where first-token latency is user-visible; on background and agentic routes the pre-answer thinking is usually worth keeping.
**Severity filters still depress measured recall.** Unchanged from 4.7/4.8: if a review harness says "only report high-severity issues" or "be conservative", Claude Opus 5 follows it literally. Ask it to report everything with confidence and severity, and filter in a separate pass - see the **Code review** guidance in the Opus 4.7 section for the recommended prompt.
### Claude Opus 5 Migration Checklist
**`[BLOCKS]`** items cause a 400 error if missed; **`[TUNE]`** items are quality/cost adjustments - surface them to the user as recommendations.
- [ ] **[BLOCKS]** Update the `model=` string to `claude-opus-5`
- [ ] **[BLOCKS]** Any route combining `thinking: {type: "disabled"}` with `effort` of `xhigh` or `max`: enable thinking, or lower effort to `high` or below. Validated per request, so audit every call site, not just the first
- [ ] **[BLOCKS]** Every route that never set `thinking`: it now thinks, and `max_tokens` caps thinking + response text together. Raise `max_tokens` or pass `thinking: {type: "disabled"}` at effort `high` or below - otherwise responses truncate mid-answer
- [ ] **[BLOCKS]** *(only if coming from Opus 4.7 or earlier)* Apply the **Migrating to Opus 4.7** breaking changes first - `budget_tokens` -> adaptive thinking, strip `temperature`/`top_p`/`top_k`, remove last-assistant-turn prefills
- [ ] **[TUNE]** Effort: start at `high` (the API default) and sweep down - `low`/`medium` are unusually strong on this model and are the primary cost/latency lever; reserve `xhigh`/`max` for tasks where you've measured a quality difference. Prior-model defaults rarely transfer. At `xhigh`/`max`, set `max_tokens` to at least 64K
- [ ] **[TUNE]** Re-check prompts you'd written off as uncacheable - the minimum drops to 512 tokens (from 1024 on Opus 4.8)
- [ ] **[TUNE]** Rate limits: Claude Opus 5 is a separate bucket from the combined Opus 4.x pool - confirm your tier's limits before shifting volume
- [ ] **[TUNE]** Fast mode (`speed: "fast"`, `fast-mode-2026-02-01`, $10/$50) is Claude-API-only - drop it on Bedrock, Google Cloud, and Foundry routes
- [ ] **[TUNE]** Verbosity: add a conciseness instruction (and a `<tone_preference>` tag for long system prompts). Do **not** try to shorten output by lowering `effort` - it doesn't reliably work
- [ ] **[TUNE]** Agentic sessions: add a "Communicating with the user" block to calibrate inter-tool-call narration
- [ ] **[TUNE]** Claude-authored files: add a deliverable-length instruction
- [ ] **[TUNE]** **Delete** verification instructions from prompts and verification steps from the harness - including per-prompt *"double-check your answer"* phrasing, which inverts the usual self-check best practice on this model
- [ ] **[TUNE]** Add the scope-discipline instruction if the model expands task scope
- [ ] **[TUNE]** Vision pipelines: re-validate prompt-side workarounds written for a prior model's vision limitations
- [ ] **[TUNE]** Consider mid-conversation tool changes (`mid-conversation-tool-changes-2026-07-01`) - changes the tool set between turns without invalidating the prompt cache. Note per-turn `effort` / `task_budget` were **not** in this launch (per-message `effort` shipped later, beta `mid-conversation-output-config-2026-07-01`, and works on Claude Opus 5 too - see Migrating to Claude Fable 5.1 from Claude Fable 5 § New API features; `task_budget` stays request-level)
- [ ] **[TUNE]** Subagent-capable harnesses: this model delegates *more* readily than Opus 4.8 - remove any "delegate more" guidance you added for 4.8 and add an explicit cap
- [ ] **[TUNE]** User-facing products: add the corrections instruction if self-correction narration reads as thrash
- [ ] **[TUNE]** TTFT-sensitive routes (chat, voice): add *"Latency-sensitive; begin your visible answer immediately"* to reduce pre-first-block thinking; skip on background/agentic routes
- [ ] **[TUNE]** Any route running `thinking: {type: "disabled"}`: prefer turning thinking on at `low`/`medium` effort. Disabled thinking can emit tool calls as plain text (the call silently never runs) and leak `<thinking>` tags into output. If you must stay thinking-off, delete any don't-think/don't-reason rule and add the combined *"When you use a tool, you may say a brief sentence first. If no tool can express what the user asked for, say so instead of guessing. Do not include internal or system XML tags in your response"* - do not name `<thinking>` tags in the prompt
- [ ] **[TUNE]** Vision pipelines: give it crop/analyze/verify tools - cheaper and more effective than raising thinking
- [ ] **[TUNE]** Handle `stop_reason: "refusal"` before reading `content`, and opt into `fallbacks: "default"` (`server-side-fallback-2026-07-01`) rather than pinning a model - cyber-category refusals route to Claude Opus 4.8
- [ ] **[TUNE]** Long-horizon / agentic work: give the complete task spec up front in one turn rather than building it up across interactive turns
---
## Migrating to Claude Sonnet 5
> **Model ID `claude-sonnet-5` is authoritative as written here.** When the user asks to migrate to Claude Sonnet 5, write `model="claude-sonnet-5"` exactly. Do **not** WebFetch to verify - this guide is the source of truth for migration target IDs. The corresponding entry exists in `shared/models.md`.
Claude Sonnet 5 substantially improves on Sonnet 4.6 for coding and agentic work, reaching what was previously Opus-tier quality on many tasks. Its API surface aligns with Opus 4.7/4.8: manual extended thinking is removed (adaptive or disabled only, adaptive is the default), and non-default sampling parameters are rejected. This section is layered on top of the Sonnet 4.6 migration above - if the caller is jumping from Sonnet 4.5 or older, apply the 4.6 changes first, then this one.
**TL;DR for someone already on Sonnet 4.6:** swap the model ID to `claude-sonnet-5`. Replace any remaining `thinking: {type: "enabled", budget_tokens: N}` with `thinking: {type: "adaptive"}` (the transitional escape hatch is gone - it now 400s), and note that omitting `thinking` now runs adaptive (4.6 ran thinking-off). Strip non-default `temperature`/`top_p`/`top_k`. Re-run `count_tokens()` against `claude-sonnet-5` - the new tokenizer produces ~30% more tokens for the same text, so token-budgeted limits and cost baselines shift (per-token pricing is also lower than Sonnet 4.6: $2/$10 vs $3/$15 per MTok). `effort` defaults to `high`, the same as Sonnet 4.6 - raise to `xhigh` for the hardest coding and agentic tasks (Claude Sonnet 5 supports the full `low`/`medium`/`high`/`xhigh`/`max` range), and give `max_tokens` headroom at `xhigh`/`max` (the new tokenizer means a Sonnet-4.6-tuned `max_tokens` may truncate equivalent output). Then re-tune prompts: Claude Sonnet 5 interprets instructions more literally than 4.6 - holdover style/tone directives now apply at face value; it is more agentic by default and reaches for tools and self-verification loops more readily (with thinking disabled it is less tool-eager - add an explicit nudge); it gives better in-progress updates by default (drop forced "summarize every N tool calls" scaffolding); and code-review harnesses with conservative-reporting instructions may see lower recall (tell it to report everything and filter downstream).
### Breaking changes (will 400 on Claude Sonnet 5)
These bring the Sonnet line onto the same request surface as Opus 4.7/4.8. See the **Per-SDK Syntax Reference** above for the language-specific spelling of each.
**1. Extended thinking removed - adaptive only.** `thinking: {type: "enabled", budget_tokens: N}` returns a 400. The transitional escape hatch that still worked on Sonnet 4.6 is gone. Use adaptive thinking with an effort hint:
```python
# Before - deprecated on Sonnet 4.6, now errors on Claude Sonnet 5
thinking={"type": "enabled", "budget_tokens": 10000}
# After
thinking={"type": "adaptive"},
output_config={"effort": "high"}, # or "xhigh" for the hardest coding/agentic tasks
```
To turn thinking off entirely, set `thinking: {type: "disabled"}` - but see *Adaptive vs. disabled* below before doing so.
**2. Sampling parameters rejected.** Setting `temperature`, `top_p`, or `top_k` to a non-default value returns a 400; omitting the parameter, or passing its default, is still accepted. The safest migration is to omit them entirely and steer with prompting. If the caller was relying on `temperature=0` for determinism, note in the migration comment that it never guaranteed identical outputs.
```python
# Before
client.messages.create(model="claude-sonnet-4-6", temperature=0.2, ...)
# After - omit entirely
client.messages.create(model="claude-sonnet-5", ...)
```
**3. Bedrock only: forced `tool_choice` requires `thinking: {type: "disabled"}`.** On Amazon Bedrock, pass `thinking: {type: "disabled"}` alongside `tool_choice: {type: "tool", name: ...}` or `tool_choice: {type: "any"}`. The Claude API and Vertex AI do not require this.
**Not a request-shape error, but handle it: cybersecurity safeguards.** Claude Sonnet 5 is substantially more cyber-capable than Sonnet 4.6, so - like Opus 4.7/4.8 - requests touching prohibited or high-risk topics may be refused. Handle it as a content outcome (see the `refusal` stop-reason guidance in the Claude Fable 5.1 section if the caller needs a fallback path).
**Unchanged from Sonnet 4.6:** assistant-turn prefills still return a 400 (use `output_config.format` or a system-prompt instruction); the 1M-token context window, the 128k max-output ceiling, prompt caching, batch processing, the Files API, PDF support, vision, and the full server- and client-side tool set all carry over.
### Silent default change: adaptive thinking on when `thinking` is omitted
On Sonnet 4.6, a request with no `thinking` field runs **without** thinking. On Claude Sonnet 5, the same request runs with **adaptive thinking**. This is not an error - but callers who never set `thinking` will now see thinking output (and spend thinking tokens) where they didn't before. `max_tokens` is a hard limit on total output (thinking + response text), so a workload that ran thinking-off on Sonnet 4.6 by omission may now truncate. Either set `thinking: {type: "disabled"}` explicitly to keep the old behavior, or revisit `max_tokens` to leave room for thinking.
### Silent default change: `thinking.display` defaults to `"omitted"`
`thinking.display` defaults to `"omitted"` on Claude Sonnet 5 (matching Opus 4.7/4.8 and Claude Fable 5.1); on Sonnet 4.6 it defaulted to `"summarized"`. With the default, `thinking` blocks stream with empty text - to a streaming UI this looks like a long pause before output. Combined with the adaptive-on-by-default change above, a Sonnet 4.6 caller who omits `thinking` entirely now gets adaptive thinking *and* empty-text thinking blocks. If you stream reasoning to users, set `thinking: {type: "adaptive", display: "summarized"}` explicitly. `display` controls visibility only - thinking happens and is billed the same under every setting.
### New tokenizer (~30% more tokens)
Claude Sonnet 5 uses the same new tokenizer as Opus 4.7/4.8. The same input text produces approximately 30% more tokens than on Sonnet 4.6. No request/response shape changes and no code edits are required, but **everything measured or budgeted in tokens shifts**: `usage` fields and `count_tokens()` results for the same text are higher, the 1M context window holds less text, and a `max_tokens` limit tuned for Sonnet 4.6 may truncate equivalent output. Per-token pricing is $2/$10 per MTok (Sonnet 4.6 is $3/$15), so the cost of an equivalent request differs in both directions: more tokens at a lower rate. Re-run `count_tokens()` against `claude-sonnet-5` rather than reusing counts measured against earlier models, and re-baseline cost dashboards before reacting to measured shifts.
### Choosing an effort level on Claude Sonnet 5
`effort` defaults to `high` when not set (same as Sonnet 4.6 and Opus 4.8). Claude Sonnet 5 supports the full `low`/`medium`/`high`/`xhigh`/`max` range - the first Sonnet-tier model with `xhigh`. **Keep the `high` default for most work and raise to `xhigh` for the hardest coding and agentic tasks**:
| Level | When to use on Claude Sonnet 5 |
| -------- | ----- |
| `max` | Tasks needing the absolute highest capability with no token constraint. Can deliver gains in some use cases but may show diminishing returns and is sometimes prone to overthinking - test before committing |
| `xhigh` | The hardest coding and agentic use cases - the recommended setting for those |
| `high` | The default; balances token usage and intelligence for most use cases |
| `medium` | Cost-saving step-down from the default - comparable to Sonnet 4.6 at `high` |
| `low` | Short, scoped tasks and latency-sensitive workloads that aren't intelligence-sensitive (chat, simple lookups) |
As a rough cross-model mapping when migrating: Claude Sonnet 5 at `medium` is comparable in intelligence to Sonnet 4.6 at `high`, and Claude Sonnet 5 at `high` is comparable to Sonnet 4.6 at `max`. When benchmarking, match by observed thinking length rather than effort name.
Claude Sonnet 5 **respects effort levels strictly, especially at the low end**. At `low` and `medium` it scopes its work to what was asked rather than going above and beyond - good for latency and cost, but on moderately complex tasks at `low` there is some risk of under-thinking. If you observe shallow reasoning on complex problems, **raise effort to `high` or `xhigh` rather than prompting around it**. If you must keep effort at `low` for latency, add targeted guidance:
> *"This task involves multi-step reasoning. Think carefully through the problem before responding."*
**Leave `max_tokens` headroom at `xhigh`/`max`.** Set a large output token budget (up to the 128k cap, unchanged from Sonnet 4.6) so the model has room for thinking and tool calls. On long tasks, adaptive thinking can use a large share of the budget; if the budget is tight you may see a response that is almost entirely thinking followed by a truncated answer and `stop_reason: "max_tokens"` - raise `max_tokens` or drop to `medium`. Because Claude Sonnet 5 uses the new tokenizer (~30% more tokens for the same text), `max_tokens` limits tuned for Sonnet 4.6 may truncate equivalent output.
### Adaptive vs. disabled thinking
Leave adaptive thinking on. Claude Sonnet 5 calibrates thinking spend to task complexity; the small added latency is usually worth the quality gain. If the caller was running Sonnet 4.6 with thinking off, **try adaptive + `effort: "low"` first** rather than `thinking: {type: "disabled"}`.
The triggering behavior for adaptive thinking is steerable. If the model emits thinking blocks more often than wanted (which can happen with large or complex system prompts), prompt it directly - and measure the effect on quality:
> *"Thinking adds latency and should only be used when it will meaningfully improve answer quality, typically for problems that require multi-step reasoning. When in doubt, respond directly."*
Conversely, if you're running hard workloads at `medium` and seeing under-thinking, the first lever is to raise effort; if you need finer control, prompt for it directly.
### Capability improvements
**Coding and agentic tasks.** The largest gains over Sonnet 4.6 are in coding and agentic tasks. Claude Sonnet 5 performs well out of the box on existing Sonnet 4.6 prompts.
**High-resolution vision.** Claude Sonnet 5 is the first Sonnet-tier model with high-resolution image support: maximum **2576 pixels on the long edge** (up from 1568px on Sonnet 4.6). High-res images can use up to ~3× more image tokens than on Sonnet 4.6 (4784 vs 1568 tokens per image at the limit) - if the added fidelity isn't needed, downsample before sending to control token costs. No beta header or opt-in required.
**Computer use.** Supports the `computer_20251124` tool version (beta header `computer-use-2025-11-24`). Capability works across resolutions up to the 2576px / 3.75MP maximum; sending screenshots at **1080p** provides a good balance of performance and cost. For particularly cost-sensitive workloads, **720p** or **1366×768** are lower-cost options with strong performance. Test to find the ideal settings for the use case; experimenting with `effort` can also help tune behavior.
### Behavioral shifts (prompt-tunable)
None of these break code, but prompts tuned for Sonnet 4.6 may land differently. Claude Sonnet 5 follows instructions closely, so small explicit directives close the gap.
**Response length and verbosity.** Claude Sonnet 5 calibrates response length to task complexity rather than defaulting to a fixed verbosity - usually shorter on simple lookups, longer on open-ended analysis. If a product depends on a particular verbosity, tune the prompt. To decrease verbosity:
> *"Provide concise, focused responses. Skip non-essential context, and keep examples minimal."*
If you see specific kinds of verbosity (e.g. over-explaining), add targeted instructions to prevent them. Positive examples showing the desired concision tend to be more effective than telling the model what not to do.
**Tool use triggering.** Claude Sonnet 5 is more agentic than Sonnet 4.6 by default and will reach for tools and run self-verification loops more readily. **With thinking disabled**, the model is less likely to reach for tools or consider searching - if the harness relies on tool calls with thinking off, add an explicit nudge in the system prompt. `effort` is also a lever: `high` and `xhigh` show substantially more tool usage in agentic search and coding. For scenarios where you want more tool use, also explicitly instruct when and how to use the tools (e.g. if web-search is under-used, describe in the prompt why and how it should be called).
**User-facing progress updates.** Claude Sonnet 5 provides regular, higher-quality updates to the user throughout long agentic traces by default. If the harness has scaffolding to force interim status messages ("After every 3 tool calls, summarize progress"), **try removing it**. If the length or content of the updates isn't well-calibrated to the use case, describe what they should look like in the prompt and provide an example.
**More literal instruction following.** Claude Sonnet 5 interprets prompts literally and explicitly, particularly at lower effort levels. It does not silently generalize an instruction from one item to another, and it does not infer requests that weren't made. The upside is precision - better for carefully tuned prompts, structured extraction, and pipelines that need predictable behavior. If an instruction should apply broadly, **state the scope explicitly** ("Apply this formatting to every section, not just the first one"). The same literalism means style/tone directives carried over from Sonnet 4.6 may now over-apply - re-baseline holdover lines like "be concise" before keeping them.
**Tone and writing style.** Prose style on long-form writing may shift. If a product relies on a specific voice, re-evaluate style prompts against the new baseline. For a warmer or more conversational voice:
> *"Use a warm, collaborative tone. Acknowledge the user's framing before answering."*
Because `temperature`/`top_p`/`top_k` are not accepted on Claude Sonnet 5, callers who previously relied on `temperature` for stylistic variety must use system-prompt instructions instead.
**Code review harnesses.** A review harness tuned for an earlier model may initially see lower recall on Claude Sonnet 5. This is likely a harness effect, not a capability regression: when a review prompt says "only report high-severity issues" / "be conservative" / "don't nitpick," Claude Sonnet 5 follows that instruction more faithfully than earlier models did - it investigates just as thoroughly, identifies the bugs, and then doesn't report findings it judges below the stated bar. Precision typically rises, but measured recall can fall even though underlying bug-finding ability has improved. Recommended prompt language:
> *"Report every issue you find, including ones you are uncertain about or consider low-severity. Do not filter for importance or confidence at this stage - a separate verification step will do that. Your goal here is coverage: it is better to surface a finding that later gets filtered out than to silently drop a real bug. For each finding, include your confidence level and an estimated severity so a downstream filter can rank them."*
This works even without an actual second step, but moving confidence filtering out of the finding stage often helps. If you do want single-pass self-filtering, be concrete about where the bar is rather than using qualitative terms like "important" - e.g. "report any bugs that could cause incorrect behavior, a test failure, or a misleading result; only omit nits like pure style or naming preferences." Iterate against a subset of evals to validate recall/F1 gains.
**Design and frontend defaults.** Claude Sonnet 5 may settle into a consistent default visual style on open-ended frontend and design briefs. Generic instructions ("don't use that color," "make it clean and minimal") tend to shift it to a different fixed palette rather than producing variety. Two approaches work reliably: **specify a concrete alternative** (the model follows explicit specs precisely - give the palette, typography, layout, and spacing), or **have the model propose options before building** (e.g. "Before building, propose 4 distinct visual directions tailored to this brief - bg hex / accent hex / typeface plus a one-line rationale - ask the user to pick one, then implement only that direction"). Because `temperature` isn't accepted on Claude Sonnet 5, the propose-then-pick approach is the recommended way to get meaningfully different design directions across runs. To steer away from generic AI-aesthetic patterns, a short directive in the system prompt also helps:
> *"NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white or dark backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. Use unique fonts, cohesive colors and themes, and animations for effects and micro-interactions."*
**Interactive coding products.** Token usage and behavior can differ between autonomous, asynchronous coding agents (single user turn) and interactive, synchronous coding agents (multiple user turns). To maximize both performance and token efficiency, use `effort: "xhigh"` or `"high"`, add autonomous features like an auto mode, and reduce the number of human interactions required. Specify task, intent, and constraints upfront in the first turn - well-specified initial prompts maximize autonomy and intelligence while minimizing extra token usage after user turns; ambiguous or progressively-revealed prompts tend to reduce token efficiency and sometimes performance.
### Claude Sonnet 5 Migration Checklist
Every item is tagged: **`[BLOCKS]`** items cause a 400 error or truncated output if missed; **`[TUNE]`** items are quality/cost adjustments - surface them to the user as recommendations.
- [ ] **[BLOCKS]** Update the `model=` string to `claude-sonnet-5`
- [ ] **[BLOCKS]** Replace `thinking: {type: "enabled", budget_tokens: N}` with `thinking: {type: "adaptive"}` + `output_config.effort` - the Sonnet 4.6 transitional escape hatch is gone
- [ ] **[BLOCKS]** Strip `temperature`, `top_p`, `top_k` from request construction (use system-prompt instructions for tone/variety instead)
- [ ] **[BLOCKS]** Bedrock only: pass `thinking: {type: "disabled"}` alongside forced `tool_choice` (`{type: "tool"}` / `{type: "any"}`) - not required on the Claude API or Vertex AI
- [ ] **[BLOCKS]** At `effort: "xhigh"` or `"max"`: set a large `max_tokens` (up to 128k, unchanged from Sonnet 4.6) so the model has room for thinking and tool calls - Sonnet-4.6-tuned limits may truncate equivalent output under the new tokenizer (symptom: `stop_reason: "max_tokens"`)
- [ ] **[TUNE]** Thinking-field omitted: adaptive is now the default (4.6 ran thinking-off) - either set `thinking: {type: "disabled"}` to preserve the old behavior, or revisit `max_tokens` for the added thinking spend
- [ ] **[TUNE]** `thinking.display` defaults to `"omitted"` (4.6 defaulted to `"summarized"`): if you stream reasoning to users, set `thinking: {type: "adaptive", display: "summarized"}` explicitly - the default streams empty-text thinking blocks (long pause before output)
- [ ] **[TUNE]** New tokenizer: re-run `count_tokens()` against `claude-sonnet-5` (~30% more tokens for the same text); revisit `max_tokens` and compaction triggers sized close to expected output length; re-baseline cost dashboards before reacting (per-token pricing is lower than Sonnet 4.6: $2/$10 vs $3/$15 per MTok)
- [ ] **[TUNE]** Effort: keep the `high` default; raise to `xhigh` for the hardest coding/agentic tasks; `medium` is a cost-saving step-down (~ Sonnet 4.6 at `high`); reserve `low` for short, latency-sensitive, non-intelligence-sensitive tasks. If shallow reasoning shows up at `low`/`medium`, raise effort rather than prompting around it
- [ ] **[TUNE]** Thinking-off callers: try `thinking: {type: "adaptive"}` + `effort: "low"` instead of `disabled`; if `disabled` must stay, add an explicit tool-triggering nudge (the model is less tool-eager with thinking off)
- [ ] **[TUNE]** Tool usage: more agentic than 4.6 by default (reaches for tools and self-verification more readily) - `effort` is a lever (`high`/`xhigh` for more tool use); add explicit when/how triggering instructions for under-used tools
- [ ] **[TUNE]** Drop forced progress-update scaffolding ("after every N tool calls, summarize") - the default updates are higher quality; describe the desired update shape if it still needs tuning
- [ ] **[TUNE]** Re-baseline holdover style/tone/scope directives - instructions are followed literally; state the scope explicitly when one should apply broadly
- [ ] **[TUNE]** Verbosity-sensitive routes: tune response length via prompt (positive examples > "don't" instructions)
- [ ] **[TUNE]** Code-review harnesses with conservative-reporting instructions ("only high-severity", "don't nitpick"): switch to a coverage-first prompt (report everything with confidence + severity) and filter downstream - measured recall can otherwise fall even though bug-finding improved
- [ ] **[TUNE]** Open-ended frontend/design briefs: specify a concrete spec, or have the model propose 3-4 visual directions and pick one (the recommended substitute for `temperature`-driven variety)
- [ ] **[TUNE]** Interactive coding products: use `effort: "xhigh"`/`"high"`, add autonomous features (e.g. auto mode), and put task/intent/constraints in the first turn
- [ ] **[TUNE]** Vision-heavy / computer-use pipelines: leave images at native resolution up to 2576px long edge for the accuracy gain (downsample to control image-token cost if fidelity isn't needed); for computer use, 1080p screenshots are a good performance/cost balance with `computer_20251124`
- [ ] **[TUNE]** Security workloads: add handling for safeguard refusals (cyber-capable topics may now be declined where Sonnet 4.6 answered)
---
## Migrating to Claude Fable 5.1
> **Model IDs `claude-fable-5-1` and `claude-mythos-5-1` are authoritative as written here.** When the user asks to migrate to Claude Fable 5.1, write `model="claude-fable-5-1"` exactly; a Mythos Preview migrator in Project Glasswing writes `model="claude-mythos-5-1"` (everyone else: `claude-fable-5-1`). Do **not** WebFetch to verify - this guide is the source of truth for migration target IDs. The corresponding entries exist in `shared/models.md`.
Claude Fable 5.1 is Anthropic's most capable widely released model - for the most demanding reasoning and long-horizon agentic work. **Claude Mythos 5.1** (`claude-mythos-5-1`) offers the same capabilities, pricing, and API behavior through Project Glasswing (participation is the only way to access it), and succeeds the invitation-only **Claude Mythos Preview** (`claude-mythos-preview`). Everything in this section applies to both models - only the ID differs. Mythos Preview migrators in Project Glasswing target `claude-mythos-5-1`; everyone else targets `claude-fable-5-1`. 1M token context window by default (the maximum is also the default), up to 128K output tokens per request.
**Migrate to Claude Fable 5.1 only when the user explicitly chose it.** It is not the default Opus upgrade path - pricing is above Opus-tier. For "upgrade to the latest model" requests, the target remains `claude-opus-5`.
### Breaking changes (vs Opus-tier and Mythos Preview)
> Claude Fable 5.1 carries three further breaking changes introduced after Claude Fable 5: forced `tool_choice` (`any` / `tool`) returns a 400, thinking blocks are bound to the producing model, and editing earlier turns invalidates thinking blocks. They are covered in § Migrating to Claude Fable 5.1 from Claude Fable 5 below - apply that section on top of this one when coming from Opus-tier or older.
1. **Thinking is always on - remove all `thinking` configuration.** Adaptive thinking applies automatically whenever the `thinking` parameter is unset (an explicit `{type: "adaptive"}` is also accepted). Any other configuration is rejected: `thinking: {type: "disabled"}` and `{type: "enabled", budget_tokens: N}` both return a 400. `budget_tokens` has no replacement - the `output_config.effort` parameter is a separate output-level control, not a thinking budget.
```python
# Before (Mythos Preview / older models)
client.messages.create(
model="claude-mythos-preview",
max_tokens=16000,
thinking={"type": "enabled", "budget_tokens": 10000},
messages=[...],
)
# After (Claude Fable 5.1) - no thinking field at all
client.messages.create(
model="claude-fable-5-1",
max_tokens=16000,
output_config={"effort": "high"},
messages=[...],
)
```
2. **Assistant prefill is not supported.** Replace last-assistant-turn prefills with structured outputs (`output_config.format`) or system prompt instructions - same replacement patterns as the 4.6-family prefill removal above. (One exception: the fallback-credit prefill claim - the server accepts the echoed assistant message when redeeming a credit; see the refusal section below.)
3. **Interleaved scratchpad is not supported** (Mythos Preview migrators only). Inter-tool reasoning is returned in thinking blocks instead, which adaptive thinking produces automatically between tool calls.
### Thinking output on Claude Fable 5.1 and Claude Mythos 5.1
On Claude Fable 5.1 and Claude Mythos 5.1, the raw chain of thought is never returned. What you receive are **regular `thinking` blocks**, not encrypted blobs or `redacted_thinking`: `display: "summarized"` returns a readable summary of the reasoning, and with `"omitted"` - the default, same as Opus 4.8/4.7 - responses still include `thinking` blocks but the `thinking` field is an empty string. `display` controls visibility only; thinking happens and is billed the same under every setting. When continuing a conversation on the same model, pass thinking blocks back to the API **unchanged** (the standard multi-turn pattern; dropping or editing them breaks the turn).
When continuing on the same model, pass each thinking block back **exactly as received - including blocks whose `thinking` text is empty**. The API rejects blocks whose content has been *modified*, not blocks you have read; displaying the summary is fine, editing or reconstructing blocks is not.
Regular thinking blocks aren't origin-locked - they replay across models fine (the server renders them into the target model's prompt). Fable-tier thinking is the exception: a Claude Fable 5.1 / Claude Mythos 5.1 block is read only by that pair (apart from Claude Mythos 5.1, no other model can read a Claude Fable 5.1 block - see Migrating to Claude Fable 5.1 from Claude Fable 5), and a thinking block from Claude Fable 5/Claude Mythos 5 replayed to a different model is **dropped from the prompt** rather than rendered (except by Claude Fable 5.1 / Claude Mythos 5.1, which read these blocks) - typically silently (early-access builds hard-rejected with `invalid_request_error`; that broke workflows and was reverted before launch, but the new behavior is still rolling out, so don't build logic that depends on either outcome). The drop happens before the prompt is priced, so a dropped block **lowers `usage.input_tokens`** - you aren't billed for it, and there's nothing to strip for cost. Don't strip *regular* thinking blocks either: removing them can trigger ordering/signature 400s. Two rules for replay bodies stand regardless: fallback-credit retries must echo the refused body **unchanged**, and `fallback` blocks from a mid-output fallback stay where they appeared.
Related: a request that tries to elicit the model's internal reasoning *in the response text* can be refused with `stop_details.category: "reasoning_extraction"` - applications needing reasoning visibility should read the summarized `thinking` blocks instead of prompting for reasoning.
### Tokenizer - unchanged from Opus 4.8
Claude Fable 5.1 uses the **same tokenizer as Claude Opus 4.8** (the tokenizer introduced with Opus 4.7). Token counts are roughly unchanged when migrating from Opus 4.7/4.8 or from `claude-mythos-preview`; per-token pricing differs.
- Coming **from Opus 4.7/4.8 or `claude-mythos-preview`**: token counts are roughly unchanged. Re-baseline cost and latency on your own workloads for the per-token price difference.
- Coming **from Opus 4.6, Sonnet, Haiku, or older**: the Opus 4.7 tokenizer tokenizes the same content to roughly 1×-1.35× as many tokens (varies by content and workload shape). Do not reuse token counts, context-window budgets, or `max_tokens` settings measured on the old model; re-baseline with `count_tokens`.
To measure the difference on your own prompts, call `count_tokens` once with your current model and once with `model: "claude-fable-5-1"`, and compare the two `input_tokens` values.
### `refusal` stop reason - handle before reading content
Claude Fable 5.1 runs safety classifiers on incoming requests, targeting research biology and most cybersecurity content (Claude Fable 5.1 is not intended for those domains); benign adjacent work - security tooling, life-sciences tasks - can occasionally trigger false positives, which is why the fallback patterns below matter even for legitimate workloads. (Most Claude consumer surfaces ship with built-in Opus 4.8 fallbacks; API callers configure their own.) A declined request returns a **successful HTTP 200** with `stop_reason: "refusal"`, plus a `stop_details` object with the policy category (values such as `"cyber"`, `"bio"`, `"reasoning_extraction"`, `"frontier_llm"`, or `null` - treat `null` as a permanent valid state; see the refusal category table in the public docs for the full set). **Branch on `stop_reason`, never on `stop_details`** - `stop_details` is informational and can be `null` even on a refusal, and `explanation` is not guaranteed present. Note that classifier blocks and ordinary model refusals (the model itself declining) both surface as `stop_reason: "refusal"`; `stop_details.category` tells you which class you're handling, and therefore whether retrying on a fallback model is the right response. The classifier can fire **before any output** (empty `content` array; not billed at all - no input or output tokens, no rate-limit consumption) or **mid-stream** after partial output (already-streamed output is billed at normal rates - discard the partial output rather than treating it as complete). Code that reads `response.content[0]` unconditionally will break - check `stop_reason` first:
```python
response = client.messages.create(model="claude-fable-5-1", max_tokens=1024, messages=[...])
if response.stop_reason == "refusal":
# classifiers declined; content is empty (pre-output) or partial (mid-stream)
handle_refusal()
else:
print(response.content[0].text)
```
**Default to opting in.** Fallbacks are not automatic on the API - a request without them simply stops on a refusal. Migrated and new Claude Fable 5.1 code should ship with pattern 1 below (pattern 2 on providers without server-side support) from day one, not as a later hardening step: emit the opt-in in the code, tell the user it's there, and remove it only if they decline.
Three ways to retry a refused request on another model, in order of preference:
**1. Server-side `fallbacks` parameter (beta; Claude API and Claude Platform on AWS) - preferred.** One round trip, a plain client, no client-side logic. Name substitute models (the supported fallback targets are `claude-opus-4-8` and `claude-opus-5`, expansion expected); on a policy decline the API runs the next model on the same request and returns its answer, with credit-style repricing applied automatically. A `stop_reason: "refusal"` on the final response means the whole chain refused.
```python
response = client.beta.messages.create(
model="claude-fable-5-1",
max_tokens=1024,
betas=["server-side-fallback-2026-06-01"],
fallbacks=[{"model": "claude-opus-4-8"}],
messages=[{"role": "user", "content": "Hello, Claude"}],
)
# Switch points: one fallback block per model that ran and declined this turn
for block in response.content:
if block.type == "fallback":
print(f"{block.from_.model} declined; {block.to.model} continued")
# Served-by signal: a fallback_message in usage.iterations means a fallback model
# ran; pair it with stop_reason to confirm the fallback served the response
# (a fallback model can also refuse). Covers sticky turns too.
fallback_ran = any(
entry.type == "fallback_message" for entry in response.usage.iterations or []
)
if fallback_ran and response.stop_reason != "refusal":
print(f"Served by {response.model}")
```
Key semantics:
- **Header depends on the form you use.** The **array** form (`fallbacks: [{...}]`) requires exactly `server-side-fallback-2026-06-01` - other `server-side-fallback-*` values reject it with a 400, and that header carries the *earliest* date of the series (`-2026-06-09` and `-2026-06-02` were earlier previews), so do not "correct" it to a newer-looking date. The **`"default"` scalar** form uses `server-side-fallback-2026-07-01` instead - see § New API features under Migrating to Claude Opus 5. Pairing either header with the other form 400s. Rejected on the Batches API; available on the Claude API and Claude Platform on AWS; not on Amazon Bedrock, Vertex AI, or Microsoft Foundry (use pattern 2 there - the SDK middleware). Entries may override `max_tokens` per hop (bounding that attempt's own output independently of the top-level `max_tokens`); `thinking`, `output_config`, and `speed` overrides are rolling out (`speed` additionally requires its beta) - until your requests accept them, include only `model` and `max_tokens` in each entry. Entries must be distinct and must be in the requested model's `allowed_fallback_models` (published on `/v1/models` when the `server-side-fallback-2026-06-01` beta header is set - not yet visible under the `fallback-credit-*` header alone, and not exposed on Amazon Bedrock, Vertex AI, or Microsoft Foundry). The request *with an entry's overrides merged in* must be valid as a direct request to that entry's model.
- **Triggers on policy declines only** - rate limits, overloads, and server errors on the requested model are returned as-is, never falling back.
- **Reading the response:** a `fallback` content block (`{"type": "fallback", "from": {"model": ...}, "to": {"model": ...}}`) marks each switch point in `content`; the served-by signal is a `fallback_message` entry in `usage.iterations` (don't rely on the block - sticky-served turns have none). Top-level `model` names the model that produced the message.
- **Billing:** `usage.iterations` is the per-attempt source of truth; top-level `usage` covers only the attempt that produced the returned message. Declined-before-output attempts are reported but not billed; fallback attempts bill at the fallback model's rates. Each attempt claims the rate limits of the model that ran it - if the fallback model is rate-limited or overloaded, the fallback attempt is not made and the preceding refusal is returned instead with `stop_details.recommended_model` naming a model to retry directly (the recommendation is a hint, not a guarantee, and is `null` when no recommendation is available) - size fallback-model limits for expected refusal volume.
- **Sticky routing:** once a conversation falls back, later requests with `fallbacks` (streaming and non-streaming - on a stream the decision is made before it opens, so `message_start` already names the fallback model) are served directly by the fallback model for ~1 hour (best-effort; org-scoped content-hash record, not message content; not recorded for ZDR orgs). Handle the requested model being tried again at any time.
- **Echoing fallback turns back:** after a mid-output fallback, omit `thinking`, `redacted_thinking`, and `tool_use` blocks - plus any `server_tool_use` block without its matching `server_tool_result`, and any other unrecognized model-internal block type - that appear *before* the final `fallback` block; text blocks, paired server-tool blocks, and everything after the boundary echo normally. The `fallback` block itself is an ignored audit marker (keep or drop). Streaming: the retry happens on the same stream and already-received content is never invalidated - a pre-output block is seamless (`message_start` names the fallback model; the `fallback` block arrives as an ordinary `content_block_start`, first in `content` - there is no special SSE event type; note `message_start` arrives only after the declined attempt, so time-to-first-byte includes it), and a mid-stream block keeps the partial, marks the boundary with the block, and continues - only the partial's `text` blocks are passed to the fallback model as continuation context (other block types stay in `content` but aren't part of it). Non-streaming mid-output declines omit the declined partial entirely.
**2. SDK client-side middleware - for providers without server-side fallbacks (Amazon Bedrock, Vertex AI, Microsoft Foundry).** Register it on the client and every `client.beta.messages` request (streaming included) retries refusals automatically, splicing the fallback model's events onto the open stream in the same wire shape as pattern 1 (a `fallback` content block at each boundary, per-hop `usage.iterations`). It is also a beta surface: the middleware sends the `fallback-credit-2026-07-01` header by default (the earlier `-2026-06-01` value is still accepted) so retries are repriced via credit tokens (override with its `betas` option). `BetaFallbackState` pins follow-up turns to the model that accepted (the client-side analog of sticky routing) - reuse one state object per conversation:
```python
from anthropic import Anthropic, BetaFallbackState, BetaRefusalFallbackMiddleware
client = Anthropic(middleware=[BetaRefusalFallbackMiddleware([{"model": "claude-opus-4-8"}])])
state = BetaFallbackState() # pins follow-ups to the model that accepted
with state:
response = client.beta.messages.create(model="claude-fable-5-1", max_tokens=1024, messages=messages)
```
Create **one state per conversation** - it is the pinning scope; sharing one across conversations pins unrelated threads together, and a conversation without a state is never pinned. Per-language naming (from the GA SDK examples - don't improvise):
- **TypeScript**: `betaRefusalFallbackMiddleware([...])` in the client's `middleware` array; pass `{ fallbackState: state }` (a `BetaFallbackState`) as a request option.
- **Go**: `option.WithMiddleware(betafallback.BetaRefusalFallbackMiddleware([]anthropic.BetaFallbackParam{{Model: ...}}))` (package `lib/betafallback`); state via `betafallback.WithBetaFallbackState(&betafallback.BetaFallbackState{})` passed as a request option. Server-side equivalents: `Fallbacks: []anthropic.BetaFallbackParam{...}` + `anthropic.AnthropicBetaServerSideFallback2026_06_01`.
- **C#**: it's a *handler* - `new AnthropicClient { Handlers = [new BetaRefusalFallbackHandler { Fallbacks = [new(Model.ClaudeOpus4_8)] }] }` (namespace `Anthropic.Helpers`); state via `BetaFallbackState.Create()` scoped per call with `using (fallbackState.Use()) { ... }`. Server-side equivalents: `Fallbacks = [new(Model.ClaudeOpus4_8)]` + `AnthropicBeta.ServerSideFallback2026_06_01`.
For languages not listed (Java, Ruby, PHP) - or for a full runnable program in any language - each public SDK repo ships a fallbacks example under `examples/` (e.g. `examples/fallbacks.py`, `examples/refusal-fallback/`): WebFetch the repo from `shared/live-sources.md` § SDK Repositories rather than improvising the binding.
**3. Hand-rolled retry + fallback credit (raw HTTP, or SDKs without the middleware).** Detect the refusal via `stop_reason` and re-send the conversation as-is on a model with broader availability such as `claude-opus-4-8` (no stripping required either way: Claude Fable 5's thinking blocks are silently ignored by models other than Claude Fable 5.1 / Claude Mythos 5.1, which read them, and Claude Fable 5.1's own blocks are dropped by the API for any other model - breaking change 2 in § Migrating to Claude Fable 5.1 from Claude Fable 5); keep using the fallback model for subsequent turns. **Fallback credit** (beta: Claude API, Claude Platform on AWS, Amazon Bedrock, Vertex AI, and Microsoft Foundry) makes those retries cheaper. Prompt caches are per-model, so a plain retry pays cold cache-writes on the new model. With the `fallback-credit-2026-07-01` beta header (send it on both the original request and the retry; `-2026-06-01` is still accepted, and `server-side-fallback-2026-07-01` grants the same fields), a refusal's `stop_details` carries `fallback_credit_token` (opaque; `null` when unavailable) and `fallback_has_prefill_claim`. Echo the token as the top-level `fallback_credit_token` request parameter on the retry (typed in the GA SDKs; on a pre-GA SDK pass it via `extra_body`) and the previously-cached span bills at cache-read rates - the retry costs what it would have if the conversation had been on that model all along. Rules: the retry body must match the refused request **exactly** in every prompt-shaping field (`system`, `messages`, `tools`, `tool_choice`, `thinking` - do **not** strip thinking blocks when redeeming a credit - the server handles them); the retry model must be in the refused model's `allowed_fallback_models`; the token expires in 5 minutes; Batches results carry no tokens. If `fallback_has_prefill_claim` is `true`, append one assistant message echoing the refused response's `content` - the retry model continues from where the refused model stopped (and completed server-tool work isn't re-run). When echoing, strip trailing whitespace from a final `text` block (the prefill validator rejects it; the credit match tolerates that edit), after omitting any unpaired `tool_use` blocks. On a 400, fall back to the unchanged body with the token; on a 400 naming `fallback_credit_token`, retry without it (credit forfeited).
**Migrating code built on the v1 preview.** If the code you're editing carries any of these markers, it targets the discontinued early-access surface - migrate it to the v2 shapes above, and ship the header and parameter changes together (the v1 parameter shape under the v2 header is a 400):
| v1 marker (replace) | v2 |
|---|---|
| `server-side-fallback-2026-06-09` / `-2026-06-02` header | `server-side-fallback-2026-06-01` (array form; the `"default"` scalar form uses `-2026-07-01`) |
| `fallback: {model, on_partial}` single object | `fallbacks: [{model, ...}]` array (1-3); `on_partial` no longer exists - partial-output behavior is fixed (streams keep the partial; non-streaming omits it). Unknown keys in an entry are a 400 |
| Top-level `response.fallback` object (`from_model`, `reason`) | Never emitted - read `fallback` content blocks (switch points, no `reason` field) and `usage.iterations` (served-by) |
| `event: fallback` SSE with discard indices | No dedicated event; streamed content is never invalidated - the switch arrives as an ordinary `content_block_start`/`stop` pair of type `fallback` |
| `fallback_primary` / `fallback_retry` iteration types | Blocked attempts are plain `message` entries; the serving attempt is `fallback_message` |
| `reason: "sticky"` | No reason field - sticky turns carry no block; detect via `fallback_message` in `usage.iterations` + `response.model` |
| `recommended_model` meaning "primary served the refusal" | Now populated only when the fallback attempt *couldn't run* (rate-limited/overloaded) - its presence means a direct retry on that model may succeed, not that it refused too |
### Data retention requirement
Claude Fable 5.1 requires **30-day data retention** and is not available under zero data retention. Requests from an organization whose data-retention configuration doesn't meet the requirement return `400 invalid_request_error` - if a migration suddenly 400s with no obvious request problem, check the org's retention configuration before debugging the payload. On Amazon Bedrock, Google Vertex AI, and Microsoft Foundry, data-retention requirements are set by each platform.
### What carries over unchanged
Same Messages API and tool-use patterns as Opus-tier and Mythos Preview. Supported at launch: `output_config.effort` (`low`/`medium`/`high`/`xhigh`/`max`), Task Budgets (beta, `task-budgets-2026-03-13` header - on Claude Fable 5.1 confirm at launch), compaction (beta, `compact-2026-01-12` header), the memory tool, tool-call clearing via context editing, and high-resolution vision (no downscaling cap, as on Opus 4.7+).
### Behavioral shifts (prompt-tunable)
None of these are API-breaking, but they're where migrated workloads feel different. Claude Fable 5.1's biggest gains are on work *above* what prior models could do (long-horizon autonomous runs, first-shot implementations of well-specified systems, end-to-end enterprise deliverables - financial analysis, spreadsheets, slides, docs - code review/debugging and repository-history search, vision on dense or degraded images - it's explicitly trained to use bash and crop tools on flipped/blurry/noisy inputs - navigating ambiguity, parallel sub-agent delegation and collaboration - it reliably sustains ongoing communications with long-running sub-agents and peer agents; note bug-finding gains exclude security-focused analysis, where the cyber classifiers apply) - don't evaluate it only on workloads older models already handled.
**Longer turns by default - the biggest structural shift.** Individual requests on hard tasks can run many minutes at higher effort (a 15-minute single request is normal when the task involves gathering context, building, and self-verifying). Before migrating, plan timeouts, streaming, and user-facing progress indicators; structure work so callers check in on runs asynchronously rather than blocking inside one request. On ambiguous tasks Claude Fable 5.1 may need a small nudge to avoid overplanning:
> When you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision the user has already made, or narrate options you will not pursue in user-facing messages. If you are weighing a choice, give a recommendation, not an exhaustive survey. This does not apply to thinking blocks.
**Consider all effort levels.** `output_config.effort` is the primary intelligence/latency/cost control. Recommended defaults: `high` for most tasks, `xhigh` for the most capability-sensitive workloads, `medium`/`low` for routine work. Lower effort settings - including `low` - still perform very well on Claude Fable 5.1, often exceeding the `xhigh` or even `max` performance of previous models. Reduce effort if a task completes correctly but takes longer than necessary, or for a quicker interactive working style. At higher effort on routine work, Claude Fable 5.1 can gather context and deliberate beyond what the task needs (the flip side: higher effort buys excellent verification behavior and the most rigorous outputs). To prevent unrequested tidying or refactoring at higher effort:
> Don't add features, refactor, or introduce abstractions beyond what the task requires. A bug fix doesn't need surrounding cleanup and a one-shot operation usually doesn't need a helper. Don't design for hypothetical future requirements - do the simplest thing that works well. Avoid premature abstraction. Avoid half-finished implementations either. Don't add error handling, fallbacks, or validation for scenarios that cannot happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
**Instruction following is strong - use it.** Claude Fable 5.1 is very responsive to explicit communication-style sections in system prompts; invest in them rather than fighting output style downstream. Un-steered - especially at higher effort - it can elaborate beyond what the task needs: heavily-structured PR descriptions, sections on alternatives that weren't chosen, comments narrating what the next line does. You don't need to enumerate these behaviors by name; a brief instruction is just as effective:
> Lead with the outcome. Your first sentence after finishing should answer "what happened" or "what did you find" - the thing the user would ask for if they said "just give me the TLDR." Supporting detail and reasoning come after. Being readable and being concise are different things, and readability matters more. The way to keep output short is to be selective about what you include (drop details that don't change what the reader would do next), not to compress the writing into fragments, abbreviations, arrow chains like A -> B -> fails, or jargon.
**Ground progress claims on long runs.** Require progress claims to be audited against tool results - in testing this nearly eliminated fabricated status reports on tasks designed to elicit them:
> Before reporting progress, audit each claim against a tool result from this session. Only report work you can point to evidence for; if something is not yet verified, say so explicitly. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
**State boundaries explicitly.** Claude Fable 5.1 sometimes takes unrequested-but-adjacent actions (e.g. composing an email straight to drafts, creating backup git branches). Define what it should *not* do:
> When the user is describing a problem, asking a question, or thinking out loud rather than requesting a change, the deliverable is your assessment. Report your findings and stop. Don't apply a fix until they ask for one. Before running a command that changes system state - restarts, deletes, config edits - check that the evidence actually supports that specific action. A signal that pattern-matches to a known failure may have a different cause.
**Let it delegate - asynchronously.** Parallel sub-agents are dependable on Claude Fable 5.1 - instead of suppressing delegation (a common prior-model guardrail), use sub-agents frequently and give explicit guidance on *when* delegation is desirable. Sub-agents that communicate **asynchronously** with the orchestrator outperform spawn-and-block: long-lived agents keep their context instead of re-establishing it per subtask (cache-read savings), the orchestrator isn't bottlenecked on the slowest sub-agent, and context persists across subtasks.
> Delegate independent subtasks to sub-agents and keep working while they run. Intervene if a sub-agent goes off track or is missing relevant context.
**Give it a memory surface.** Claude Fable 5.1 performs notably better when it can write learnings somewhere for future reference - even a plain `.md` file. Tell it where, tell it to consult that file in future sessions, and give it a format:
> Store one lesson per file with a one-line summary at the top. Record corrections and confirmed approaches alike, including why they mattered. Don't save what the repo or chat history already records; update an existing note rather than creating a duplicate; delete notes that turn out to be wrong.
**Rare: early stopping.** Deep into long sessions it can occasionally end a turn with a text-only statement of intent ("I'll now run X") without the tool call, or ask permission it doesn't need. A "continue" recovers it interactively; for autonomous pipelines add a system reminder:
> You are operating autonomously. The user is not watching in real time and cannot answer questions mid-task, so asking 'Want me to...?' or 'Shall I...?' will block the work. For reversible actions that follow from the original request, proceed without asking. Offering follow-ups after the task is done is fine; asking permission after already discussing with the user before doing the work is not. Before ending your turn, check your last paragraph. If it is a plan, an analysis, a question, a list of next steps, or a promise about work you have not done ('I'll...', 'let me know when...'), do that work now with tool calls. End your turn only when the task is complete or you are blocked on input only the user can provide.
**Rare: context anxiety.** In very long sessions it can worry about running out of context - suggesting a new session or trimming its own work - most often when the harness surfaces a remaining-token countdown. Avoid showing explicit context-budget counts; if you must:
> You have ample context remaining. Do not stop, summarize, or suggest a new session on account of context limits - continue the work.
**Give the reason, not just the request.** Claude Fable 5.1 performs better when it understands the intent behind a request - it connects the task to relevant information rather than inferring intent on its own. This matters most for long-running agents juggling context from disparate workstreams:
> I'm working on [the larger task] for [who it's for]. They need [what the output enables]. With that in mind: [request].
**Readability in long agentic sessions.** Deep into extended conversations (many tool calls, large working context) Claude Fable 5.1 can produce text users find hard to follow - dense arrow-chain shorthand, implementation-level detail, references to thinking the user never saw. A communication-style addendum strongly mitigates this; adapt:
> Terse shorthand is fine between tool calls (that's you thinking out loud, and brevity there is good). Your final summary is different: it's for a reader who didn't see any of that. If you've been working for a while without the user watching - overnight, across many tool calls, since they last spoke - your final message is their first look at any of it. Write it as a re-grounding, not a continuation of your working thread: the outcome first, then the one or two things you need from them, each explained as if new. The vocabulary you built up while working is yours, not theirs; leave it behind unless you re-introduce it. When you write the summary at the end, drop the working shorthand. Write complete sentences. Spell out terms instead of abbreviating them. Don't use arrow chains, hyphen-stacked compounds, or labels you made up earlier - the reader doesn't have the context to decode them. When you mention files, commits, flags, or other identifiers, give each one its own plain-language clause saying what it is or what changed - never pack several into one parenthesized run or slash-separated list. Open with the outcome: one sentence on what happened or what you found. Then the supporting detail. If you have to choose between short and clear, choose clear.
### Long-running agent recommendations
- **Make self-verification explicit.** For long-running builds, instruct it to establish and run its own checking harness on a cadence ("Establish a method for checking your own work as you build; run it every [interval], verifying against the specification with sub-agents"). Separate fresh-context verifier sub-agents tend to outperform self-critique.
- **De-prescribe migrated prompts and skills.** Prompts and skills written for prior models are often too prescriptive for Claude Fable 5.1 and *reduce* output quality. After migrating, A/B the workload with older step-by-step scaffolding removed - prefer stating the goal and constraints over enumerating the steps. Claude Fable 5.1 is also good at updating skills on the fly from what it learns mid-task - let it.
- **Start at the top of your difficulty range.** The teams with the best early-access outcomes gave it their hardest unsolved problems first - have it scope the problem, ask questions, then execute.
- **Add a `send_to_user` tool for verbatim mid-task delivery.** When an asynchronous agent must deliver something the user sees *exactly as written* mid-run (a deliverable, a progress update with specific numbers, a direct answer), give it a client-side tool whose input you render directly in the UI - tool inputs are never summarized, so content arrives intact. Return a simple acknowledgement as the tool result:
```json
{
"name": "send_to_user",
"description": "Display a message directly to the user. Use this for progress updates, partial results, or content the user must see exactly as written before the task finishes.",
"input_schema": {
"type": "object",
"properties": {
"message": { "type": "string", "description": "The content to display to the user." }
},
"required": ["message"]
}
}
```
For agents that only narrate routine progress, the model's default progress narration is typically adequate without this tool.
### Claude Fable 5.1 Migration Checklist
- [ ] **[BLOCKS]** Also apply the Claude Fable 5.1 from Claude Fable 5 Migration Checklist below - it carries the three breaking changes introduced after Claude Fable 5 (forced `tool_choice` 400s, model-bound thinking blocks, the history-editing check), which this checklist predates
- [ ] **[BLOCKS]** Update the `model=` string to `claude-fable-5-1` (`claude-mythos-5-1` for Mythos Preview migrators in Project Glasswing)
- [ ] **[BLOCKS]** Remove `thinking: {type: "disabled"}` (errors on Claude Fable 5.1)
- [ ] **[BLOCKS]** Replace assistant prefill with structured outputs or system prompt instructions
- [ ] **[BLOCKS]** Confirm the org meets the 30-day data-retention requirement (ZDR orgs get `400 invalid_request_error` on every request; ZDR only if expressly authorized by Anthropic, or enable 30-day retention for one workspace)
- [ ] **[BLOCKS]** Remove all other `thinking` configuration (`{type: "enabled", budget_tokens: N}` returns a 400, same as on Opus 4.7/4.8); control depth with `output_config.effort` instead
- [ ] **[BLOCKS]** If thinking content is surfaced to users or stored in logs: add `thinking: {type: "adaptive", display: "summarized"}` (the default is `"omitted"` - otherwise the rendered text is empty)
- [ ] **[TUNE]** Re-baseline cost and latency on your own workloads - token counts are roughly unchanged from Opus 4.7/4.8 and Mythos Preview (same tokenizer); per-token pricing differs. Coming from Opus 4.6, Sonnet, Haiku, or older, token counts differ - use `count_tokens` with each model to compare
- [ ] **[TUNE]** Add `stop_reason == "refusal"` handling before reading `response.content` (pre-output: empty + unbilled; mid-stream: partial output billed - discard); opt into a fallback by default - server-side `fallbacks` (Claude API and Claude Platform on AWS: `fallbacks: "default"` with `server-side-fallback-2026-07-01`, or the array form with `server-side-fallback-2026-06-01`) where available, otherwise the SDK middleware or fallback credit (`fallback-credit-2026-07-01`, exact body); a bare client-side replay (history as-is; models other than Claude Fable 5.1 / Claude Mythos 5.1 drop Fable's thinking blocks) is the floor, not the recommendation
- [ ] **[TUNE]** If you surfaced thinking text to users, plan for the thinking output change - the raw chain of thought is never returned; render the `display: "summarized"` summary (per the [BLOCKS] item above); pass blocks back unchanged on the same model; other models drop them from the prompt (unbilled; Claude Mythos 5.1 instead reads them)
- [ ] **[TUNE]** Plan for minutes-long turns: timeouts, streaming, async check-ins, progress UX (see Behavior changes above)
- [ ] **[TUNE]** Run an effort sweep including low/medium for routine workloads; add the no-tidying instruction if higher effort produces unrequested refactors
- [ ] **[TUNE]** A/B with prior-model scaffolding removed - over-prescriptive prompts/skills reduce Claude Fable 5.1 output quality
---
## Migrating to Claude Fable 5.1 from Claude Fable 5
> **Model IDs `claude-fable-5-1` and `claude-mythos-5-1` are authoritative as written here.** When the user asks to migrate to Claude Fable 5.1, write `model="claude-fable-5-1"` exactly; a Project Glasswing participant migrating from Claude Mythos 5 writes `model="claude-mythos-5-1"`. Do **not** WebFetch to verify - this guide is the source of truth for migration target IDs. The corresponding entries exist in `shared/models.md`.
Claude Fable 5.1 succeeds Claude Fable 5 in the same tier at the same per-token price, with stronger long-running agentic coding, multistep research, and document / spreadsheet / slide work. **Claude Mythos 5.1** (`claude-mythos-5-1`) is the same model for Project Glasswing participants (see § Claude Mythos 5.1 below for the two ways it differs). Same 1M token context window (default and maximum), same 128K max output, same tokenizer as Claude Fable 5 (token counts unchanged; coming from a pre-Opus-4.7 model, expect roughly 30% more tokens - follow the tokenizer guidance in § Migrating to Claude Fable 5.1 above). Available on the Claude API, Amazon Bedrock (`anthropic.claude-fable-5-1`), Claude Platform on AWS, Google Cloud, and Microsoft Foundry (Anthropic-hosted). Existing Claude Fable 5 prompts should perform well out of the box.
**Migrate to Claude Fable 5.1 only when the user explicitly chose it** - same rule as Claude Fable 5: it is not the default Opus upgrade path. For "upgrade to the latest model" requests, the target remains `claude-opus-5`; the docs' own positioning is "start with Claude Opus 5; use Claude Fable 5.1 for demanding reasoning and long-horizon agentic work, or when evals on Claude Opus 5 at higher effort still fall short".
**What changes, in one line:** three breaking changes (forced tool choice 400s; thinking blocks are preserved only for the model that produced them or a newer one; thinking blocks are preserved only in the conversation that produced them - the docs group the last two as "preserved thinking"), five additions (per-message effort, turn-scoped system messages, progress updates between tool calls, a lower cache-read price, content provenance), and agent-loop behavior that differs in three prompt-tunable ways. Read the path that matches the source model: from Claude Fable 5, everything below applies directly; from Claude Opus 5, also read § Coming from Claude Opus 5; from Opus 4.8 or earlier, apply § Migrating to Claude Fable 5.1 above first (Opus 4.7 or earlier: the Claude Opus 5 section before that), then this one.
### Breaking change 1: forced tool use is rejected
`tool_choice: {"type": "any"}` and `tool_choice: {"type": "tool", "name": "..."}` return a 400 `invalid_request_error` on Claude Fable 5.1 and Claude Mythos 5.1 (as they already do on Mythos Preview) - on the Messages API, the Message Batches API, and the token-counting endpoint:
```text
tool_choice: type "tool" and "any" are not supported for this model.
```
This is a model-specific restriction, not a consequence of always-on thinking (Claude Fable 5 and Claude Opus 5 also think by default and still accept forced tool choice). `{"type": "auto"}` (the default) and `{"type": "none"}` are unchanged. `disable_parallel_tool_use: true` still works with `auto` but now means *at most* one call - the "exactly one tool" guarantee it gave in combination with `any`/`tool` is gone.
Migrate by intent:
- **Steering toward a tool:** keep `tool_choice: {"type": "auto"}` (or omit it) and state in the prompt when the tool applies ("Use the `get_weather` tool to answer"). Claude Fable 5.1 follows explicit tool instructions reliably, and thinking first improves the arguments it passes. If the *application* (not the user) requires a specific call on the current turn of a multi-turn conversation, append a `role: "system"` message after the latest `user` turn that names the tool, says the call is required for this turn, and tells Claude to open its response with it - and keep that message in the history on later requests.
- **Guaranteeing schema-valid arguments:** the argument-validity guarantee `any` gave you comes back with strict tool use - `strict: true` on the tool definition (with `additionalProperties: false` in the schema) under `auto`. (In a CMEK organization, structured outputs including `strict: true` aren't available on Fable models - rely on the instruction alone.)
- **Extracting structured data:** if the forced call existed only to get JSON back, replace it with structured outputs (`output_config.format`) - see the prefill-replacement table under Breaking Changes by Source Model for the `messages.parse()` / `output_config.format` shapes.
- **Advisor tool:** a Claude Fable 5.1 or Claude Mythos 5.1 *executor* rejects forced `tool_choice` too, so nudge the advisor call from the prompt instead (see `shared/tool-use-concepts.md` § Advisor).
```python
# Before - 400 on Claude Fable 5.1
response = client.messages.create(
model="claude-fable-5",
max_tokens=4096,
tools=[get_weather_tool],
tool_choice={"type": "tool", "name": "get_weather"},
messages=[{"role": "user", "content": "Check Tokyo, then summarize."}],
)
# After - let it think, name the tool, keep the schema guarantee with strict tool use
get_weather_tool["strict"] = True # schema must set additionalProperties: false
response = client.messages.create(
model="claude-fable-5-1",
max_tokens=4096,
tools=[get_weather_tool],
tool_choice={"type": "auto"},
messages=[{"role": "user", "content": "Use the get_weather tool to check Tokyo, then summarize."}],
)
```
### Breaking change 2: thinking blocks are preserved only for the model that produced them, or a newer one
Every `thinking` block records which model produced it. Claude Fable 5.1 and Claude Mythos 5.1 read each other's blocks and those from Claude Opus 5, Claude Fable 5, Claude Mythos 5, and earlier models that don't encrypt their reasoning in the signature (Opus 4.8 and earlier Opus, Sonnet, Haiku 4.5) - so a conversation that *moves onto* `claude-fable-5-1` keeps its earlier reasoning. They don't read Mythos Preview's blocks. **The binding is one-way: apart from Claude Mythos 5.1, no other model can read a Claude Fable 5.1 block.**
When a request carries a block the receiving model can't read - a router switch, a client-side retry on another model, a classifier refusal fallback (server-side or SDK middleware) - the API drops it before the model sees it: the request succeeds, the dropped block doesn't count toward `input_tokens` and isn't billed, and the target model re-plans without that reasoning (expect higher cost and latency on the first turn after a switch). A dropped block changes the cached prefix from its position onward on that request. Without the `thinking-binding-controls-2026-08-01` beta header the drop is silent; with it, the response carries a top-level `input_transformations` array naming each dropped block with `reason: "model_binding_mismatch"` (shape below). Amazon Bedrock is configured to read a narrower set today (own family only) - confirm at launch.
Keep passing thinking blocks back unchanged when you switch models - the API drops what the target can't read, unbilled, so there are no input tokens to save by stripping; removing blocks yourself can trigger ordering/signature 400s, and a fallback-credit retry must echo the refused body unchanged.
### Breaking change 3: thinking blocks are preserved only in the conversation that produced them
The published docs file this and breaking change 2 together under *preserved thinking* ("pass blocks back unchanged and let the API decide which the model can use"); this one is the conversation check - editing earlier turns invalidates every later thinking block. The API field names for it say `prefix_mismatch_behavior` / `prefix_binding_mismatch` - the same check.
A Claude Fable 5.1 thinking block's `signature` also records the conversation prefix that produced it - the top-level `system` prompt, the set of tools in `tools`, and every message before the block (with server-side compaction, the prefix starts at the most recent compaction block) - plus a chain to the previous thinking block across turns (earlier thinking blocks aren't part of the prefix, but each block records the one before it, which is why blocks can be removed from the *front* of the history and not from the middle). When the transcript comes back, the API checks that this prefix is unchanged. Claude Code, claude.ai, Managed Agents, and the Agent SDK keep the prefix intact for you; **if your code builds the `messages` array itself, check it before migrating** (the three-step check is below). **Who is enforced:** new accounts **created on or after August 31, 2026** (Claude API organizations, Amazon Bedrock accounts, Google Cloud projects, Microsoft Foundry resources). Anthropic plans to enforce it for every account on future models, so adopt the patterns now even if your account isn't enforced today. For accounts created earlier the API *records* the mismatch but acts on it only when the request opts in: setting `thinking.block_binding.prefix_mismatch_behavior` - **any value, including `"error"`, opts the request into enforcement**, which is also how you test from an older organization - or sending the `thinking-binding-controls-2026-08-01` header alone, which opts the request into the beta's default, `drop_block`. If you ship a tool or framework that people run with their own API key, test with the field set: your users on new organizations are enforced before you are. To see whether your own organization is enforced by default, send a request that edits history without the beta header - a 400 that names the header means it is. Platform note: the opt-in controls themselves (the beta header, `prefix_mismatch_behavior`, `input_transformations`) are on the Claude API and Claude Platform on AWS at launch, arrive per model on Amazon Bedrock and Google Cloud (until then the header is rejected there), and aren't offered on Microsoft Foundry - on a platform without the controls the opt-in test path doesn't apply and recovery is strip-and-retry (`shared/platform-availability.md` has the matrix).
**What invalidates every later thinking block:**
- Editing, reordering, or removing an earlier turn while keeping later ones - including deleting old tool results (use server-side tool-result clearing instead).
- Injecting per-request text into an earlier turn (a reminder, a status line, a token count) that you remove or rebuild on the next request.
- Rebuilding the top-level `system` prompt or `tools` array between requests in the same conversation.
- Removing a thinking block from anywhere other than the start of the run (see below).
- An image or document URL in an earlier turn that serves different bytes on a later request - the bytes are bound, not the URL string, so a rotating signed URL for the same file is fine; for content referenced across turns, upload it once with the Files API and send the `file_id`, or send base64.
**What keeps later blocks valid:** append-only histories, including appended `role: "system"` messages and cleared turn-scoped (`clear_at`) messages or reminder text blocks left in place; removing a *leading* run of thinking blocks, oldest first (the first block in the conversation - or the first after the most recent compaction block - then the next, and so on); reordering `tools` without changing them (bound as a name-sorted set; confirm at launch) and adding a `defer_loading: true` tool nothing has referenced yet; changing any request parameter outside `system` / `tools` / `messages` (`max_tokens`, `output_config` incl. `effort`, `tool_choice`, `metadata`); adding, moving, or removing `cache_control` markers; a rotating signed URL that returns the same bytes; server-side compaction and context editing, including thinking-block clearing (they don't count as edits, because the check compares the conversation *as you sent it*, not the server's edited copy; after a compaction the checked prefix starts from the compaction block).
**Where the check is enforced, a request that replays an invalidated block is rejected** with a 400 `invalid_request_error`, decided before any output. Retrying the same body fails the same way; the token-counting endpoint runs the same check. (In the Message Batches API the *unset* default drops failing blocks instead of failing the item - set `"error"` explicitly if you want batch items to error.)
```text
messages.5.content.0: Invalid `signature` in `thinking` block. The block is bound to a different conversation. Remove the block, or set `thinking.block_binding.prefix_mismatch_behavior` to "drop_block". That setting requires the `thinking-binding-controls-2026-08-01` value in the `anthropic-beta` header.
```
The last sentence appears only when the request didn't send the beta header; the message can end with one more sentence naming the first message that changed - the actionable diagnostic. (A tampered or undecryptable signature is a different failure: the same leading clause with *no* "bound to a different conversation" sentence, always a 400, and `prefix_mismatch_behavior` doesn't apply.) Two recoveries:
1. **Strip every `thinking` and `redacted_thinking` block from the history** (each turn's `text` and `tool_use` blocks stay), then retry once - the no-beta path. The model answers that turn without the reasoning those blocks carried. Dropping thinking once, at a boundary such as a compaction, has little effect; an integration that invalidates its own history on every request loses that reasoning and restarts the prompt cache each time, which can raise cost per task. Treat this as a one-time recovery, not a steady-state pattern.
2. **Ask the API to drop instead of erroring:**
```http
POST /v1/messages
anthropic-beta: thinking-binding-controls-2026-08-01
{"model": "claude-fable-5-1", "max_tokens": 4096,
"thinking": {"type": "adaptive", "block_binding": {"prefix_mismatch_behavior": "drop_block"}},
"messages": [ ...full history with thinking blocks replayed verbatim... ]}
```
`thinking.block_binding.prefix_mismatch_behavior` takes `"error"` or `"drop_block"`. The defaults differ by surface: without the header, an enforced account errors on a mismatch (the 400 above); sending the header **alone** switches the request to the beta's own default, `drop_block` - so set the field explicitly rather than relying on either default (the header is what lets you set the field, and it adds `input_transformations` to responses). With `"drop_block"` the API drops the first mismatched block **and every thinking block after it** (up to the next compaction block, if any - including blocks in an assistant turn whose `tool_use` is still waiting on its `tool_result`), the request proceeds, and each drop is reported in the response's top-level `input_transformations` array:
```json
"input_transformations": [
{"type": "thinking_dropped", "path": "messages.1.content.0", "reason": "prefix_binding_mismatch"}
]
```
The drop applies to *that request only*: keep sending `"drop_block"` for the rest of the session, or remove the failing blocks from the history yourself. `reason` is `"prefix_binding_mismatch"` (your history changed) or `"model_binding_mismatch"` (the conversation switched models - not a bug in your code); ignore entries whose `type` or `reason` you don't recognize, because later checks add values. With the header, every response from a thinking-capable model carries the array (empty when nothing was dropped, never `null`); without it the field is absent. When streaming it arrives on the `message` object in `message_start` (and again in the final `message_delta` after a mid-stream server-side fallback). Sending `block_binding` without the header is a 400 ending in `block_binding: Extra inputs are not permitted`. The object is accepted alongside `thinking.type: "adaptive"` and `"enabled"`, and models that don't enforce the conversation check accept it and report only model-check drops, so one request body works across models. The launch SDKs type it in the beta namespace (`client.beta.messages.create(..., thinking={"type": "adaptive", "block_binding": {"prefix_mismatch_behavior": "drop_block"}}, betas=["thinking-binding-controls-2026-08-01"])`; typed enum names such as `PrefixMismatchBehavior` are open at launch - fall back to `extra_body` / a cast if the field isn't typed yet). Some older tooling spells the field `block_binding.mismatch_behavior` - an undocumented alias; write the canonical name and never send both.
**The three-step check for an existing integration:**
1. Capture the exact request bodies it sends over a few normal turns, including a compaction or a tool change if the product has them. For each pair of consecutive requests, compare the `system` prompt, the `tools` array, and the shared prefix of `messages` - they should be byte-identical up to the newly appended turns.
2. Run a normal multi-turn session against `claude-fable-5-1` with the `thinking-binding-controls-2026-08-01` header and `prefix_mismatch_behavior: "drop_block"`, and log `input_transformations` on every response. An empty array on every turn means the history is intact; a `prefix_binding_mismatch` entry means something before the block at `path` changed since the previous request; a `model_binding_mismatch` entry means the conversation switched models. This works from any organization on a platform that offers the controls (see the platform note above; strip-and-retry is the recovery elsewhere), because setting the field opts the request into enforcement. In CI, set `"error"` instead so an edit fails the run.
3. Choose a production setting and **set it explicitly** under the `thinking-binding-controls-2026-08-01` header (the defaults differ by surface - above): `"error"` if a prefix mismatch can only mean a bug in your code, or `"drop_block"` to degrade instead of fail - and monitor the 400s or the `input_transformations` entries either way. Don't leave the field unset: on an account created before 2026-08-31, an unset field with no header means the check only records server-side - no 400s and no `input_transformations` to monitor (see the defaults note above).
**Making a harness compatible - replace each transcript edit with its append-only form:**
| You were doing | Do this instead |
|---|---|
| Editing the system prompt mid-session | Freeze the top-level `system` at session start; append a `{"role": "system", "content": "..."}` message at the point where the change becomes true (GA, no header; see `shared/prompt-caching.md` § Mid-conversation system messages). It gets system-prompt authority and becomes part of the prefix later blocks are locked to. |
| Editing the `tools` array mid-session | Declare the full set in `tools` at session start (`defer_loading: true` on the ones that start hidden) and send `tool_addition` / `tool_removal` blocks in a `role: "system"` message (beta `mid-conversation-tool-changes-2026-07-01`; `shared/tool-use-concepts.md` § Mid-conversation tool changes). |
| Injecting a per-turn reminder and deleting it next request | Send it as a turn-scoped system message (`clear_at: "next_user_message"`, addition 2 below) after the `tool_result` message and leave it in the history; without that beta, a text block after the `tool_result` blocks in the same user message, earlier copies left in place. |
| Deleting old tool results / snipping old turns client-side | Server-side context editing (tool-result clearing, thinking clearing) or compaction - they don't count as edits (the check compares the conversation as you sent it). |
| Compacting | Prefer server-side compaction (beta `compact-2026-01-12`; its `instructions` parameter takes your own summarization prompt) or context editing - neither counts as an edit. Client-side, **simple compaction** is the recommended shape: when the conversation grows too long, summarize it into a single message, start the next request with that summary plus the new user turn, and replay nothing else - no earlier turns, no earlier thinking blocks. Nothing carried over is tied to the old transcript; Claude models are trained on long-horizon tasks with this scheme and it performs comparably to more elaborate ones. Any compaction resets the cache, and don't compact in the middle of a tool round (an assistant turn whose `tool_use` is still waiting on its `tool_result` should go back with its thinking intact). Thinking from before the summary isn't carried forward, so the summary is all the model has of that work - tell the summarizer what to retain (the compaction prompt under Behavioral shifts, or server-side compaction's `instructions`). |
| Referencing an image/document by URL across turns | Upload once to the Files API and send the `file_id`, or send base64. |
Two client-side compaction shapes **break** under the check. *Keep-tail compaction* (summarize older turns, keep the most recent turns verbatim) fails on the retained turns: their thinking blocks were created with the full history present, so replaying them after the summary returns a 400 even though the retained turns are unchanged - strip the thinking blocks from the retained turns (text and tool calls can stay) or set `"drop_block"`. *Background (async) compaction* (compact off the critical path and swap the summary in while the conversation continues) fails the same way but affects more of the transcript: by the time the summary lands, several newer turns exist above the swap point and all of their thinking blocks predate it - send `"drop_block"` on every request that still carries pre-swap thinking blocks (or strip those blocks yourself; `input_transformations` on the first response after the swap lists exactly which ones), or compact synchronously. The same applies to the compaction beta's `pause_after_compaction` flow if you re-insert assistant turns after the compaction block: remove their `thinking` blocks or send `"drop_block"`. Snipping individual turns out of the *middle* of the transcript invalidates every later thinking block, and no client-side shape avoids it - use a mid-conversation system message for the instruction change you were making, or server-side context editing for selective removal.
### What carries over unchanged from Claude Fable 5
The API surface, limits, per-token pricing, tokenizer, always-on adaptive thinking, refusal handling, and `stop_details` categories all match Claude Fable 5: no `thinking` config other than `{type: "adaptive"}` (`disabled` and `budget_tokens` both 400), `display` defaults to `"omitted"` and the raw chain of thought is never returned, interleaved thinking is automatic (no header), no assistant prefill, no non-default sampling parameters, 512-token minimum cacheable prompt, mid-conversation system messages and tool changes supported. The `refusal` stop reason must be handled before reading `content` - the classifiers cover the same categories as Claude Fable 5 (a broader set than Claude Opus 5's cyber-only classifiers), so expect `stop_details.category` values `"bio"` and `"reasoning_extraction"` as well as `"cyber"`. Deltas:
- **Fallbacks:** server-side `fallbacks` (`"default"`, or the array form) and the SDK middleware work as on Claude Fable 5; the permitted targets are `claude-opus-4-8` and `claude-opus-5`, and per-category routing is applied server-side and not published (some categories decline with no fallback). The fallback model can't read Claude Fable 5.1's thinking blocks, so the API drops them (breaking change 2). Fallback credit works as on Claude Fable 5: Claude Fable 5.1 and Claude Mythos 5.1 mint a `fallback_credit_token` on refusals, redeemable on either permitted target (pattern 3 of the refusal section in § Migrating to Claude Fable 5.1 above; for Claude Mythos 5.1 its fallback targets were unwired as of late August - confirm at launch, see the Claude Mythos 5.1 section below); a refusal before any output is unbilled, and the credit refunds the prompt-cache cost of switching models.
- **Data retention:** Claude Fable 5.1 and Claude Mythos 5.1 are Covered Models like Claude Fable 5 - 30-day retention required, **not available under zero data retention unless expressly authorized by Anthropic**. As on Claude Fable 5, a request from an organization or workspace without 30-day retention returns `400 invalid_request_error` ("In order to access this model, your organization or workspace must have data retention enabled.") - check the retention configuration before debugging the payload. (An earlier draft of the launch docs described a 404 with the model hidden from `/v1/models`; the final wording is the 400. If you do see a 404 on the ID, check retention before anything else.) A ZDR organization that needs the model should contact its Anthropic account team (the "expressly authorized" path) or enable 30-day retention for one workspace; a ZDR org that *can* already reach the model has such an authorization, not proof the requirement is gone. (Earlier drafts of the launch docs described a time-bound enterprise exemption through 2026-12-31; that sentence was removed on Aug 28 - don't cite it.)
- **Priority Tier:** not supported on Claude Fable 5.1 or Claude Mythos 5.1 (Claude Fable 5 is). A Claude Fable 5 caller on Priority Tier loses it on migration.
- **Rate limits:** Claude Fable 5.1 shares one "Fable 5.x" pool with Claude Fable 5 (combined traffic; the Mythos models share a separate pool on the same terms) - re-baseline headroom if you run both during the migration.
- **Pricing:** $10 / $50 per MTok, 5-minute cache writes $12.50, 1-hour cache writes $20, batch $5 / $25 - all as Claude Fable 5 - except **cache reads at $0.25 per MTok** (0.025x base input, versus 0.1x on other models - whether Claude Mythos 5.1 shares the 0.025x rate is open at launch): a quarter of the Claude Fable 5 rate and half of Claude Opus 5's. Long agentic sessions that re-read a cached prefix get most of the saving; caching break-even math in `shared/prompt-caching.md` shifts accordingly - and because a miss is now much more expensive relative to a hit, keeping the cache warm matters more: per-message effort and turn-scoped system messages exist partly for that, and for idle gaps of 5-60 minutes a `max_tokens: 0` keep-alive re-send on the default 5-minute TTL is usually cheaper than the 1-hour TTL (send it with `stream` off; not with structured outputs or Batches - see `shared/prompt-caching.md` § Choosing the TTL). Expect cost per task at or under the Claude Fable 5 figures in `shared/cost-optimization.md`.
- **Tool surface:** the same tool versions as Claude Fable 5 - code execution `code_execution_20250825` / `_20260120` / `_20260521` (programmatic tool calling needs `_20260120` or later), tool search (`tool_search_tool_regex_20251119`, `_bm25_20251119`), computer use `computer_20251124`, browser use, structured outputs, web fetch with dynamic filtering (`web_fetch_20260318`), and the advisor tool (as executor or advisor; Claude Fable 5.1 / Claude Mythos 5.1 advisors return the encrypted `advisor_redacted_result`). Task budgets: beta (`task-budgets-2026-03-13`, 20k minimum) - confirm at launch.
- **Content provenance (new, no request change):** text from Claude Fable 5.1 and Claude Mythos 5.1 carries Anthropic's statistical text watermark on every platform (no extra tokens or hidden characters, nothing about your org). Supported image, audio, and video files Claude produces in the code-execution sandbox carry signed C2PA Content Credentials when downloaded through the Files API on the Claude API - the manifest adds a few kilobytes, so the downloaded file's size and checksum differ from the file inside the container; text, PDF, and office files aren't signed. Platform scope beyond the Claude API is open at launch.
- **1M context on Bedrock / Google Cloud and the batch 300k-output beta:** open at launch - confirm before promising either on a partner platform.
### New API features
Three additions, each behind a beta header. All optional - a migrated request works without them - but the first two are how a harness stays cache-friendly and keeps its thinking preserved, so read them before touching an agent loop.
**1. Per-message effort - beta `mid-conversation-output-config-2026-07-01`.** On Claude Fable 5.1, Claude Mythos 5.1, and Claude Opus 5 (Claude API; Bedrock / Google Cloud / Foundry not confirmed at launch, and Claude Opus 5 is excluded on Bedrock), a `role: "system"` message with empty content and `output_config: {effort: ...}` changes effort from that point on without invalidating the prompt cache - raise it for a hard step, lower it for routine ones:
```http
POST /v1/messages
anthropic-beta: mid-conversation-output-config-2026-07-01
{"model": "claude-fable-5-1", "max_tokens": 4096,
"output_config": {"effort": "high"},
"messages": [
{"role": "user", "content": "Plan the migration."},
{"role": "assistant", "content": "Here's the plan: ..."},
{"role": "system", "content": [], "output_config": {"effort": "low"}},
{"role": "user", "content": "Now rename the config file."}
]}
```
Values are the level names (`low`, `medium`, `high`, `xhigh`, `max`). The new level takes effect from the next `user` turn and holds until a later `role: "system"` message changes it. An effort-only message carries no text, so the placement rules for mid-conversation system messages don't apply - it can sit anywhere in `messages`, including first or between an assistant turn and the next user turn. Lowering effort this way is reliable; raising works best for large jumps (e.g. `low` to `xhigh`). On Claude Fable 5.1 prefer this form over changing the top-level value between requests: a top-level change restarts the cache *and* steers the model less reliably (its earlier replies were written at the previous level and it tends to stay consistent with them) - though a top-level change does not invalidate thinking blocks. Unsupported models, Claude Fable 5 included, 400: `output_config.effort requires a model that supports per-turn effort; this model does not`. The older spellings `mid-conversation-effort-2026-08-01` and `per-turn-control-2026-07-01` still resolve to the same feature but are undocumented - don't write new code with them. Open at launch: whether the beta opens to all organizations or stays a limited (allowlisted) beta. This supersedes the "per-turn effort is not in this launch" note in the Claude Opus 5 checklist.
**2. Turn-scoped mid-conversation system messages - beta `mid-conversation-system-clear-at-2026-08-21`.** A harness often needs to tell the model something that is only true for one turn ("check your inbox before running code", "the user can't see that tool output"). Injecting the reminder and deleting it next request is a history edit - it restarts the prompt cache and, on Claude Fable 5.1, invalidates every later thinking block. Instead give a `role: "system"` message `clear_at: "next_user_message"`: its text carries system-prompt authority for the current turn, then stops rendering once a later `user` message exists. **Keep sending it back verbatim** - it stays in `messages`, so nothing earlier changes, the cache keeps matching, later thinking blocks stay valid, and a cleared message costs no input tokens.
```http
POST /v1/messages
anthropic-beta: mid-conversation-system-clear-at-2026-08-21
{"model": "claude-fable-5-1", "max_tokens": 4096,
"tools": [...],
"messages": [
{"role": "user", "content": "Run the analysis script."},
{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01", "name": "bash",
"input": {"command": "python analyze.py"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_01",
"content": "Analysis complete; report written."}]},
{"role": "system", "clear_at": "next_user_message",
"content": "Results have landed in your inbox; check it before running more code."}
]}
```
The main use is a per-turn reminder in a tool loop: append the message after each `tool_result` user message you want it in view for, and **leave every earlier copy where it is** - a `tool_result`-only user message counts as the next user message, so the earlier copies are already cleared (rendering nothing, costing nothing, still part of the prefix the thinking is tied to) and the model reads only the newest one. Rules: `clear_at` takes `"never"` (the default) or `"next_user_message"`; a turn-scoped message is `text`-only (no `tool_addition`/`tool_removal` blocks, no `output_config`), takes no `cache_control` (put the breakpoint on the preceding user turn), and follows the normal placement rules - one followed directly by another `user` message is a 400, so put all of a tool round's results in one user message and the reminders after it. Deleting, rewording, rebuilding from current state, or changing the `clear_at` of a copy already sent is an edit like any other. Same models and platforms as mid-conversation system messages (on Bedrock and Google Cloud pass the beta value the way that platform passes betas); the launch SDKs may not type the field yet - send it via `extra_body` / a cast. **Without the beta**, append the reminder as a `text` block after the `tool_result` blocks in the same user message and leave earlier copies in place - the model acts on the newest one.
**3. Progress updates between tool calls - `thinking.display: "updates"`, beta `thinking-display-updates-2026-08-18`.** Between tool calls, Claude Fable 5.1, Claude Mythos 5.1, and Claude Fable 5 write short progress updates - what it just found, what it will do next - each returned as its own `thinking` block with its own signature immediately before the tool call it introduces, separate from any reasoning block at the same point. Under the default `display: "omitted"` those blocks come back empty, like reasoning, which is why a long agentic turn can look silent for minutes. Request `display: "updates"` and the progress updates come back as text while reasoning stays hidden:
```http
POST /v1/messages
anthropic-beta: thinking-display-updates-2026-08-18
{"model": "claude-fable-5-1", "max_tokens": 4096,
"thinking": {"type": "adaptive", "display": "updates"},
"tools": [...],
"messages": [{"role": "user", "content": "Review the PRs open against our billing service."}]}
```
How to consume them: under `"updates"` **any `thinking` block with non-empty text is a progress update** (normally a sentence or two) - render it as a status line; render nothing for an empty block (a progress block can come back empty under any `display` value). When streaming, a progress block streams its text as `thinking_delta` events before the `tool_use` block it introduces - treat a block as a progress update as soon as a `thinking_delta` carries non-empty text; a pause of several seconds before the block opens is normal. A response can contain zero of them and the model can skip any gap, so build for zero-or-more. When a response stops on `max_tokens`, `model_context_window_exceeded`, or `stop_sequence` soon after a tool call or result, its last block can be a progress block standing in for unfinished work whose text is exactly `This part of the response was interrupted before it finished.` - to continue, pass the assistant turn back unchanged and append a new `user` message (with a `tool_result` for each `tool_use` in that turn). The update is billed at its full length in `usage.output_tokens`, not the summary's. Echo progress blocks back unchanged like any thinking block. `"summarized"` returns their text too, mixed with the reasoning summaries. Available on every platform - on Bedrock, Google Cloud, and Foundry pass the beta value the way that platform passes beta headers; without it `"updates"` is rejected as an unknown `display` value.
### Coming from Claude Opus 5
Beyond the three breaking changes: `thinking: {type: "disabled"}` 400s at **any** effort (on Claude Opus 5 it was accepted at `high` or lower) - remove it, control spend with lower effort, and revisit `max_tokens`. Text the model wrote *between tool calls* on Claude Opus 5 came back as `text` blocks; on Claude Fable 5.1 it comes back as progress-update `thinking` blocks, empty under the default `"omitted"` - set `display: "updates"` (or `"summarized"`) if your UI rendered that narration. The classifier set is broader (`bio`, `reasoning_extraction` in addition to `cyber`). ZDR is lost (Claude Opus 5 is available under ZDR). Pricing goes from $5 / $25 to $10 / $50 per MTok, with cache reads at half Claude Opus 5's rate; the 512-token cache minimum is unchanged. Per-message effort already works on Claude Opus 5, so an Claude Opus 5 harness that uses it needs no change there.
From Opus 4.8 or earlier: apply § Migrating to Claude Fable 5.1 above first (Opus 4.7 or earlier: the Claude Opus 5 section before that), then this one - and budget time for the history-editing check: integrations written for Opus 4.8 and earlier often truncate old turns, strip or rebuild earlier messages, or refresh the `system` prompt each request, and Opus 4.8 never objected. Review prompts near the 512-token caching minimum.
### Claude Mythos 5.1
`claude-mythos-5-1` is the same model as Claude Fable 5.1 - same capabilities, limits, API behavior, and per-token pricing (cache-read rate open at launch) - offered only to approved Project Glasswing customers, and the only model besides Claude Fable 5.1 that reads Claude Fable 5.1's thinking blocks (it also reads Claude Mythos 5's; not the reverse). Confirm the organization's access with the account team before switching IDs. Two differences from a Claude Mythos 5 migrator's point of view: **Claude Mythos 5.1 runs safeguards** that depend on the access program the organization is approved under (Claude Mythos 5 ran none) - handle `stop_reason: "refusal"`, read `stop_details.category`, and set up fallback as on Claude Fable 5.1 (its fallback targets were unwired as of late August; confirm at launch) - and it is **not offered on Claude Platform on AWS** (Claude API, Amazon Bedrock as `anthropic.claude-mythos-5-1` in us-east-1 only and not publicly listed, Google Cloud, Microsoft Foundry). It shares the Mythos rate-limit pool with Claude Mythos 5. Whether Claude Mythos 5 access carries over automatically is open at launch.
### Capability improvements versus Claude Fable 5
The gap is widest at higher effort levels. Six areas: **agentic coding over long sessions** (multi-file features, large refactors and migrations, debugging, code review across sessions that run for hours); **knowledge work with documents, spreadsheets, and slides** (from a first question to a finished document, live-formula spreadsheet, or deck built from a blank page); **research and search** (multistep web research that follows up on what it finds); **vision** (dense charts, filings, and tables nested in PDFs - strongest when it has tools to crop and zoom); **long-context retrieval** deep in the 1M window; and **computer use** (operating a browser and desktop applications more reliably, recovering from failed steps). Multilingual performance is on par with Claude Fable 5. Held pending confirmation at launch: that it expands a request's scope partway through less often, and that an instruction given once at the start of a long session persists better - if the latter holds, remove instruction repetition inserted every few turns for Claude Fable 5 and re-test (the per-turn batching nudge below is a separate case: it targets one behavior on the next turn, so keep it where measurements show it helps).
### Behavioral shifts (prompt-tunable)
None of these are API-breaking. The behavioral guidance in § Migrating to Claude Fable 5.1 above (longer turns, grounding progress claims, stating boundaries, delegation, memory surfaces, the readability addendum) still applies; these are the Claude Fable 5.1-specific deltas. Three of them show up without any code change: it batches implied tool calls less, narrates less between tool calls, and answers from memory more at `low` effort.
**Effort.** Start with `high` (the default) and re-run your effort sweep even if you ran one on Claude Fable 5 - level names don't correspond to the same amount of thinking across models. The gains over Claude Fable 5 show up across levels and are largest at the higher settings; at `medium`, results roughly match Claude Fable 5 at lower cost, so step down to `medium` or `low` where your evals show quality holds. At `high` and above set a large `max_tokens` - it is a hard limit on total output (thinking plus response). At `low`, Claude Fable 5.1 is often competitive with Opus and Sonnet on cost per task while performing better - evaluate low Fable effort against below-frontier usage before reaching for a cheaper model. Per-message effort (addition 1) lets one conversation mix levels without a cache reset.
**Long deliverables at `xhigh` and `max`.** At `xhigh`, and especially `max`, the model thinks more before it starts writing. When one request asks for a long deliverable - a full rewrite of a long document, a large table, a complete code file - it may draft much of it in its thinking and then write it out again as the reply: a longer wait and roughly double the output tokens. Simplest fix: run those requests at `high` (the recommended start anyway) and move up only where you've measured a quality gain. If you do run them at `xhigh`/`max`, set `max_tokens` to leave room for the thinking *and* the reply, and append this to the end of the user message - it makes the thinking much shorter on prose and code requests (replace the bracket with the request's actual `max_tokens`, e.g. 64,000). Like every appended per-request note on this model (addition 2 above), leave each earlier copy in place byte-for-byte on later requests, each keeping the value it was sent with - removing or rebuilding one is a history edit that invalidates the thinking blocks after it:
> Everything Claude produces in one reply, including any reasoning or drafting it does before the reply, counts toward a single limit of about [max_tokens] tokens. If that limit is reached before the reply is finished, the person receives a cut-off response and has to start over. Composing an entire output or deliverable in full as reasoning and then again as a reply would double the length of the turn without improving the result, so Claude doesn't do that.
> Instead, when the person has asked for a long or effort-intensive deliverable such as a multi-section document, a large table or dataset, or a complete code file, Claude spends extra effort on understanding the request, checking the inputs Claude's answer depends on, settling the structure and other difficult decisions, and otherwise using the reasoning space to reason and the output space to write an output. If Claude plans well then it should not need to draft its output multiple times (and Claude is pretty good at planning, so this should not be an issue).
**Batch independent tool calls in agent loops.** When a request explicitly names several things to fetch, Claude Fable 5.1 issues those calls in parallel; standard function calling is unaffected. In long agent loops where the next independent reads are only *implied* (custom coding agents, bash-and-editor harnesses, computer use) it may issue one call per turn where Claude Fable 5 batched several - same answers, more round trips and wall-clock. Measure first: track the share of assistant turns with more than one tool call, and add the nudge only if that share is low (over-batching shows up as calls issued before results they depend on). Placement matters more than wording - one sentence near the end of the current request moves the number far more than the same text in the system prompt or a tool description. Each time you send tool results back, append the sentence after that user message as a turn-scoped system message (`clear_at: "next_user_message"`, addition 2) - or, without that beta, as a `text` block after the `tool_result` blocks in the same user message - **appending a fresh copy each turn and leaving the earlier copies in place byte-for-byte**; rewriting earlier turns to remove them restarts the cache and, on this model, invalidates the thinking blocks after them. Keep the word "privately" - without it the model sometimes answers the reminder ("nothing further is needed") instead of the user in its final reply:
> First privately list what you need next; then request every item that doesn't depend on another's result in this one response.
**User-facing progress updates.** Claude Fable 5.1 writes fewer user-facing updates during long tool-calling turns than Claude Fable 5 - more so at higher effort and in longer tool chains. Users see the agent go quiet for minutes, or a final message that describes only the last step; its agentic coding summaries are shorter too. In order: (1) **request `display: "updates"`** (addition 3) - if you aren't, the model's between-tool notes aren't reaching you; (2) **remove prompt text written for update-eager older models** ("hold all findings for the final response", "don't narrate") *before* adding anything; (3) if you still want more - pair programming, human-in-the-loop - add a short, specific system-prompt line saying when you want user-facing text:
> Before you start, say in a line what you're about to do; brief updates while you work help the user follow along. Close with a short recap that stands on its own - what you found, what you did, and what's next - so a reader who only sees the last message has the full picture.
Relatedly, **if the harness collapses or hides tool output, tell the model** - otherwise Claude Fable 5.1 may run commands to "show" the user output they cannot see. Deliver it as a turn-scoped system message (`clear_at: "next_user_message"`, addition 2), or without the beta alongside the tool results in the same user message, left in place on later requests:
> Only you see that command's output - the user's terminal shows at most a few lines of it. If the user needs to read any of it, put it in your reply.
**Writing density.** Claude Fable 5.1's writing is generally preferred, but prose can be denser than Claude Fable 5's - longer sentences, fewer paragraph breaks. Defining "mannered prose" as an anti-pattern has helped; style instructions placed in the first user turn of a session hold better than the same text in the system prompt:
> Mannered prose substitutes metaphor and flourish for direct statement. Instead of "a parameter worth varying," the mannered writer produces "a dial worth turning." Instead of "this point still matters," they write "this point earns its keep." The phrases exist to display the writer, not to convey the idea, and readers can tell. That is why mannered prose irritates: it makes the reader work harder so the writer can perform. It is also imprecise. Metaphors drag in connotations the writer did not choose and cannot control. The fix is to say what you mean. When a literal phrase is available, use it.
The short form - "Please remove all mannered prose." - also tends to work.
**Formatting.** Where earlier models over-used bullets and bold in chat, Claude Fable 5.1 does the opposite: less bold, fewer headers, lists, and quotation marks. **If the prompt contains anti-formatting language, remove it** or replace it with a rule that says when formatting is appropriate:
> Use lists and bullet points when asked to, or when the content is multifaceted enough that they help with clarity. If the person explicitly requests minimal formatting, always format your responses without bullet points, headers, lists, or bold emphasis, as requested. In conversational, personal, or emotional exchanges, keep to plain prose.
When summarizing documents, Claude Fable 5.1 is more likely than Claude Fable 5 to reproduce passages of source text without marking them as quotations. The fix is one complete example of a correct response in the system prompt - the user's request, the response, and a sentence saying why the response is correct. Replace the two `[web_search: ...]` lines with your own tool name so the model reads them as templated tool output, not as literal desired output:
```xml
<example>
<user>look up how the Riverton Ledger and the Coast Dispatch each covered the Harbor Bridge closure and compare their reporting</user>
<response>
[web_search: Harbor Bridge closure Riverton Ledger]
[web_search: Harbor Bridge closure Coast Dispatch]
Both outlets agree on the basics: the bridge closed on March 3 after inspectors found cracked welds, and the state expects repairs to take about eight months. Where they differ is emphasis. The Ledger treats it as a local-economy story. The Dispatch frames it as a funding failure; its editorial calls the closure "entirely foreseeable." Read together, the Ledger explains who is affected now and the Dispatch explains how it came to this - neither account alone gives the whole picture.
</response>
<rationale>CORRECT: The response is organized around where the two outlets agree and differ, not as a walk through either article. Each outlet's reporting is conveyed in one or two sentences of the assistant's own indirect speech. One short marked phrase from one source; every other claim is reworded. The response is still specific and complete.</rationale>
</example>
```
**Maximizing long-horizon execution.** Claude Fable 5.1 is capable of very long autonomous runs, but on complex asynchronous workloads it needs a nudge not to stop at *describing* the next step ("Next, I'll ...") or asking permission for a step the request already covered ("Shall I apply this?"). Users experience it as having to reply "continue" - fine for pair programming, but it caps the model's long-horizon capability. Two system-prompt additions together mitigated this; apply both unless context is tight, in which case the first keeps most of the effect. The opening sentence of the first ("The user is not watching") is load-bearing - keep it as written; if the product needs stops for specific confirmations, add a sentence listing them. This prompt can make the model less likely to clarify ambiguous requests. With either block the model writes slightly more code - mostly extra tests in files it's already editing - so pair them with the "ground progress claims" audit instruction in § Migrating to Claude Fable 5.1 above and the test-coverage line below. If your existing prompt asks the model to test or check its work before reporting, **keep it** when migrating - the Claude Opus 5 guidance to delete verification instructions doesn't apply here (tentative: rests on a small number of reports).
> You are operating autonomously. The user is not watching in real time and cannot answer questions mid-task, so asking 'Want me to...?' or 'Shall I...?' will block the work. For reversible actions that follow from the original request, proceed without asking. Stop only for destructive actions or genuine scope changes the user must decide. Offering follow-ups after the task is done is fine; asking permission before doing the work is not.
>
> Exception: when the user is describing a problem, asking a question, or thinking out loud rather than requesting a change, the deliverable is your assessment. Report your findings and stop. Don't apply a fix until they ask for one.
>
> Before ending your turn, check your last paragraph. If it is a plan, an analysis, a question, a list of next steps, or a promise about work you have not done ('I'll...', 'let me know when...'), do that work now with tool calls. That includes retrying after errors and gathering missing information yourself. Do not stop because the context or session is long. End your turn only when the task is complete or you are blocked on input only the user can provide.
>
> Before running a command that changes system state (such as restarts, deletes, or config edits), check that the evidence actually supports that specific action. A signal that pattern-matches to a known failure may have a different cause.
The second tells it to hold the scope the user set:
> \# Delivering work
> The user's request - or the plan they approved - sets the scope, and the scope is the deliverable: don't quietly narrow, widen, or swap it. Read ambiguity the way a careful colleague would: make routine judgment calls yourself, and check in only when different readings would lead to materially different work. If you see a real problem with the task as specified, say so in a sentence or two and keep building under stated assumptions; if the user hears the concern and reaffirms, that is their decision, so deliver the full request.
>
> If a question comes up partway, first do everything that doesn't depend on the answer; then state the assumption you made, or - when going ahead on a wrong guess would be unsafe or would make the work useless - put the question at the end of a turn that also delivers that progress. If one part turns out to be blocked, complete every other part in full and say exactly what you left out and why - the whole task is the deliverable, and scaling it down is the user's call, not yours. A step you have decided on is something to run, not to announce: describing the next step and ending the turn leaves it undone until the user replies.
>
> Keep changes to what the request needs. Something else you notice worth doing - cleanup or documentation the task didn't call for, a change to a file the task didn't require - is a suggestion to make at the end, not a change to make; actions clearly beyond what the ask implies, and risky or destructive ones, still need the user's go-ahead.
(The published snippets use em dashes and ellipsis characters where this file has hyphens and three periods; the bundled skill is ASCII-only, and the difference has no effect on the model.)
**Scope and test coverage.** Asked to implement an open-ended feature, Claude Fable 5.1 delivers what was asked and sometimes more - fixing nearby code, writing extra tests, committing scratch checks as permanent test files. It responds well to explicit instructions about what to leave out; with this prompt the guide's authors saw far fewer unrequested additions and much less committed test code with no measurable change in task success (an earlier, shorter form - "keep verification scripts outside the repository, e.g. under /tmp, and delete any you did add" - still works if you only care about test sprawl):
> If, while working or testing, you find a pre-existing bug, a performance concern, or behavior the task doesn't mention, don't fix, optimize or extend it in this change unless the requested behavior cannot work without it; report it as a follow-up in your summary. Where the task is ambiguous, implement the reading its wording and the surrounding code most directly support, state that assumption in your summary, and don't build for the other readings as well. Verify your work however you like; scratch scripts and quick checks need not be kept. Commit tests only where the task asks for them or this repository already keeps tests for this kind of change, sized like the neighboring test files - roughly one focused test per stated behavior - and don't turn scratch checks into additional permanent test files. This is about extras only: implement every behavior the task asks for, completely.
**Search triggering at low effort.** At `low` effort, Claude Fable 5.1 calls a search or retrieval tool less often than Claude Fable 5 and answers from memory more - most visibly for named products, models, and tools it recognizes but has out-of-date knowledge of. Raising effort for those turns (per-message effort, feature 1) is often the simplest fix. Otherwise tell it in the system prompt that recognizing a name is not the same as knowing its current state, and that such names should be searched as the user wrote them:
> When a query centers on a name you do not confidently recognize, or recognize from a fast-moving area like AI models and developer tools where the landscape shifts within months, the name itself is the thing to verify: search before answering, and include the name as the user wrote it in at least one query alongside any reformulations. This holds even when you have some background on it - partial background is exactly what makes an out-of-date answer sound authoritative, so familiarity is not a reason to skip the search.
**Vision: let it crop, zoom, and verify.** Claude Fable 5.1's pure vision is better out of the box, and it is best when it can iteratively analyze, crop, and visually verify its own work. For complex inputs - dense charts, filings, tables nested in PDFs, video - run it as an agent with a container that holds the raw images/videos and basic image-processing libraries (PIL, OpenCV) preinstalled. If a container is too much overhead, most of the uplift comes from a single crop tool that takes a bounding box and returns that region cropped and enlarged (the recipe is in the Claude Opus 5 section's vision guidance) - this scales test-time compute with image tokens instead of effort. At `low` effort the model may answer from an overall impression without calling it, so check the logs for the call and raise effort on image turns if it's missing.
**Safeguard false positives.** The classifiers produce fewer false positives than Claude Fable 5's did at launch, and finding vulnerabilities in source code is permitted; a blocked request still returns `stop_reason: "refusal"`, so keep the refusal handling and fallbacks in place. Three situations make false positives more likely: compile-check phrasing (ask "Are there any bugs in this program?" rather than "Does this program compile without errors?"); lesser-known programming languages (give the model context on what the language is and how it works, e.g. its docs); and tools that return base64-encoded data into the model's context (remove them).
**Whole-file rewrites.** Claude Fable 5.1 is more likely than Claude Fable 5 to rewrite an entire file where a targeted edit would do - same result, more output tokens and time. Appending this to the system prompt (or the first user message - equally effective) restored targeted edits for small and medium changes:
> The number of tokens used to edit files is best minimized, all else being equal. Therefore, when it will not affect the end result, try to surgically edit a file rather than rewrite the entire thing.
**Summarization prompt for client-side compaction.** Claude Fable 5.1 responds well to being told explicitly what to retain in a compaction summary. Server-side compaction already does this; if you compact on the client (the simple-compaction shape under breaking change 3), this summarization instruction has been effective. The final sentence is load-bearing when the summarization request still carries the conversation's `tools` (breaking change 3 means you can't drop them for one request): without it the model occasionally calls a tool instead of writing the summary.
> Summarize the transcript inside <summary></summary> tags. Include relevant information in the summary such that this conversation will be continued by a new context window without needing to redo work or be reprovided with relevant constraints or context. Be sure to preserve: (1) any difficulties or problems that came up, and how they were handled or resolved; (2) any possibilities, options, or approaches that were raised, tried, or set aside, and why; (3) anything that was asked for, decided, agreed, ruled out, or established as a preference, constraint, or boundary - stated exactly; (4) exactly where things stand now - what has been covered, settled, or completed so far; (5) anything still open, unresolved, promised, or expected to happen next; (6) specific details that would be hard to reconstruct - names, numbers, dates, exact wording, links or references - kept exactly. Be complete on these even at the cost of length; keep everything else concise. Weight the two voices differently: keep what the user said, asked for, shared, or established carefully and close to their own words; your own explanations and reasoning can be condensed much further, to what they concluded or produced - as long as nothing in the six items above is dropped. Do not call any tools while writing this summary; respond with text only.
**Non-blocking sub-agents in coding.** If the coding agent delegates to sub-agents, Claude Fable 5.1 finishes sooner when the lead is not forced to stop and wait for each one - lower average time to completion at similar quality, token usage, and cost. Have the tool that starts a sub-agent return immediately and deliver the sub-agent's result to the lead in a later user message when ready; the model will still often *choose* to wait, so also give it a separate tool that waits for its sub-agents. The time savings come from the cases where the lead carries on with other work. (This extends the asynchronous-delegation guidance in § Migrating to Claude Fable 5.1 above.)
### Claude Fable 5.1 from Claude Fable 5 Migration Checklist
- [ ] **[BLOCKS]** Update the `model=` string to `claude-fable-5-1` (`claude-mythos-5-1` for Project Glasswing participants coming from Claude Mythos 5; confirm access first)
- [ ] **[BLOCKS]** Remove `tool_choice: {type: "any"}` and `{type: "tool", name: ...}` (400, also on `count_tokens` and Batches) - `auto` plus the instruction in the `user` turn (or an appended `role: "system"` message when the application requires the call), `strict: true` for schema-valid arguments, structured outputs for JSON extraction; delete any retry-on-missing-tool loop that depended on forcing
- [ ] **[BLOCKS]** Coming from an Opus-tier or older model (not from Claude Fable 5): apply the Claude Fable 5.1 Migration Checklist above (the Opus-tier -> Fable migration) first, plus § Coming from Claude Opus 5 - `thinking: {type: "disabled"}` now 400s at any effort, between-tool narration moves into `thinking` blocks, ZDR is lost, price doubles
- [ ] **[BLOCKS]** Data retention: 30-day retention required (Covered Model; ZDR only if expressly authorized by Anthropic) - a ZDR org gets `400 invalid_request_error` on every request, as on Claude Fable 5; check the retention configuration before debugging the payload
- [ ] **[BLOCKS]** Keep passing `thinking` blocks back unchanged on every turn, including empty ones and `redacted_thinking` - the history-editing check rejects edited history
- [ ] **[BLOCKS]** Preserved thinking / the history-editing check (new accounts created on/after 2026-08-31 on every platform, and any request that sets `prefix_mismatch_behavior` or sends the controls beta header; later models enforce it for everyone): stop editing history between requests - freeze the top-level `system`, use `role: "system"` messages for mid-session instructions, `tool_addition`/`tool_removal` for tool changes, turn-scoped (`clear_at`) system messages - or, without that beta, retained user-message text blocks - appended after the tool results and never deleted, for per-turn reminders, server-side context editing / compaction (summary-only if client-side) for trimming, `file_id` for cross-turn files. Run the three-step check on a platform offering the controls beta (`shared/platform-availability.md`) (`prefix_mismatch_behavior: "drop_block"` + log `input_transformations`; fix every `prefix_binding_mismatch`, `model_binding_mismatch` after a model switch is expected; `"error"` in CI), then pick a production setting and monitor it. If you ship a tool others run with their own key, test with the field set. Keep-tail and background compaction need `"drop_block"` (per request - keep sending it) or stripped thinking on the retained turns; never compact mid tool round
- [ ] **[TUNE]** Fallbacks: keep server-side `fallbacks` (targets `claude-opus-4-8` / `claude-opus-5`; routing unpublished) or the SDK middleware; the fallback model can't read 5.1 thinking blocks (dropped, unbilled); fallback credit works as on Claude Fable 5
- [ ] **[TUNE]** Adopt `thinking: {type: "adaptive", display: "updates"}` with `thinking-display-updates-2026-08-18` (all platforms) if users watch long tool-calling turns; render non-empty `thinking` blocks as status lines, handle the interrupted-response sentinel, echo them back unchanged
- [ ] **[TUNE]** Adopt per-message effort (`mid-conversation-output-config-2026-07-01`; also on Claude Opus 5) where a loop mixes hard and routine steps - lowering is reliable, raising wants a big jump; re-run the effort sweep (`high` default; `medium` as cost control; `xhigh`/`max` only for capability-sensitive work; `low` often beats below-frontier models on cost per task); size `max_tokens` for `high`+
- [ ] **[TUNE]** Agent loops: measure the share of multi-tool-call turns and add the "privately list what you need next" nudge (fresh copy each turn, earlier copies kept) if it's low; remove "hold findings for the final response" / anti-narration text and anti-formatting rules before adding the progress-update and formatting snippets
- [ ] **[TUNE]** Add the autonomy + scope prompts for unattended runs; the hidden-tool-output note if the harness collapses tool output; the targeted-edit and scope/test-coverage prompts for coding agents; the long-deliverable note (with the real `max_tokens`) for `xhigh`/`max` requests; the compaction summarization prompt if you summarize client-side; the mannered-prose instruction for prose-heavy work; the name-verification line for search products; a crop tool (or an image-processing container) for vision
- [ ] **[TUNE]** Priority Tier is not supported on Claude Fable 5.1; rate limits share the Fable 5.x pool with Claude Fable 5 - re-baseline headroom; cache reads cost a quarter of the Claude Fable 5 rate (re-check caching break-even; for 5-60 minute idle gaps a `max_tokens: 0` keep-alive on the 5-minute TTL usually beats the 1-hour TTL - sent with `stream` off; not with structured outputs or Batches); tokenizer unchanged from Claude Fable 5, so re-baseline token counts only if you weren't on Claude Fable 5
- [ ] **[TUNE]** Supported image, audio, and video files produced in the code-execution sandbox carry a C2PA manifest when downloaded through the Files API on the Claude API - size and checksum differ from the in-container file (text, PDF, and office files aren't signed; platform scope beyond the Claude API open at launch); adjust integrity checks for signed media only
---
## Verify the Migration
After updating, spot-check that the new model is actually being used. Replace `YOUR_TARGET_MODEL` with the model string you migrated to (e.g. `claude-fable-5-1`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-haiku-4-5`) and keep the assertion prefix in sync:
```python
YOUR_TARGET_MODEL = "claude-opus-5" # or "claude-opus-4-7", "claude-sonnet-5", "claude-sonnet-4-6", "claude-haiku-4-5"
response = client.messages.create(model=YOUR_TARGET_MODEL, max_tokens=64, messages=[...])
assert response.model.startswith(YOUR_TARGET_MODEL), response.model
```
For rate-limit headroom changes, pricing, or capability deltas (vision, structured outputs, effort support), query the Models API:
```python
m = client.models.retrieve(YOUR_TARGET_MODEL)
m.max_input_tokens, m.max_tokens
m.capabilities["effort"]["max"]["supported"]
```
See `shared/models.md` for the full capability lookup pattern.
# Claude Model Catalog
**Only use exact model IDs listed in this file.** Never guess or construct model IDs - incorrect IDs will cause API errors. Use aliases wherever available. For the latest information, WebFetch the Models Overview URL in `shared/live-sources.md`, or query the Models API directly (see Programmatic Model Discovery below).
## Programmatic Model Discovery
For **live** capability data - context window, max output tokens, feature support (thinking, vision, effort, structured outputs, etc.) - query the Models API instead of relying on the cached tables below. Use this when the user asks "what's the context window for X", "does model X support vision/thinking/effort", "which models support feature Y", or wants to select a model by capability at runtime.
```python
m = client.models.retrieve("claude-opus-4-8")
m.id # "claude-opus-4-8"
m.display_name # "Claude Opus 4.8"
m.max_input_tokens # context window (int)
m.max_tokens # max output tokens (int)
# capabilities is an untyped nested dict - bracket access, check ["supported"] at the leaf
caps = m.capabilities
caps["image_input"]["supported"] # vision
caps["thinking"]["types"]["adaptive"]["supported"] # adaptive thinking
caps["effort"]["max"]["supported"] # effort: max (also low/medium/high)
caps["structured_outputs"]["supported"]
caps["context_management"]["compact_20260112"]["supported"]
# filter across all models - iterate the page object directly (auto-paginates); do NOT use .data
[m for m in client.models.list()
if m.capabilities["thinking"]["types"]["adaptive"]["supported"]
and m.max_input_tokens >= 200_000]
```
Top-level fields (`id`, `display_name`, `max_input_tokens`, `max_tokens`) are typed attributes. `capabilities` is a dict - use bracket access, not attribute access. The API returns the full capability tree for every model with `supported: true/false` at each leaf, so bracket chains are safe without `.get()` guards. TypeScript SDK: same method names, also auto-paginates on iteration.
### Raw HTTP
```bash
curl https://api.anthropic.com/v1/models/claude-opus-4-8 \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01"
```
```json
{
"id": "claude-opus-4-8",
"display_name": "Claude Opus 4.8",
"max_input_tokens": 1000000,
"max_tokens": 128000,
"capabilities": {
"image_input": {"supported": true},
"structured_outputs": {"supported": true},
"thinking": {"supported": true, "types": {"enabled": {"supported": false}, "adaptive": {"supported": true}}},
"effort": {"supported": true, "low": {"supported": true}, ..., "max": {"supported": true}},
...
}
}
```
## Current Models (recommended)
| Friendly Name | Alias (use this) | Full ID | Context | Max Output | Status |
|-------------------|---------------------|-------------------------------|----------------|------------|--------|
| Claude Fable 5.1 | `claude-fable-5-1` | - | 1M | 128K | Active |
| Claude Mythos 5.1 | `claude-mythos-5-1` | - | 1M | 128K | Active (Project Glasswing only) |
| Claude Fable 5 | `claude-fable-5` | - | 1M | 128K | Active |
| Claude Mythos 5 | `claude-mythos-5` | - | 1M | 128K | Active (Project Glasswing only) |
| Claude Opus 5 | `claude-opus-5` | - | 1M | 128K | Active |
| Claude Opus 4.8 | `claude-opus-4-8` | - | 1M | 128K | Active |
| Claude Opus 4.7 | `claude-opus-4-7` | - | 1M | 128K | Active |
| Claude Opus 4.6 | `claude-opus-4-6` | - | 1M | 128K | Active |
| Claude Sonnet 5 | `claude-sonnet-5` | - | 1M | 128K | Active |
| Claude Sonnet 4.6 | `claude-sonnet-4-6` | - | 1M | 128K | Active |
| Claude Haiku 4.5 | `claude-haiku-4-5` | `claude-haiku-4-5-20251001` | 200K | 64K | Active |
### Model Descriptions
- **Claude Fable 5.1** - Anthropic's most capable widely released model, for the most demanding reasoning and long-horizon agentic work. Successor to Claude Fable 5 in the same tier at the same per-token price ($10/$50 per MTok; cache reads $0.25/MTok - 0.025x, a quarter of Claude Fable 5's; batch $5/$25); stronger long-running agentic coding, knowledge work with documents/spreadsheets/slides, multistep research, vision, long-context retrieval, and computer use. Same API surface as Claude Fable 5 (thinking always on, no prefill, no sampling params, `refusal` stop reason, 512-token cache minimum) with three breaking changes: forced tool use (`tool_choice` `any` / `tool`) returns a 400; thinking blocks are bound to the producing model (only Claude Mythos 5.1 can read them - other models drop them); and editing earlier turns invalidates thinking blocks ("preserved thinking"; new accounts created on/after 2026-08-31 get a 400 on edited history; later models enforce it for everyone; the opt-in controls are per-platform - `shared/platform-availability.md`). Adds per-message `effort`, turn-scoped `clear_at` system messages, `thinking.display: "updates"` progress updates, and content provenance. Same tokenizer as Claude Fable 5; 1M context (default), 128K max output. Covered Model: 30-day retention required (ZDR only if expressly authorized by Anthropic) - ZDR orgs get `400 invalid_request_error`, as on Claude Fable 5. No Priority Tier; shares the Fable 5.x rate-limit pool. See `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5.
- **Claude Fable 5** / **Claude Mythos 5** (`claude-fable-5` / `claude-mythos-5`) - the previous Fable / Mythos release: same tier, limits and per-token pricing as Claude Fable 5.1, which adds three breaking API changes over them (see above; cache reads here are $1/MTok rather than Claude Fable 5.1's $0.25); still served and selectable by id. Claude Mythos 5 ran no safety classifiers, so `stop_reason: "refusal"` does not occur on it. Prefer claude-fable-5-1 for new work.
- **Claude Mythos 5.1** - The same model as Claude Fable 5.1 (same capabilities, limits, per-token pricing, API behavior), offered only to approved Project Glasswing customers; successor to Claude Mythos 5 (which itself succeeded the invitation-only `claude-mythos-preview`). Unlike Claude Mythos 5 it runs safeguards that depend on the access program, so handle `stop_reason: "refusal"`. Not offered on Claude Platform on AWS. Use it only when the org participates in Project Glasswing; otherwise use `claude-fable-5-1`.
- **Claude Opus 5** - For complex agentic coding and enterprise work; a step-change over Claude Opus 4.8, strongest on deep reasoning, agentic and long-horizon work, and test-time compute scaling, at half the cost of Claude Fable 5.1 (Claude Fable 5.1 remains the highest-capability tier). Safety classifiers can return `stop_reason: "refusal"` - handle it before reading `content`. A drop-in upgrade at Opus 4.8's pricing ($5/$25 per MTok) with the same feature set. Thinking is on by default (omitting `thinking` runs adaptive; `{type: "adaptive"}` is equivalent), and `thinking: {type: "disabled"}` is available only at effort `high` or lower - pairing it with `xhigh`/`max` returns a 400. Raw thinking tokens are never returned. Full effort ladder through `max`; 512-token prompt-cache minimum (down from 1024 on Opus 4.8); fast mode on the Claude API only. Elevated cybersecurity safeguards. Separate rate-limit bucket from the combined Opus 4.x pool. 1M context window (default and maximum), 128K max output. See `shared/model-migration.md` -> Migrating to Claude Opus 5.
- **Claude Opus 4.8** - The most capable model in the Opus 4 series - highly autonomous, state-of-the-art on long-horizon agentic work, knowledge work, and memory; clearer, warmer writing. Same API surface as Opus 4.7 (adaptive thinking only; sampling parameters and `budget_tokens` removed). 1M context window at standard API pricing (no long-context premium). See `shared/model-migration.md` -> Migrating to Opus 4.8 - a 4.7 -> 4.8 move is a model-ID swap plus prompt re-tuning, no new breaking changes.
- **Claude Opus 4.7** - Previous-generation Opus. Highly autonomous; strong on long-horizon agentic work, knowledge work, vision, and memory. Adaptive thinking only; sampling parameters and `budget_tokens` removed. 1M context window. See `shared/model-migration.md` -> Migrating to Opus 4.7.
- **Claude Opus 4.6** - Older Opus. Supports adaptive thinking (recommended), 128K max output tokens (requires streaming for large outputs). 1M context window.
- **Claude Sonnet 5** - The best combination of speed and intelligence in the Sonnet tier; near-Opus quality on coding and agentic work. Adaptive thinking on by default (omitting `thinking` runs adaptive); manual `budget_tokens` removed; non-default sampling parameters rejected. `effort` supports `low`/`medium`/`high`/`xhigh`/`max`. New tokenizer (~30% more tokens for the same text vs Sonnet 4.6). High-resolution vision (2576px). 1M context window, 128K max output. See `shared/model-migration.md` -> Migrating to Claude Sonnet 5.
- **Claude Sonnet 4.6** - Previous-generation Sonnet. Supports adaptive thinking (recommended). 1M context window. 128K max output tokens.
- **Claude Haiku 4.5** - Fastest and most cost-effective model for simple tasks.
## Legacy Models (still active)
| Friendly Name | Alias (use this) | Full ID | Status |
|-------------------|---------------------|-------------------------------|--------|
| Claude Opus 4.5 | `claude-opus-4-5` | `claude-opus-4-5-20251101` | Active |
| Claude Opus 4.1 | `claude-opus-4-1` | `claude-opus-4-1-20250805` | Deprecated (retires 2026-08-05 - migrate to `claude-opus-5`) |
| Claude Sonnet 4.5 | `claude-sonnet-4-5` | `claude-sonnet-4-5-20250929` | Active |
## Deprecated Models (retiring soon)
| Friendly Name | Alias (use this) | Full ID | Status | Retires |
|-------------------|---------------------|-------------------------------|------------|--------------|
| Claude Sonnet 4 | `claude-sonnet-4-0` | `claude-sonnet-4-20250514` | Deprecated | TBD |
| Claude Opus 4 | `claude-opus-4-0` | `claude-opus-4-20250514` | Deprecated | TBD |
| Claude Haiku 3 | - | `claude-3-haiku-20240307` | Deprecated | Apr 19, 2026 |
## Retired Models (no longer available)
| Friendly Name | Full ID | Retired |
|-------------------|-------------------------------|-------------|
| Claude Sonnet 3.7 | `claude-3-7-sonnet-20250219` | Feb 19, 2026 |
| Claude Haiku 3.5 | `claude-3-5-haiku-20241022` | Feb 19, 2026 |
| Claude Opus 3 | `claude-3-opus-20240229` | Jan 5, 2026 |
| Claude Sonnet 3.5 | `claude-3-5-sonnet-20241022` | Oct 28, 2025 |
| Claude Sonnet 3.5 | `claude-3-5-sonnet-20240620` | Oct 28, 2025 |
| Claude Sonnet 3 | `claude-3-sonnet-20240229` | Jul 21, 2025 |
| Claude 2.1 | `claude-2.1` | Jul 21, 2025 |
| Claude 2.0 | `claude-2.0` | Jul 21, 2025 |
## Resolving User Requests
When a user asks for a model by name, use this table to find the correct model ID:
| User says... | Use this model ID |
|-------------------------------------------|--------------------------------|
| "fable", "most capable model" | `claude-fable-5-1` |
| "most powerful" | `claude-fable-5-1` |
| "mythos", "mythos 5.1" | `claude-mythos-5-1` (Project Glasswing participants only; otherwise use `claude-fable-5-1`) |
| "fable 5", "mythos 5" (previous version) | `claude-fable-5` / `claude-mythos-5` (still served; prefer `claude-fable-5-1` for new work) |
| "mythos preview" | `claude-mythos-5-1` (successor to `claude-mythos-preview` - see migration guide) |
| "opus" | `claude-opus-5` |
| "opus 5" | `claude-opus-5` |
| "opus 4.8" | `claude-opus-4-8` |
| "opus 4.7" | `claude-opus-4-7` |
| "opus 4.6" | `claude-opus-4-6` |
| "opus 4.5" | `claude-opus-4-5` |
| "opus 4.1" | `claude-opus-4-1` (deprecated, retires 2026-08-05 - suggest `claude-opus-5`) |
| "opus 4", "opus 4.0" | `claude-opus-4-0` (deprecated - suggest `claude-opus-5`) |
| "sonnet", "balanced" | `claude-sonnet-5` |
| "sonnet 5" | `claude-sonnet-5` |
| "sonnet 4.6" | `claude-sonnet-4-6` |
| "sonnet 4.5" | `claude-sonnet-4-5` |
| "sonnet 4", "sonnet 4.0" | `claude-sonnet-4-0` (deprecated - suggest `claude-sonnet-5`) |
| "sonnet 3.7" | Retired - suggest `claude-sonnet-5` |
| "sonnet 3.5" | Retired - suggest `claude-sonnet-5` |
| "haiku", "fast", "cheap" | `claude-haiku-4-5` |
| "haiku 4.5" | `claude-haiku-4-5` |
| "haiku 3.5" | Retired - suggest `claude-haiku-4-5` |
| "haiku 3" | Deprecated - suggest `claude-haiku-4-5` |
# Platform Availability
Which features work on which provider platform. **This table is the single source of truth in this skill** - per-feature sections elsewhere point here instead of restating availability. When writing code for a third-party platform (Bedrock, Vertex, Foundry) or Claude Platform on AWS, check this table first; a feature not supported there means use the first-party Claude API surface or a different approach.
Columns: **1P** = first-party Claude API, **P-AWS** = Claude Platform on AWS (Anthropic-operated, same-day parity), **Bedrock** = Amazon Bedrock, **Vertex** = Google Cloud Vertex AI, **Foundry** = Microsoft Foundry. Yes = GA, beta = beta, No = not supported.
| Feature | 1P | P-AWS | Bedrock | Vertex | Foundry | Notes |
|---|---|---|---|---|---|---|
| Messages, streaming, tool use | Yes | Yes | Yes | Yes | Yes | Core API |
| PDF input | Yes | Yes | Yes | Yes | beta | |
| Structured outputs / strict tool use | Yes | Yes | Yes | Yes | beta | |
| Adaptive thinking / effort | Yes | Yes | Yes | Yes | beta | |
| Extended thinking | Yes | Yes | Yes | Yes | beta | |
| Prompt caching (5m, 1h) | Yes | Yes | Yes | Yes | Yes | |
| Automatic prompt caching | Yes | Yes | Yes | Yes | Yes | The legacy Bedrock integration (Opus 4.6 and earlier) rejects top-level `cache_control` with a 400 - explicit breakpoints only there |
| Token counting | Yes | Yes | Yes | Yes | beta | |
| Citations | Yes | Yes | Yes | Yes | beta | |
| Search results content blocks | Yes | Yes | Yes | Yes | beta | |
| Fine-grained tool streaming | Yes | Yes | Yes | Yes | Yes | |
| Compaction | beta | beta | beta | beta | beta | |
| Context editing | beta | beta | beta | beta | beta | |
| Context windows (1M) | Yes | Yes | Yes | Yes | beta | |
| `inference_geo` (data residency) | Yes | Yes | No | No | No | |
| **Server-side tools** | | | | | | |
| Web search | Yes | Yes | No | Yes | beta | Vertex: basic `web_search_20250305` only (no `_20260209` dynamic filtering) |
| Web fetch | Yes | Yes | No | No | beta | |
| Code execution | Yes | Yes | No | No | beta | |
| Tool search | Yes | Yes | Yes | Yes | beta | Bedrock: InvokeModel API only, not Converse |
| Advisor tool | beta | beta | No | No | No | |
| **Client-implemented tools** | | | | | | |
| Bash, text editor, memory | Yes | Yes | Yes | Yes | beta | |
| Computer use | beta | beta | beta | beta | beta | |
| **Agentic / orchestration** | | | | | | |
| Agent Skills (Messages API) | Yes | Yes | No | No | beta | |
| Programmatic tool calling | Yes | Yes | No | No | beta | |
| MCP connector | beta | beta | No | No | beta | |
| Managed Agents | beta | beta | No | No | No | Foundry: No (inferred; not in Foundry docs either way) |
| Self-hosted sandboxes | beta | beta | No | No | No | P-AWS: worker authenticates with IAM/SigV4 or an AWS-Console API key + `AnthropicSelfHostedEnvironmentAccess` (Console environment keys don't work there); sessions on self-hosted environments cannot attach memory stores; `GET /v1/environments/{id}/work` list endpoint not supported, other work endpoints OK |
| **API endpoints** | | | | | | |
| Message Batches | Yes | Yes | No | No | No | |
| Files API | Yes | Yes | No | No | beta | |
| Models API | Yes | Yes | No | No | No | |
| **Other** | | | | | | |
| Mid-conversation system messages | Yes | Yes | Yes | Yes | No | Claude Opus 5, Claude Opus 4.8, Claude Fable 5, Claude Fable 5.1, Claude Mythos 5, Claude Mythos 5.1; not Claude Sonnet 5. Bedrock: InvokeModel passthrough, not ARN-versioned models |
| Turn-scoped (`clear_at`) system messages | beta | beta | beta | beta | No | Same models as mid-conversation system messages; beta `mid-conversation-system-clear-at-2026-08-21` (on Bedrock/Vertex pass the value as a beta) |
| Per-message `effort` (system message `output_config`) | beta | No | No | No | No | Claude Fable 5.1, Claude Mythos 5.1, Claude Opus 5; beta `mid-conversation-output-config-2026-07-01`; Claude API at launch (Bedrock/Vertex/Foundry unconfirmed; Claude Opus 5 excluded on Bedrock) |
| `thinking.display: "updates"` | beta | beta | beta | beta | beta | Claude Fable 5.1, Claude Mythos 5.1, Claude Fable 5; beta `thinking-display-updates-2026-08-18` (pass the beta value per platform); without it `"updates"` is rejected as an unknown `display` value |
| Thinking block-binding controls | beta | beta | per model | per model | No | `thinking.block_binding` + `input_transformations`; beta `thinking-binding-controls-2026-08-01` (on Bedrock via the `anthropic_beta` body field); the controls beta arrives per model on Bedrock/Vertex - until then the header is rejected; the history-editing enforcement itself follows the account-age rule in `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 |
| Server-side `fallbacks` | beta | beta | No | No | No | `"default"` -> beta `server-side-fallback-2026-07-01`; array form -> beta `server-side-fallback-2026-06-01` |
| Fast mode | beta | No | No | No | No | Research preview, beta `fast-mode-2026-02-01`, first-party API only |
| Cache diagnostics | beta | No | No | No | No | First-party API only |
| Task budgets | beta | beta | No | No | No | Beta header `task-budgets-2026-03-13`; 3P availability not documented - assume unsupported |
# Prompt Audit - Finding and Removing Dated Prompting Patterns
> **If you arrived via `/claude-api prompt-audit`:** this is the right file. Execute the steps below in order - do not summarize them back to the user. Start with Step 0 (establish scope and target model), and finish by producing both deliverables: the audit report (Step 5) and the proposed diff (Step 6).
Prompts, skills, and tool descriptions accumulate instructions tuned to older models: emphasis added because an old model under-triggered, step-by-step scripts added because an old model planned poorly, format scaffolds written before the API had structured outputs. Current Claude models follow instructions more closely and more literally than the models much of this text was written for, so the leftover text is not just wasted tokens - specific outdated instructions actively degrade behavior (over-triggering, over-planning, rigid responses in gray areas), while merely irrelevant text is comparatively harmless. The audit's job is therefore to find **specific dated instructions**, not to make prompts shorter. "Every token earns its place" is the frame; "make it short" is not.
**The audit produces two artifacts - both, always:**
1. **An audit report**: every finding with its location (`file:line`), the pattern it matches, why it is obsolete for the target model, and a confidence level.
2. **A proposed diff**: concrete edits for the findings that warrant them. Propose - never apply edits without the user's consent.
**Prime directive: distinguish cruft from load-bearing content.** A finding you cannot tie to a named pattern below, with a reason grounded in the target model's documented behavior, is not a finding. When in doubt, flag it in the report with low confidence and leave it out of the diff. Indiscriminate deletion is the one way an audit makes things worse - see "What not to flag" below, which is as binding as the pattern tables. The inverse binds too: **an audit that finds nothing should change nothing** - a clean surface is a valid outcome, and an empty diff beats a manufactured one.
---
## Step 0: Establish scope and target model
**Before reading any file, establish two things - from the request and the repository, not by asking.** This audit is non-interactive by design: it runs the same way in a chat session, a CI job, or a batch migration, so it states its assumptions and proceeds instead of pausing for confirmation. Both assumptions go at the top of the report (Step 5), where the user can correct them by re-running with a narrower request.
1. **Scope.** Which files count as the prompt surface? If the user's request names a file, directory, or file list, that is the scope. Otherwise the scope is the whole working directory's prompt surface - everything Step 1's inventory finds.
2. **Target model.** Cruft is relative to a model: a workaround that is load-bearing on one generation is dead weight on the next. Resolve the target in this order: the model the request names; else the destination of an in-progress migration the repository documents (vendor notes, migration docs, TODOs); else the newest model the repository's own code or docs point at; else the current flagship generation of the provider the code calls. If the audit is part of a migration, read `shared/model-migration.md` -> the per-target section alongside this file, since every migration section's checklist is also a removal checklist.
## Step 1: Inventory the prompt surface
Find everything that reaches the model as text, not just the file named "prompt":
- **System prompts** and the code that assembles them (f-strings, template files, conditional sections)
- **Tool definitions** - `description` fields and parameter descriptions in the `tools` array
- **Skill and rule files** - `SKILL.md`, `CLAUDE.md`, `.cursorrules`-style rule files, agent instruction files
- **Request-building code** - model IDs, `thinking` configuration, sampling parameters, stop sequences, prefill construction, retry logic, beta headers
- **Few-shot blocks and embedded examples**, wherever they live
List what you found before auditing it, so the user can correct the inventory.
## Step 2: Establish provenance
Where git history is available, `git blame` the prompt files. The question for every emphatic or prohibitive line is: **which failure, on which model, did this prevent - and does that failure still reproduce on the target model?** Lines added as mitigations for a model that is no longer in use are presumptive removal candidates; a line nobody can justify is suspect by default.
Prompts can also be dated by their idioms even without history. `<scratchpad>` / `<brainstorm>` tag instructions, "think step by step", assistant-turn prefills, quotes-first extraction scaffolds, and ROLE -> CONTEXT -> RULES -> EXAMPLES boilerplate all mark text written for much earlier Claude generations - techniques that are now natively trained (thinking, calibrated refusals) or superseded by API features (structured outputs). Idiom-dating alone is a flag-only signal (low confidence in the Step 5 rubric); it earns medium or high only when paired with a reason grounded in the target model's documented behavior - a blame line tying the text to a retired model's era is the strongest form of that pairing.
## Step 3: Classify every line - the deletion rule
For each instruction, ask one question: **could the model already know this?**
- **Keep what only the author knows**: the audience and product, environment facts, the quality bar, tool contracts and mechanics, genuinely hard judgment calls, and the *reasons* behind constraints. This is context, and context is never cruft.
- **Candidates for removal**: restatements of trained defaults ("be accurate and helpful"), behavior the model already does unprompted (thoroughness, planning, tool use), and workarounds for failures the target model no longer has.
A second distinction sharpens the first: is the line a **constraint on behavior** (deletion candidate - test it) or **context the model can't get elsewhere** (usually keep)? This check prevents the audit from becoming a length contest: a naive shortening pass deletes exactly the highest-value words.
## Step 4: Scan for the anti-pattern groups
Work through the four groups. "Signals" rows are greppable - run them over the inventory rather than eyeballing.
### Group 1 - Dated prompt text
#### 1a. Pressure language - say exactly what you mean, at normal volume
Older, less steerable models genuinely needed forcefulness; current models are highly responsive to the system prompt, so the same text over-applies. This cuts in **both directions**: inflated emphasis causes over-triggering and rigid behavior, while leftover hedges ("try to", "if possible") are now read literally as permission to under-deliver.
| Before (written for older models) | After (current models) |
|---|---|
| `CRITICAL: You MUST use this tool when...` | `Use this tool when...` |
| `IMPORTANT: NEVER do X` (several per prompt) | State the one or two real constraints plainly, with the reason |
| `If in doubt, use [tool]` / `Default to [tool]` | *(delete, or)* `Use [tool] when it would improve X` |
| `Be thorough. Do not be lazy. Do not stop early.` | *(delete - current models are proactive by default)* |
| `Try to include a summary if possible` (when it's required) | `Include a summary.` |
| `You have a tendency to over-X, so...` / `Don't be too verbose` | State the desired behavior: `Keep responses to the length the question needs.` |
When several instructions are each marked critical, the markers stop carrying information - and the prompt's register becomes the output's register: an anxious prompt produces a cautious, hedging model. Emphasis is not banned; it is a tested, scoped fix for one demonstrably underweighted instruction, not a first-draft register.
**Signals:** density of `MUST|NEVER|ALWAYS|CRITICAL|IMPORTANT` in caps; `!!`; emphasis with no adjacent "because"; `try to|if possible|ideally` attached to actual requirements; `you (tend to|often|sometimes)` trait claims; `don't be too [adjective]`.
#### 1b. Scaffolds replaced by API features - replace, don't rewrite
These aren't tuned down; they're swapped for the feature that replaced them. For per-model specifics (what errors on which model, exact syntax), read `shared/model-migration.md`.
| Scaffold in the prompt or request code | Replacement |
|---|---|
| "Think step by step", `<scratchpad>`/`<thinking>` tag instructions | Adaptive thinking (`thinking: {type: "adaptive"}`) + `effort`. On thinking models the incantation is redundant at best; control depth via configuration, not prose. |
| "Use the think tool to plan" / "plan before acting" | Delete - current models plan without being told, and these cause over-planning. If behavior is still too aggressive after cleanup, lower `effort` rather than adding prose. |
| "Show your thinking" / required reasoning sections in the output | Read thinking blocks via the API. On Claude Fable 5.1, instructing reasoning reproduction can trigger a `refusal` (reasoning extraction) - this is an explicit audit item when migrating. |
| Assistant-turn prefill (`{"role": "assistant", "content": "{"`) and the JSON-forcing stack around it: stop-sequences, regex extraction, retry-on-parse loops, "output ONLY valid JSON" | Structured outputs (`output_config.format`). Prefill 400s on 4.6-and-later Opus- and Sonnet-tier models and Claude Fable 5.1 - confirm in the per-target section of `shared/model-migration.md` before claiming the error. Where it applies, the *surrounding code* is cruft too - audit the request builder, not just the prompt string. Only a **trailing** assistant turn is a prefill - partial or complete-looking (a few-shot block ending on the assistant side still counts): assistant turns mid-array are ordinary conversation history and must stay. |
| "Summarize progress every N tool calls" choreography; hard word caps (`at most N words`) | Delete and re-baseline: current models narrate appropriately, and output caps starve reasoning on hard problems. Prefer qualitative length guidance ("be concise") over numeric caps tuned against an older model's verbosity. |
| Inline lookup tables, point systems, arithmetic rubrics the model must compute | Data in files or tool results; arithmetic in code. Leave the model the judgment layer. |
| `budget_tokens`, non-default `temperature`/`top_p`/`top_k`, stale beta headers, dead 400-retry paths | See `shared/model-migration.md` - whether each one hard-errors or is merely deprecated depends on the target model, so take the error claim from the per-target section there, not from memory. Where it does error, the retry/workaround code around it is removable too. |
| Forced tool use - `tool_choice: {type: "any"}` / `{type: "tool", name: ...}` - and the JSON-via-forced-tool pattern | Prompt instruction naming the tool under `tool_choice: auto` (steering), or structured outputs (extraction). Returns a 400 on Claude Fable 5.1 / Claude Mythos 5.1 (and Mythos Preview); elsewhere it works but is usually a prompt-instruction in disguise - `strict: true` keeps the schema guarantee under `auto`. Audit the retry-on-missing-tool loop around it as well. |
**Signals:** `think step by step|take a deep breath`; `<scratchpad>|<thinking>` in instructions; `stop_sequences` guarding JSON; `json.loads` inside retry loops; `budget_tokens|temperature|top_p` in request code; `every \d+ (tool calls|messages)`; `at most \d+ (words|sentences)`.
#### 1c. Over-specification - describe the goal, not the method
| Pattern | Why it's cruft now | Fix |
|---|---|---|
| Step-by-step choreography for judgment tasks (`STEP 1: ... STEP 2: ...`) | Skills and prompts written for prior models are often too prescriptive for current ones and degrade output quality - the model's own plan usually beats a hand-written script | State outcomes, constraints, and how to verify; keep numbered steps only where order truly matters |
| Prohibition lists ("do not X, never Y, avoid Z...") | Describing success beats enumerating failure; a prohibition against a failure the model wasn't going to make can *anchor it toward* that failure | Keep prohibitions whose failure reproduces on the target model; rewrite the rest as positive statements of intent |
| Example over-indexing: the single gold output; stale few-shot blocks | Concrete examples are the strongest signal in a prompt - the model matches their length, tone, and structure, and examples written for an older model freeze that model's behavior into the new one | Several deliberately varied examples, labeled illustrative; delete examples of judgment the model already owns; keep examples that pin a genuinely format-sensitive output shape |
| Bullet walls and heavy formatting for behavioral guidance | Bullets flatten priority and sever rules from reasons, and prompt format bleeds into output format | Structure for reference data; prose for behavior, carrying the "because" |
| Padding: generic virtues ("be accurate, thorough, clear"), repetition as reinforcement, kitchen-sink edge cases, limits with escape hatches | The model treats everything as actionable signal; asides get applied where they don't fit; duplicated rules make the model spend effort reconciling wordings; bulk also directly inflates adaptive-thinking spend | Say it once, in the right place; cover the hard judgment calls instead of the easy parts |
| Grader and eval vocabulary ("you will be graded on...", "hidden tests") | Describes the scoring apparatus instead of the requirement and pushes effort toward being-watched | State every requirement the grader checks; never describe the grader |
| Strategy coaching next to task rules ("it's usually best to...") | The author's heuristics are wrong in some situations and the model's plan is usually better | If removing the sentence wouldn't change what is legal or how success is measured, it's strategy - delete it |
**Signals:** `STEP \d`/numbered imperatives for non-fragile work; runs of 3+ `Do not|Never|Avoid` lines; `do not hallucinate` (re-test whether you still need it - removal here is low confidence, not a documented harm); single embedded gold outputs; near-duplicate sentences across sections; `Remember,|Again,|As stated above`; `grade|graded|rubric|hidden test`.
#### 1d. Fossils - text that outlived its model
| Pattern | Why it's cruft now | Fix |
|---|---|---|
| Model-version workarounds: formatting fixes, over-refusal softeners, retry hints, "known issue with [model]" comments, date-conditional guidance | Nobody owns the removal, so prompts accumulate the union of every generation's mitigations | Each mitigation names (or gets traced to) the model it patched; if that model is retired, remove and re-test |
| Migration-relative phrasing: "X now works differently", "also counts", "no longer" | The text is a diff against a previous prompt version the model never saw; relative phrasing implies phantom alternatives | Write as if current rules are the only rules that ever existed |
| Patch accretion: many narrow conditionals, each traceable to one incident | The model navigates a maze of special cases instead of a coherent principle, and fails unpredictably between them; an eval win for adding a line on top of the stack is not evidence the stack should exist | Generalize the principle or fix the underlying context; test removals, not just additions |
| Unenforced instructions: rules no code path, eval, or reviewer checks - visibly violated in the app's own transcripts | If nothing checks it and nobody noticed, it carries no signal - and behavioral rules that could be hooks, allowlists, or schema validators are less reliable as prose | Enforce in code what can be enforced in code; delete what nothing enforces and nobody misses |
| Identity stubs standing in for context ("You are a helpful assistant") | A role line is fine as a one-sentence focus-setter; the defect is an identity statement *substituting* for audience, product, and quality bar | Don't flag a short role line; flag when it's the only context the prompt gives |
| Update suppressors written for chatty models: "hold all findings for the final response", "don't narrate", "no interim updates" | Tuned against models that over-narrated; current models (Claude Fable 5.1 especially) under-narrate with these present, and the harness may not be requesting the model's between-tool progress notes at all (`thinking.display: "updates"`) | Remove first and re-test; if more narration is still wanted, replace with a specific line saying *when* user-facing text is wanted (see `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 -> User-facing progress updates) |
| Anti-formatting rules: "never use bullets", "no headers", "no bold" | Written against models that over-formatted; Claude Fable 5.1 already under-formats, so the rule now strips formatting the reader wanted | Remove, or replace with a rule that says when formatting is appropriate (the conditional-formatting snippet in the Claude Fable 5.1 migration section) |
| Instruction re-insertion every few turns ("reminder: ..." repeated on a cadence in the harness) | A retention crutch for models that lost instructions over long sessions; current models retain a once-stated instruction, and each repeat costs tokens and, under preserved thinking's history-editing check, is a history edit if it is later removed | Remove the repetition and re-test; where a genuinely per-turn reminder remains, send it as a turn-scoped (`clear_at`) system message - or a text block after the tool results - and never delete earlier copies |
**Signals:** retired model names in prompts or comments (`claude-2|claude-3|claude-instant|3\.5|3\.7`); `hold (all )?(findings|results)|don't narrate|no interim`; `never use (bullets|headers|bold)|no (bullet|header)`; `reminder:` on a turn cadence; `before|after [date]` conditionals; `now|no longer|instead of` attached to behavioral rules; rules whose reason nobody remembers; `^You are (a|an) (helpful|expert)` with nothing task-specific following.
#### 1e. Prohibition clusters - judge by provenance, not by whether the model "needs it"
A run of unconditional "never / don't / must not" lines is audited by asking, for each, **does it carry a stated reason or encode a real business/policy constraint?** - not "does the target model still need this guardrail?" (the latter question keeps everything, because nothing is *harmful* to say). Prohibitions that encode observable constraints (refund caps, data rules, compliance language, promises the business must not make) stay, ideally with their reason beside them. Prohibitions that merely describe an undesirable *output style* with no provenance - banned phrases, tic lists, "don't start with 'Certainly'" written against an older model's habits - are cruft: restate the desired style positively in one line, or attach the real reason if there is one. A surrounding cluster of legitimate reasoned prohibitions does not launder the no-provenance ones mixed into it; classify each line separately.
#### 1f. Output-shaping choreography - one pattern, remove every limb
Fixed interim-update cadences ("after every third tool call, post a progress note"), numeric output ceilings ("under 120 words", "at most five bullets"), and cut-the-detail instructions are manifestations of the **same** over-constraint pattern, written for models that padded or rambled. They are removed *together*: a stated operational reason ("queue throughput", "supervisors skim") does not convert a numeric clamp into a keeper - re-express the goal as audience/outcome framing without the number ("replies are scan-able and answer only what was asked"), and keep any genuinely format-sensitive requirement as a format instruction, not a word count. Removing the cadence while keeping the ceilings leaves the pattern in place.
### Group 2 - Brittle skill files
Skill files (`SKILL.md`, `CLAUDE.md`, rule files) inherit everything in Group 1, plus failure modes of their own. Skill size is a tax paid on every trigger.
| Pattern | Why it's cruft now | Fix |
|---|---|---|
| Verbose SKILL.md explaining things the model already knows | Every paragraph must justify its token cost; general programming knowledge doesn't | Apply the Step 3 deletion rule paragraph by paragraph |
| Wrong degrees of freedom | Exact scripts for judgment calls over-constrain; vague prose for fragile operations under-constrains | Match specificity to fragility: prose heuristics for open fields, exact commands (`do not modify this command`) only for narrow bridges |
| The recency trap: one session's stumble encoded as a permanent rule | The next session steps around a pothole that isn't there | Before keeping a rule, ask: would this have helped most recent sessions, or just the one that wrote it? |
| Volatile specifics: hardcoded paths, flags, version numbers, API claims with no verification date | Skills rot factually as code ships; nothing re-checks them by default | Encode architecture, data models, and workflows; verify surviving factual claims against current code as part of the audit |
| Time-sensitive content ("if before [date]...", option menus, duplicated info across SKILL.md and reference files) | Dates rot; menus of alternatives dilute; duplicates drift apart | An "old patterns" section instead of dates; one default plus an escape hatch; information lives in exactly one place |
| History narratives: past tense, incident IDs, PR numbers, pinned model names | A rule's authority is the behavior it prescribes, not the incident that motivated it; pinned model names silently degrade after the next release | State the current rule; drop the archaeology |
| Trigger-case enumeration: description lists of near-synonymous example queries, growing one phrase per missed trigger | Descriptions ride in every request; enumeration taxes every token budget and generalizes worse than intent categories | Name generalized categories of intent; see Group 3 for the trigger/behavior split |
**Signals:** `SKILL.md` not readable in one sitting; hardcoded paths and version pins; past tense in instruction files; descriptions that only ever grow in git history.
### Group 3 - Tool descriptions
**The rubric for tool descriptions is precision and contract accuracy, not brevity** - this is where a "trim it" instinct most often points the wrong way. Detailed descriptions are by far the most important factor in tool performance, and the most common failure is *under*-description. What changed on current models is *which content* belongs there: contract and mechanics in, behavioral steering and worked examples out. A tool description is a man page - what the tool does, when to use it (and when not to), what each parameter means, caveats, what it does not return.
| Pattern | Direction | Fix |
|---|---|---|
| Vague one-liners; parameters without descriptions; no when-not-to-use | **Under-described - add** | 3-4+ sentences minimum; description must precisely match actual behavior (a contract/behavior mismatch sends the model down paths no prompt text can fix) |
| `CRITICAL: You MUST use this tool when...` | Over-steered - dial back | Plain `Use this tool when...` - triggering boosters written against under-triggering models now cause over-triggering |
| Worked examples, fake dialogue turns, embedded protocols (numbered workflows, HEREDOCs) in the description - in any quantity, even ones that "measurably lift the call rate" | Misplaced - move | Examples constrain the exploration space and cost tokens on every request; move teaching material to skills/progressive disclosure; make parameters expressive (well-named enums carry intent) |
| Scolding cross-references (`ALWAYS use X, NEVER use Y for this`) and behavior-smuggling ("after showing results, always recommend...") | Misplaced - move or delete | A description is a contract about functionality, not a channel for conversational instructions; put a preference for tool X in X's description, not scattered across its rivals |
| Tool names in the system prompt; prose lists that shadow the real tool list | Duplicated - delete | The system prompt shouldn't name tools; then enabling or disabling one never leaves a dangling reference. Don't expose tools that are invalid in the current configuration |
| Near-duplicate overlapping tools; bloated response payloads; full catalogs of 30+ always-loaded tools | Structural | Fewer, clearly bounded tools with explicit boundaries in both descriptions; high-signal responses; past a few dozen tools use tool search / deferred loading instead of always-loading every schema |
**One deliberate split: trigger text is not behavioral text.** Text whose job is routing - a skill's frontmatter `description`, a trigger block - may legitimately carry calibrated urgency, because skills currently under-trigger; ideally it's tuned against a trigger eval rather than vibes. Text whose job is behavior should explain rather than shout. These look identical to a grep, so classify by function before flagging.
**Signals:** descriptions under ~3 sentences (add); `MUST|ALWAYS|NEVER` steering behavior inside descriptions (dial back); fake dialogue or worked examples in descriptions (move); tool names in system-prompt prose (delete).
### Group 4 - Request config and architecture
The same audit keeps surfacing these next to prompt cruft; report them even though they're not prompt text.
- **API fossils**: parameters and headers that error or are deprecated on the target model - the per-model lists live in `shared/model-migration.md`; treat each migration checklist as a removal checklist.
- **Cache-hostile ordering**: timestamps, UUIDs, per-user content interpolated above stable content. Read `shared/prompt-caching.md` -> Silent invalidators, and run its greps during this audit.
- **Budget countdowns rendered into context**: surfacing remaining-token counts to the model can cause premature wrap-up behavior; avoid showing them where possible.
- **An LLM executor for a deterministic plan**: agent sessions whose transcript is the same loop body N times; calls whose inputs fully determine outputs. **Run this check, don't wait to notice it**: in every pipeline, batch job, or agent loop, *count the model-call sites* and ask of each whether its inputs fully determine its output. Routing, tallying, normalizing, filtering, and formatting steps go back into plain code; keep exactly one model call where the work is genuinely adaptive (classifying the ambiguous remainder, writing the judgment summary). Zero model calls is an over-fix when a judgment step exists - name the one call that stays.
- **Redundant specialist sub-agents**: inspect the sub-agent roster / agent config as a surface in its own right. Two agents doing the same task with the same tools and near-duplicate prompts, differing only in a filter or a payload field, are one agent that should take the distinction as input. The fix is a concrete roster edit - delete the redundant definition and fold its one real difference into the surviving agent's prompt or payload - proposed as a diff like any other finding, not left as an advisory note.
- **No token accounting**: without per-surface cost visibility, every other issue here is invisible. If the user has no accounting, recommend adding it first - it's the prerequisite for measuring any cleanup.
---
## What not to flag - the keep list
An audit that only says "delete" hurts the users who follow it most diligently. These stay, even when a grep matches:
1. **Context is never cruft.** Audience, product, environment facts, quality bar, constraints, and the *reasons* for them - what only the author knows. Too-short prompts produce generic output because the model fills gaps with safe defaults; give the model more context than seems necessary, not less.
2. **Cruft != length.** The harm comes from specific outdated instructions, not from volume. Never justify a deletion by character count alone.
3. **Fragile operations keep exact scripts.** Low-freedom, prescriptive text is correct where exactly one sequence is safe (destructive commands, auth flows, compliance steps). Prompting effort should scale with how far the task is from what the model does naturally.
4. **Tool contract detail stays - and often grows.** Parameter semantics, limits, failure modes, what the tool does not return. The audit removes steering and examples from descriptions, not contract.
5. **Prohibitions against current, demonstrated failures stay.** The discriminator is whether the failure reproduces on the target model in this context - not whether the sentence pattern-matches "prohibition".
6. **Trigger/routing text may carry calibrated urgency** (see Group 3). Flag shouting in bodies, not load-bearing trigger text.
7. **Format-pinning examples on genuinely format-sensitive outputs stay**, labeled illustrative.
8. **Working redundancy is not cruft.** Duplicated or overlapping content that is *functioning* - the same contract stated in two files, a worked example the prompt could in principle do without, content you would merely organize differently - is a refactoring preference, not a dated pattern. If it isn't causing errors and the target model reconciles it, an audit leaves it alone; propose deduplication or consolidation only when the duplicates actually disagree. "An audit that finds nothing should change nothing" extends to this: on a clean surface, report that it is clean.
9. **A one-line role statement is fine.** Flag identity text only when it substitutes for real context.
10. **Deliberate recap is not padding.** A single end-of-prompt restatement of the few key constraints is a known, reasonable pattern; the anti-pattern is scattered duplication.
11. **Re-baselining adds text too.** Matching a prompt to a new model sometimes means *adding* guidance for the new model's failure modes (see the per-target "Behavioral shifts" sections in `shared/model-migration.md`). The audit's job is fit, in both directions.
---
## Step 5: Produce the audit report
One entry per finding, in this shape:
| Field | Content |
|---|---|
| **Location** | `file:line` (or `file:line-range`) |
| **Evidence** | The exact text, quoted |
| **Pattern** | The group/row above it matches |
| **Why obsolete** | One or two sentences tying it to the target model's documented behavior ("current models are proactive by default; this booster now causes over-triggering") |
| **Confidence** | **High** - documented in current Claude docs or errors on the target model. **Medium** - consistent, widely-observed behavior (e.g. example over-indexing). **Low** - heuristic or idiom-dating; flag, don't edit. |
| **Action** | `remove` / `rewrite` (give the replacement) / `move` (say where) / `replace-with-API-feature` / `add` (under-description - the fix is *more* text; give it) / `flag` (no edit proposed) |
Order the report by confidence, highest first. Summarize at the top: counts per group, and the two or three highest-impact findings in prose. Findings you cannot tie to a pattern and a target-model reason go at the bottom as `flag` items or not at all.
**The flag-versus-fix threshold.** A finding that matches a documented row in the groups above *is* a high- or medium-confidence finding, and it gets a concrete proposed action - `remove`, `rewrite` (with the replacement text), `move`, or `add`. `flag` is reserved for two things only: low-confidence idiom-dating that no row documents, and items outside the audit's scope. Do not downgrade a documented-pattern match to `flag` because it "seems minor," "reads as a soft nudge," "is a product judgment," or "measurably helps" - those are reasons the user may *decline* your proposed fix, not reasons to withhold it. An audit that correctly identifies the pattern and then proposes nothing has done half the job; the user can always reject a hunk they disagree with, but they cannot accept a fix you never wrote.
## Step 6: Produce the proposed diff
- Include only findings with action `remove`/`rewrite`/`move`/`replace-with-API-feature`/`add` at **high or medium confidence**. `flag` and low-confidence items appear in the report only.
- One finding per hunk, so effects attribute and the user can take hunks selectively.
- Rewrites beat bare deletions where the instruction has a live purpose: re-express it simply ("look before you delete") rather than keeping the verbose original or dropping the concern.
- A removal is complete only when everything referencing it goes too: tests asserting the old behavior, call sites and helper functions, docs, and every model-ID pin (READMEs and rule files included). Grep the project for the removed symbols and the old model ID before calling the diff done - a prompt fixed while its smoke test still asserts the old behavior is a broken app, not an audit win.
- For request-construction patterns (assistant-turn prefill, stop-sequence scaffolding, sampling-parameter fossils), the diff must *eliminate the capability* on every code path - after the fix, no path through the request builder can still emit the dated shape (e.g. no reachable branch yields a trailing assistant turn) - not merely rewire its current consumer. Include every call site of the changed function and the parser/retry helpers that existed only to serve the old mechanism, and rewrite the tests that assert the old request shape.
- The report and the proposed diff are the deliverables - produce both in full and stop there. Do not pause mid-audit to ask whether to continue, and do not end by asking whether to apply: present the diff and let the user take hunks on their own schedule. Apply edits to files only when the request itself explicitly asked for the changes to be applied (e.g. "clean it up", "remove the cruft"), and even then keep `flag`/low-confidence items out of the applied set.
## Step 7: Verify - removal is a hypothesis, not a conclusion
- **Probe behavior, not self-report.** For each contested change, run a small behavioral check before and after on a scratch copy (the user's eval suite if one exists; otherwise construct a minimal probe that exercises the instruction's purpose). Asking the model whether it needs an instruction is not a measurement.
- **One change at a time** where stakes are high, so regressions attribute to their cause.
- **If a cut regresses, re-add simply.** Re-express the instruction in its minimal form and re-probe - don't restore the verbose original.
- **Check out-of-band dependencies before deleting.** Grep the wider system for the exact prompt text first - classifiers, tests, and log parsers sometimes match on prompt strings.
- **Re-audit at every model release.** Prompts are per-model artifacts; a line that is load-bearing on one generation is cruft on the next. Each new migration section in `shared/model-migration.md` is the trigger to run this audit again.
# Prompt Caching - Design & Optimization
This file covers how to design prompt-building code for effective caching. For language-specific syntax, see the `## Prompt Caching` section in each language's README or single-file doc.
## The one invariant everything follows from
**Prompt caching is a prefix match. Any change anywhere in the prefix invalidates everything after it.**
The cache key is derived from the exact bytes of the rendered prompt up to each `cache_control` breakpoint. A single byte difference at position N - a timestamp, a reordered JSON key, a different tool in the list - invalidates the cache for all breakpoints at positions >= N.
Render order is: `tools` -> `system` -> `messages`. A breakpoint on the last system block caches both tools and system together.
Design the prompt-building path around this constraint. Get the ordering right and most caching works for free. Get it wrong and no amount of `cache_control` markers will help.
---
## Workflow for optimizing existing code
When asked to add or optimize caching:
1. **Trace the prompt assembly path.** Find where `system`, `tools`, and `messages` are constructed. Identify every input that flows into them.
2. **Classify each input by stability:**
- Never changes -> belongs early in the prompt, before any breakpoint
- Changes per-session -> belongs after the global prefix, cache per-session
- Changes per-turn -> belongs at the end, after the last breakpoint
- Changes per-request (timestamps, UUIDs, random IDs) -> **eliminate or move to the very end**
3. **Check rendered order matches stability order.** Stable content must physically precede volatile content. If a timestamp is interpolated into the system prompt header, everything after it is uncacheable regardless of markers.
4. **Place breakpoints at stability boundaries.** See placement patterns below.
5. **Audit for silent invalidators.** See anti-patterns table.
---
## Placement patterns
### Large system prompt shared across many requests
Put a breakpoint on the last system text block. If there are tools, they render before system - the marker on the last system block caches tools + system together.
```json
"system": [
{"type": "text", "text": "<large shared prompt>", "cache_control": {"type": "ephemeral"}}
]
```
### Multi-turn conversations
Put a breakpoint on the last content block of the most-recently-appended turn. Each subsequent request reuses the entire prior conversation prefix. Earlier breakpoints remain valid read points, so hits accrue incrementally as the conversation grows.
```json
// Last content block of the last user turn
messages[-1].content[-1].cache_control = {"type": "ephemeral"}
```
### Shared prefix, varying suffix
Many requests share a large fixed preamble (few-shot examples, retrieved docs, instructions) but differ in the final question. Put the breakpoint at the end of the **shared** portion, not at the end of the whole prompt - otherwise every request writes a distinct cache entry and nothing is ever read.
```json
"messages": [{"role": "user", "content": [
{"type": "text", "text": "<shared context>", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "<varying question>"} // no marker - differs every time
]}]
```
### Mid-conversation system messages
**Claude Opus 5, Claude Opus 4.8, Claude Fable 5, Claude Fable 5.1, Claude Mythos 5, and Claude Mythos 5.1; no beta header. Not available on Claude Sonnet 5** - use top-level `system` there. (Sources conflict on Claude Sonnet 5: the model config marks it supported, but every canonical docs page omits it. Treat it as unsupported and catch the 400.) When an operator instruction arrives mid-conversation - a mode switch, updated context, dynamically injected state - send it as `{"role": "system", "content": "..."}` appended to `messages[]`, rather than editing top-level `system`. Editing top-level `system` changes the prefix ahead of the entire conversation history, so every cached turn is re-processed uncached; a `role: "system"` message sits after the history and leaves the cached prefix intact.
```json
// Top-level system stays byte-identical; new instruction goes after the cached history
"system": [{"type": "text", "text": "<stable core>", "cache_control": {"type": "ephemeral"}}],
"messages": [
...history,
{"role": "user", "content": "..."},
{"role": "system", "content": "Terse mode enabled - keep responses under 40 words."}
]
```
This is also the prompt-injection-safe replacement for embedding operator instructions as text inside a user turn (the `<system-reminder>` pattern): both have the same caching profile, but `role: "system"` is the non-spoofable operator channel, whereas text inside user/tool content can be forged by anything that writes to user-visible input.
Must follow a `role: "user"` message (or an `assistant` message ending in server-tool use), and must be either the last entry in `messages` or be followed by an `assistant` turn; cannot be `messages[0]` - use top-level `system` for the initial prompt. Content is text-only. Unsupported models return a 400 (`BadRequestError`: `role 'system' is not supported on this model`); catch that error and fall back to putting the instruction in a user-turn `<system-reminder>` block.
**Per-turn reminders in a tool loop: turn-scoped messages, never deleted.** A reminder injected into history and removed on the next request is a history edit - the cache misses from that point and, on Claude Fable 5.1 / Claude Mythos 5.1, every later thinking block is invalidated. Instead give the `role: "system"` message `clear_at: "next_user_message"` (beta `mid-conversation-system-clear-at-2026-08-21`; same models and platforms as mid-conversation system messages): it renders for one turn, then stays in the transcript cleared - costing no input tokens, not cache-eligible (`cache_control` on it is a 400; put the breakpoint on the preceding user turn), and still part of the prefix. Append a fresh copy after each `tool_result` message and leave earlier copies in place; without the beta, a `text` block after the `tool_result` blocks in the same user message, earlier copies kept. Separately, per-message effort (beta `mid-conversation-output-config-2026-07-01`; Claude Fable 5.1, Claude Mythos 5.1, Claude Opus 5; Claude API): a `role: "system"` message with `content: []` and `output_config: {effort: ...}` changes effort from the next user turn on **without** the messages-cache invalidation that a top-level `effort` change causes, and is exempt from the placement rules (it can sit anywhere) - see the Invalidation hierarchy below and `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 -> New API features.
### Prompts that change from the beginning every time
Don't cache. If the first 1K tokens differ per request, there is no reusable prefix. Adding `cache_control` only pays the cache-write premium with zero reads. Leave it off.
---
## Architectural guidance
These are the decisions that matter more than marker placement. Fix these first.
**Keep the system prompt frozen.** Don't interpolate "current date: X", "mode: Y", "user name: Z" into the system prompt - those sit at the front of the prefix and invalidate everything downstream. Inject dynamic context later in `messages` instead - as a `{"role": "system", ...}` message where supported (see § Mid-conversation system messages above), or as text in a user message otherwise. A message at turn 5 invalidates nothing before turn 5.
**Don't change tools or model mid-conversation.** Tools render at position 0; adding, removing, or reordering a tool invalidates the entire cache. Same for switching models (caches are model-scoped). If you need "modes", don't swap the tool set - give Claude a tool that records the mode transition, or pass the mode as message content. Serialize tools deterministically (sort by name).
**Fork operations must reuse the parent's exact prefix.** Side computations (summarization, compaction, sub-agents) often spin up a separate API call. If the fork rebuilds `system` / `tools` / `model` with any difference, it misses the parent's cache entirely. Copy the parent's `system`, `tools`, and `model` verbatim, then append fork-specific content at the end.
---
## Silent invalidators
When reviewing code, grep for these inside anything that feeds the prompt prefix:
| Pattern | Why it breaks caching |
|---|---|
| `datetime.now()` / `Date.now()` / `time.time()` in system prompt | Prefix changes every request |
| `uuid4()` / `crypto.randomUUID()` / request IDs early in content | Same - every request is unique |
| `json.dumps(d)` without `sort_keys=True` / iterating a `set` | Non-deterministic serialization -> prefix bytes differ |
| f-string interpolating session/user ID into system prompt | Per-user prefix; no cross-user sharing |
| Conditional system sections (`if flag: system += ...`) | Every flag combination is a distinct prefix |
| `tools=build_tools(user)` where set varies per user | Tools render at position 0; nothing caches across users |
Fix by moving the dynamic piece after the last breakpoint, making it deterministic, or deleting it if it's not load-bearing.
---
## API reference
```json
"cache_control": {"type": "ephemeral"} // 5-minute TTL (default)
"cache_control": {"type": "ephemeral", "ttl": "1h"} // 1-hour TTL
```
- Max **4** `cache_control` breakpoints per request.
- Goes on any content block: system text blocks, tool definitions, message content blocks (`text`, `image`, `tool_use`, `tool_result`, `document`).
- Top-level `cache_control` on `messages.create()` auto-places on the last cacheable block - simplest option when you don't need fine-grained placement (§ Automatic vs explicit breakpoints).
- Caches are isolated per workspace on the Claude API, Claude Platform on AWS, and Microsoft Foundry (per organization on Amazon Bedrock and Google Cloud), and never shared across organizations. Traffic for the same prompt split across workspaces writes and reads separate entries - check this before blaming a low hit rate on the prompt.
- Minimum cacheable prefix is model-dependent. Shorter prefixes silently won't cache even with a marker - no error, just `cache_creation_input_tokens: 0`:
| Model | Minimum |
|---|---:|
| Claude Opus 5, Claude Fable 5, Claude Mythos 5, Claude Fable 5.1, Claude Mythos 5.1 | 512 tokens |
| Opus 4.8, Claude Sonnet 5, Sonnet 4.6, Sonnet 4.5, Opus 4.1, Opus 4, Sonnet 4 | 1024 tokens |
| Opus 4.7, Mythos Preview, Haiku 3.5 | 2048 tokens |
| Opus 4.6, Opus 4.5, Haiku 4.5 | 4096 tokens |
**The minimum is not monotonic across generations** - 512 on the newest models, but 4096 on Opus 4.6/4.5 and Haiku 4.5. A 3K-token prompt caches on Claude Opus 5, Opus 4.8, and Sonnet 4.5, and silently won't on Opus 4.6 or Haiku 4.5. Claude Opus 5 halves the Opus 4.8 minimum (1024 -> 512), so prompts previously too short to cache now create entries with no code change.
These minimums apply on **every** platform where the model is available - the old Amazon Bedrock override for Claude Fable 5.1 was removed, and no per-platform exception remains.
**Economics:** Cache reads cost ~0.1× base input price - **0.025× on Claude Fable 5.1** ($0.25/MTok; whether Claude Mythos 5.1 shares that rate is open at launch), which moves every break-even below proportionally. Cache writes cost **1.25× for 5-minute TTL, 2× for 1-hour TTL**. Break-even depends on TTL: with 5-minute TTL, two requests break even (1.25× + 0.1× = 1.35× vs 2× uncached); with 1-hour TTL, you need at least three requests (2× + 0.2× = 2.2× vs 3× uncached). The 1-hour TTL keeps entries alive across gaps in bursty traffic, but the doubled write cost means it needs more reads to pay off.
### Choosing the TTL
A cache read refreshes the entry's timer at no additional cost, on either TTL. The lifetime is measured from the **start** of the request that writes or reads the entry - generation time counts against it, so a 4-minute generation leaves about 1 minute for the next request to start before a 5-minute entry expires. Requests that share a prefix and start less than 5 minutes apart keep the 5-minute cache warm indefinitely - the 1-hour TTL buys nothing there except the doubled write price. Choose by the start-to-start gap between requests that share the prefix:
| Start-to-start gap between requests sharing the prefix | TTL |
|---|---|
| Under 5 minutes (continuous traffic; agent loops whose turns generate well under 5 minutes) | 5-minute - every request refreshes it; strictly cheaper |
| 5-60 minutes (a user who replies after 20 minutes; an agentic side-task or a generation that runs past 5 minutes between reads) | 1-hour - the only window where the 2× write pays off |
| Over an hour | Neither helps directly - re-warm on a schedule (§ Pre-warming the cache) or accept the cold miss |
**Claude Fable 5.1 / Claude Mythos 5.1: a keep-alive is usually cheaper than the 1-hour TTL.** With cache reads at 0.025x on Claude Fable 5.1 (versus 0.1x elsewhere; whether Claude Mythos 5.1 shares that rate is open at launch - see Economics above) a miss is much more expensive *relative to a hit*, and a read is nearly free - so for the 5-60 minute gap, instead of paying the 2x write for the 1-hour TTL, stay on the default 5-minute TTL and, while idle, re-send the previous request with `max_tokens: 0` shortly before the entry would expire. That request refreshes the entry's timer and bills only a cheap cache read (no output tokens). At Claude Fable 5.1 prices this beats the 1-hour TTL unless pauses regularly approach an hour. `max_tokens: 0` follows § Pre-warming's rejected combinations; on these models the ones that can arise are `stream: true`, structured outputs, and Batches (forced `tool_choice` and `thinking.type: "enabled"` are already 400s here). Send the keep-alive with `stream` off - streaming is a transport option, not part of the cached prefix, so dropping it for this one request costs nothing - and where the request can't be reshaped that way, with structured outputs (`output_config.format`) or inside a Message Batches request, use the 1-hour TTL instead. The prompt-caching page (`shared/live-sources.md`) has a cost comparison on a sample workload and an example keep-alive request.
On the Claude API, cache reads also do not count toward input-token rate limits on most models (Haiku 3.5 is the documented exception - see the rate-limits doc), so keeping entries alive across gaps can raise effective throughput as well as cut cost.
---
## Automatic vs explicit breakpoints
Automatic caching is a top-level `cache_control` field on the request, not on any content block. The system places the breakpoint on the last cacheable block and moves it forward as the conversation grows; if the last block isn't an eligible target it silently walks backward to the nearest eligible one, and skips caching if none is found. The automatic breakpoint defaults to the 5-minute TTL (the top-level field accepts `ttl: "1h"`) and consumes one of the 4 breakpoint slots. It composes with explicit markers in the same request, with two documented 400s: all 4 slots already taken by explicit markers, and an explicit marker on the last block whose TTL differs from the top-level field's (an explicit marker there with the same TTL makes automatic caching a no-op).
Automatic is the right default for multi-turn conversations - the multi-turn placement pattern above with no marker bookkeeping. Use explicit breakpoints when:
| Situation | Why automatic is the wrong tool |
|---|---|
| The prompt ends in unique per-request content (retrieved rows, per-request context, the one-off question) | The automatic breakpoint lands after the unique tail, so every request pays the write premium on bytes that are never read back - a pure surcharge. The signature: `cache_creation_input_tokens` on every request while `cache_read_input_tokens` never covers the full shared prefix. Put an explicit marker at the end of the shared portion instead (§ Shared prefix, varying suffix). |
| Sections change at different frequencies (tools never, context daily, conversation per-turn) | Automatic places exactly one breakpoint; multiple stability boundaries need explicit markers. |
| One block should be 1-hour TTL and another 5-minute | Per-block TTL requires explicit markers - and entries with the longer TTL must appear before shorter ones (a 1-hour entry must appear before any 5-minute entries). |
| A single turn appends more than 20 positions (consecutive tool_use runs, and tool_result runs, each collapse to one position) | The lookback can miss the previous entry - § 20-block lookback window. |
| A platform or integration without automatic caching (check `shared/platform-availability.md`) | The top-level field is rejected there - use explicit markers only. |
**The robust combination for agent loops:** one explicit breakpoint on the last block of the static system prefix - the expensive shared part gets a guaranteed read point that survives whatever happens later in `messages` - plus top-level automatic caching for the growing conversation tail (where automatic caching is available - `shared/platform-availability.md`).
---
## Verifying cache hits
The response `usage` object reports cache activity:
| Field | Meaning |
|---|---|
| `cache_creation_input_tokens` | Tokens written to cache this request (you paid the ~1.25× write premium) |
| `cache_read_input_tokens` | Tokens served from cache this request (you paid ~0.1×) |
| `input_tokens` | Tokens processed at full price (not cached) |
If `cache_read_input_tokens` is zero across repeated requests with identical prefixes, a silent invalidator is at work - diff the rendered prompt bytes between two requests to find it.
**`input_tokens` is the uncached remainder only.** Total prompt size = `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. If your agent ran for hours but `input_tokens` shows 4K, the rest was served from cache - check the sum, not the single field.
Language-specific access: `response.usage.cache_read_input_tokens` (Python/TS/Ruby), `$message->usage->cacheReadInputTokens` (PHP), `resp.Usage.CacheReadInputTokens` (Go/C#), `.usage().cacheReadInputTokens()` (Java).
**Verify after every change, not just at setup.** The costliest caching failure in production is silent: requests keep succeeding, the bill is just higher - no error, nothing announces it. The typical shape is a regression, not a bad first implementation: caching works when written, then a later change to prompt assembly (a new dynamic field in the system prompt, a history-rewriting feature, a tool list that stopped being deterministic) misses on every request and goes unnoticed for months. The `usage` fields are the only ground truth that caching is working. Re-check them whenever prompt-assembly code changes, and prefer a standing check - an integration-test assertion that a second identical request shows `cache_read_input_tokens > 0`, or monitoring on the usage fields - over a one-time look.
**The healthy-loop signature.** Writes bill only the delta past the highest cache hit, so in a steady multi-turn loop each request should read everything accumulated so far and write only what the last turn added:
- `cache_read_input_tokens` - the whole prior prefix; grows turn over turn
- `cache_creation_input_tokens` - roughly the previous assistant output plus the newly appended input; small relative to the conversation
- `input_tokens` - just the tail after the last breakpoint
If `cache_creation_input_tokens` is instead near the full conversation size on every request, either the prefix is being rewritten upstream of the breakpoint, or the write is happening for a reason payload diffing and cache diagnostics can't localize - with thinking enabled on a model that strips prior-turn thinking blocks the invalidation is server-side (§ Invalidation hierarchy), and a single turn that appends more than 20 positions (parallel tool-call runs collapse to one position - § 20-block lookback window) pushes the previous entry out of the lookback so every request rewrites the whole conversation with byte-identical payloads (§ 20-block lookback window). Rule both show-nothing cases out first from the model and the turn shape. Reads can only land on positions where a previous request wrote a breakpoint, so the usage fields say *that* the prefix broke (reads collapse, often to zero) but not where - the payload diff or cache diagnostics below localizes the exact point.
**Finding the invalidator.** Log several consecutive request payloads (the full JSON body) and diff adjacent pairs. In a growing conversation, adjacent payloads legitimately differ at the end (the newly appended turn); what must be byte-identical is the overlap - the previous request's prompt should reappear unchanged as a prefix of the next. Strip `cache_control` markers before diffing: the moving marker always differs between adjacent requests and is not an invalidator (previously-marked blocks are still cache hits). The first remaining divergence inside the overlapping region is the invalidation point. This catches the class of bug code review misses - nondeterministic serialization, a library reordering keys or fields, a value that changes between requests but not within one. On the Claude API, cache diagnostics (beta header `cache-diagnosis-2026-04-07`) does this comparison server-side once you opt in: send the header on **every** request - fingerprints are stored only for requests that carried it, so a one-shot retrofit fails with `previous_message_not_found` - then pass the previous response's `id` as `diagnostics.previous_message_id` and the response's `diagnostics` object names where the two requests diverged (model, system, tools, or message history). No payload logging needed. Availability: `shared/platform-availability.md`.
**Unexplained writes:** `usage.cache_creation` breaks `cache_creation_input_tokens` down by TTL (`ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`). Server tools such as web search automatically insert a 5-minute cache write after tool results when the request already uses caching - writes at a position you didn't mark; expected behavior, not an invalidator.
---
## Invalidation hierarchy
Not every parameter change invalidates everything. The API has three cache tiers, and changes only invalidate their own tier and below:
| Change | Tools cache | System cache | Messages cache |
|---|:---:|:---:|:---:|
| Tool definitions (add/remove/reorder) | No | No | No |
| Model switch | No | No | No |
| `speed`, web-search, citations toggle | Yes | No | No |
| System prompt content | Yes | No | No |
| `tool_choice`, images | Yes | Yes | No |
| `thinking` or `effort` change | model-specific | model-specific | No |
| Message content | Yes | Yes | No |
Implication: you can change `tool_choice` per-request without losing the tools+system cache, and message-content changes never touch it. Thinking and `effort` changes always invalidate the messages cache, and on models that render the thinking configuration ahead of tools and system they invalidate those caches too - pin thinking and effort settings per route rather than varying them per request. Only tool-definition and model changes force a full rebuild on every model.
**Three of these rows have a cache-preserving escape hatch** - the tools row, the system-prompt row, and (on Claude Fable 5.1 / Claude Mythos 5.1 / Claude Opus 5) the `effort` row - each by moving the change out of the top-level request and into a system message inside `messages[]`, after the cached prefix. The inject-then-delete reminder pattern has its own hatch: a text block appended after the `tool_result` blocks in the user message, never deleted. **Availability differs per row** - they are not gated together:
| Top-level change that invalidates | Cache-preserving form | Available on |
|---|---|---|
| Tool definitions (add/remove) | `tool_addition` / `tool_removal` blocks - see `shared/tool-use-concepts.md` § Mid-conversation tool changes | Claude Opus 5 onward, behind `mid-conversation-tool-changes-2026-07-01` |
| System prompt content | A `{"role": "system", "content": "..."}` message - see § Mid-conversation system messages above | Claude Opus 5, Claude Opus 4.8, Claude Fable 5, Claude Fable 5.1, Claude Mythos 5, Claude Mythos 5.1 - **already available today**, no beta header |
| Per-turn reminder (inject, then delete next request) | A turn-scoped `clear_at: "next_user_message"` system message, left in the transcript - see § Mid-conversation system messages above (without the beta: a text block after the `tool_result` blocks, earlier copies kept) | Same models as mid-conversation system messages, behind `mid-conversation-system-clear-at-2026-08-21` |
| `effort` change | A `{"role": "system", "content": [], "output_config": {"effort": ...}}` message - see `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5 | Claude Fable 5.1, Claude Mythos 5.1, Claude Opus 5, behind `mid-conversation-output-config-2026-07-01` |
| Dropped thinking blocks (a Claude Fable 5.1 / Claude Mythos 5.1 block replayed to a model that can't read it, or a history-editing-check `drop_block`) | None - the API drops the block on that request and the messages cache changes from its position onward; tools and system caches are intact. Blocks the receiving model can read, passed back unchanged, keep the cache intact | - |
Model switch has no escape hatch: caches are model-scoped. Keep the main loop on one model and spawn a subagent for cheaper sub-tasks (see `agent-design.md` § Caching for Agents).
**Thinking blocks and the messages cache (model-specific).** On Claude Fable 5, Claude Fable 5.1, Claude Mythos 5, Claude Mythos 5.1, Mythos Preview, Opus 4.5 and later, and Sonnet 4.6 and later, previous-turn thinking blocks are preserved by default, so passing a regular (non-tool-result) user message with thinking enabled leaves the messages cache valid. On earlier Opus and Sonnet models and all Haiku models through Haiku 4.5, that same request strips previously-cached thinking blocks from context, and every message after the first stripped block falls out of cache - in an agent loop this shows up as a `cache_creation_input_tokens` spike on turns where a plain user message follows tool use. (Toggling thinking on/off between requests is a separate, all-models invalidator of the messages cache - see the hierarchy table above. Changing `output_config.effort` behaves the same as changing thinking parameters; setting the model's default effort explicitly is equivalent to omitting it, so pinning the default costs nothing.)
---
## 20-block lookback window
Each breakpoint walks backward **at most 20 positions** to find a prior cache entry. On the Claude API a run of consecutive `tool_use` blocks counts as one position, and so does a run of consecutive `tool_result` blocks, so a turn with many *parallel* tool calls doesn't push the previous request's entry out of the window; a turn that adds more than 20 positions of other content (long sequential tool loops, many text/image blocks) still can - the next request's breakpoint won't find the previous cache and silently misses.
Fix: place an intermediate breakpoint every ~15 positions in long turns, or put the marker on a block that's within 20 positions of the previous turn's last cached block.
---
## Concurrent-request timing
A cache entry becomes readable only after the first response **begins streaming**. N parallel requests with identical prefixes all pay full price - none can read what the others are still writing.
For fan-out patterns: send 1 request, await the first streamed token (not the full response), then fire the remaining N-1. They'll read the cache the first one just wrote.
The same arithmetic shapes multi-agent designs: N parallel workers each assembling a slightly different prompt over the same context write N separate cache entries and read none of each other's. When input cost dominates, fewer lanes over a byte-identical shared prefix - or one worker making N sequential passes - turn those writes into reads.
## Pre-warming the cache
To eliminate the cache-miss latency on the *first* real request, send a **`max_tokens: 0`** request at startup (or on an interval). The API runs prefill - writing the cache at your `cache_control` breakpoint - and returns immediately with `content: []`, `stop_reason: "max_tokens"`, and a populated `usage` block (zero output tokens billed; normal cache-write charge on `cache_creation_input_tokens`).
**When to pre-warm** - pre-warming trades a cache-write charge *now* for lower TTFT on the *next* real request. It's worth it when all three hold: (a) first-request latency is user-visible (chat/voice/interactive - not background jobs), (b) the shared prefix is large enough that a cold write is noticeably slow, and (c) there's a moment *before* traffic to fire it - app startup, worker boot, post-deploy, start of a scheduled window.
| Skip pre-warming when... | Because |
|---|---|
| Traffic is continuous (requests <= TTL apart) | The first real request warms the cache and every subsequent one hits it; a separate warm call is a pure extra write |
| The prefix is small or below the cacheable minimum | The cold-write penalty is negligible |
| The prefix varies per request/user | Nothing shared to pre-warm |
| You'd pre-warm many distinct prefixes speculatively | Each is a ~1.25× write; cost can exceed the latency you save |
**Scheduled re-warms:** only needed when traffic has gaps longer than the TTL. If real requests arrive more often than every 5 minutes, they keep the cache warm on their own - don't add an interval re-warm. For bursty traffic with long idle gaps, either re-warm just under the TTL or switch to `ttl: "1h"` and re-warm less often.
```python
client.messages.create(
model="claude-opus-5",
max_tokens=0,
system=[{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": "warmup"}],
)
```
**Breakpoint placement:** put `cache_control` on the **last block shared with the real request** (the system prompt or tool definitions) - **not** on the placeholder user message, and **not** via top-level automatic caching (which would key the cache to the placeholder). The placeholder can be any non-whitespace string; it's read during prefill but never answered.
**Rejected combinations:** `max_tokens: 0` is an `invalid_request_error` with `stream: true`, `thinking.type: "enabled"`, `output_config.format`, `tool_choice` of `{"type":"tool"}` or `{"type":"any"}`, or inside a Message Batches request.
**TTL still applies** - re-warm at least every 5 minutes for the default cache, or use the 1-hour TTL. This replaces the older `max_tokens: 1` workaround (no single-token reply to discard, no output tokens billed, intent is unambiguous).
# Token Counting
Use the `count_tokens` endpoint (`POST /v1/messages/count_tokens`) for accurate
token counts against Claude models. Token counts are **model-specific** - pass
the same model ID you'll use for inference.
**Do not use `tiktoken`.** It's OpenAI's tokenizer. It undercounts Claude
tokens by ~15-20% on typical text, and by much more on code or non-English
input. Any estimate from `tiktoken`, `gpt-tokenizer`, or similar is wrong for
Claude.
## Count a file or string
```python
from anthropic import Anthropic
client = Anthropic()
resp = client.messages.count_tokens(
model="claude-opus-5",
messages=[{"role": "user", "content": open("CLAUDE.md").read()}],
)
print(resp.input_tokens)
```
TypeScript: `await client.messages.countTokens({model, messages})` ->
`.input_tokens`. See `{lang}/claude-api/README.md` for other SDKs.
## CLI
```sh
ant messages count-tokens --model claude-opus-5 \
--message '{role: user, content: "@./CLAUDE.md"}' \
--transform input_tokens -r
```
## Diffing a file across two versions
The endpoint is stateless - count each version separately and subtract:
```python
from anthropic import Anthropic
import subprocess
client = Anthropic()
def count(text: str) -> int:
return client.messages.count_tokens(
model="claude-opus-5",
messages=[{"role": "user", "content": text}],
).input_tokens
before = subprocess.check_output(["git", "show", "HEAD:CLAUDE.md"], text=True)
after = open("CLAUDE.md").read()
print(count(after) - count(before))
```
Full docs: see the Token Counting entry in `shared/live-sources.md`.
# Tool Use Concepts
This file covers the conceptual foundations of tool use with the Claude API. For language-specific code examples, see the `python/`, `typescript/`, or other language folders. For decision heuristics on which tools to expose, how to manage context in long-running agents, and caching strategy, see `agent-design.md`.
## User-Defined Tools
### Tool Definition Structure
> **Note:** When using the Tool Runner (beta), tool schemas are generated automatically from your function signatures (Python), Zod schemas (TypeScript), annotated classes (Java), `jsonschema` struct tags (Go), or `BaseTool` subclasses (Ruby). The raw JSON schema format below is for the manual approach - including PHP's `BetaRunnableTool`, which wraps a run closure around a hand-written schema - or SDKs without tool runner support.
Each tool requires a name, description, and JSON Schema for its inputs:
```json
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g., San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
```
**Best practices for tool definitions:**
- Use clear, descriptive names (e.g., `get_weather`, `search_database`, `send_email`)
- Write detailed descriptions - Claude uses these to decide when to use the tool. Be **prescriptive about *when* to call it**, not just what it does (e.g. "Call this when the user asks about current prices or recent events"). On recent Opus models, which reach for tools more conservatively, trigger conditions in the description give measurable lift in should-call rate.
- Include descriptions for each property
- Use `enum` for parameters with a fixed set of values
- Mark truly required parameters in `required`; make others optional with defaults
---
### Tool Choice Options
Control when Claude uses tools:
| Value | Behavior |
| --------------------------------- | --------------------------------------------- |
| `{"type": "auto"}` | Claude decides whether to use tools (default) |
| `{"type": "any"}` | Claude must use at least one tool |
| `{"type": "tool", "name": "..."}` | Claude must use the specified tool |
| `{"type": "none"}` | Claude cannot use tools |
Any `tool_choice` value can also include `"disable_parallel_tool_use": true` to force Claude to use at most one tool per response. By default, Claude may request multiple tool calls in a single response.
**Claude Fable 5.1, Claude Mythos 5.1, and Mythos Preview reject forced tool use:** `{"type": "any"}` and `{"type": "tool", "name": ...}` return a 400 there (`tool_choice: type "tool" and "any" are not supported for this model.` - on `count_tokens` and Batches too). It is a model-specific restriction (Claude Fable 5 and Claude Opus 5 accept them). Use `{"type": "auto"}` and state the expectation in the prompt ("Use the get_weather tool to answer") - `strict: true` on the tool keeps the schema-valid-arguments guarantee `any` gave you - or structured outputs (`output_config.format`) when the forced call only existed to extract JSON. `auto` and `none` are unaffected; `disable_parallel_tool_use` with `auto` still means at most one call (the "exactly one" combination with `any`/`tool` is gone). Combining `tool_choice` `any` with `strict: true` applies only on models that support forced tool use. See `shared/model-migration.md` -> Migrating to Claude Fable 5.1 from Claude Fable 5.
---
### Tool Runner vs Manual Loop
**Tool Runner (Recommended):** The SDK's tool runner handles the agentic loop automatically - it calls the API, detects tool use requests, executes your tool functions, feeds results back to Claude, and repeats until Claude stops calling tools. Available in Python, TypeScript, Java, Go, Ruby, PHP, and C# SDKs (beta). The Python SDK also provides MCP conversion helpers (`anthropic.lib.tools.mcp`) to convert MCP tools, prompts, and resources for use with the tool runner - see `python/claude-api/tool-use.md` for details. **Default to the tool runner** for any custom-tool agent.
**The tool runner is not a black box - "I need control" is rarely a reason to drop to the manual loop.** Each iteration yields the assistant message *before* the tools run and lets you intervene, so most "fine-grained control" needs are covered without hand-writing the loop:
- **Human-in-the-loop approval / gating** - gate in the tool's run function (return a "user declined" result instead of executing), or inspect the tool call in the yielded message and override the pending request with `set_messages_params()` / `setMessagesParams()` / `append_messages()` / `pushMessages()` to allow or deny *before* the tool executes. The runner runs your function automatically only if you don't intervene.
- **Error interception** - inspect the tool result before it returns to Claude (`generate_tool_call_response()` / `generateToolResponse()`); stop early or handle it yourself.
- **Result modification** - mutate the tool result before it goes back (e.g. add `cache_control` for prompt caching, or transform the output).
- **Per-turn retries / param changes** - e.g. bump `max_tokens` and re-run a truncated turn; bound the whole loop with `max_iterations`.
- **Streaming and automatic compaction** are both supported.
These hooks are SDK helper features, not separate API parameters - for the exact method names and worked examples, WebFetch the per-language SDK repo listed in `shared/live-sources.md` -> *Claude API SDK Repositories* (the tool-runner helpers live in each repo's `tools.md` / `helpers.md`). The bundled `python/claude-api/tool-use.md` and `typescript/claude-api/tool-use.md` show the basic tool-runner setup.
**Don't drop to a manual loop because of these misconceptions:**
- The tool runner does not require Zod/Pydantic - `betaTool()` (TS) and `@beta_tool` (Python) accept raw JSON Schema; other SDKs use plain structs/maps/classes.
- The runner makes detecting the final turn *easier*, not harder - iteration ends when Claude stops calling tools, and the last yielded message is the final response. Most SDKs also offer a one-shot variant (`runner.until_done()` / `runner.runUntilDone()` / `RunToCompletion()`).
- Confirmation/approval gates work with the runner (see Security below).
**Manual Agentic Loop:** Reach for this only when you want to own the *entire* loop - you need control the runner does not expose (e.g., a custom transport, request shapes the SDK cannot build, per-token streaming on SDKs whose runner does not support it), you'd rather not take the beta dependency, or your control flow doesn't fit the runner's per-turn hooks (e.g. interleaving unrelated work mid-loop). Approval gates, logging, interception, result modification, and conditional execution do **not** require it - the tool runner covers those (above). Loop until `stop_reason == "end_turn"`, always append the full `response.content` to preserve tool_use blocks, and ensure each `tool_result` includes the matching `tool_use_id`.
**Stop reasons for server-side tools:** When using server-side tools (code execution, web search, etc.), the API runs a server-side sampling loop. If this loop reaches its default limit of 10 iterations, the response will have `stop_reason: "pause_turn"`. To continue, re-send the user message and assistant response and make another API request - the server will resume where it left off. Do NOT add an extra user message like "Continue." - the API detects the trailing `server_tool_use` block and knows to resume automatically.
```python
# Handle pause_turn in your agentic loop
if response.stop_reason == "pause_turn":
messages = [
{"role": "user", "content": user_query},
{"role": "assistant", "content": response.content},
]
# Make another API request - server resumes automatically
response = client.messages.create(
model="claude-opus-5", messages=messages, tools=tools
)
```
**Note:** the SDK tool runners do not auto-resume `pause_turn` (as of `@anthropic-ai/sdk` 0.110.0 / `anthropic` 0.116.0) - a paused turn ends the runner and is returned as the final message, with no error. In TypeScript you can resume inside the iteration body (push the paused assistant turn back onto the runner); in Python the runner cannot be resumed mid-loop - restart a new runner with the paused turn appended, or handle `pause_turn` in a manual loop. See each language's `tool-use.md` for the pattern.
Set a `max_continuations` limit (e.g., 5) to prevent infinite loops. For the full guide, see: `https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons`
> **Security:** The tool runner executes your tool functions automatically whenever Claude requests them. For tools with side effects (sending emails, modifying databases, financial transactions), validate inputs and gate destructive operations behind human approval. **Both** the tool runner and the manual loop support this - with the tool runner, gate inside the tool's run function (prompt the user and return a "user declined" result instead of executing), or inspect the tool call in each yielded message and take over message history with `set_messages_params()` / `setMessagesParams()` to allow or deny *before* the tool runs (it executes your function automatically only if you don't intervene); with the manual loop you gate inline before calling the function.
---
### Handling Tool Results
When Claude uses a tool, the response contains a `tool_use` block. You must:
1. Execute the tool with the provided input
2. Send the result back in a `tool_result` message
3. Continue the conversation
**Error handling in tool results:** When a tool execution fails, set `"is_error": true` and provide an informative error message. Claude will typically acknowledge the error and either try a different approach or ask for clarification.
**Multiple tool calls:** Claude can request multiple tools in a single response. Handle them all before continuing - send all results back in a single `user` message.
---
## Server-Side Tools: Code Execution
The code execution tool lets Claude run code in a secure, sandboxed container. Unlike user-defined tools, server-side tools run on Anthropic's infrastructure - you don't execute anything client-side. Just include the tool definition and Claude handles the rest.
### Key Facts
- Runs in an isolated container (1 CPU, 5 GiB RAM, 5 GiB disk)
- No internet access (fully sandboxed)
- Python 3.11 with data science libraries pre-installed
- Containers persist for 30 days and can be reused across requests
- Free when used with web search/web fetch tools; otherwise $0.05/hour after 1,550 free hours/month per organization
### Tool Definition
The tool requires no schema - just declare it in the `tools` array:
```json
{
"type": "code_execution_20260120",
"name": "code_execution"
}
```
Claude automatically gains access to `bash_code_execution` (run shell commands) and `text_editor_code_execution` (create/view/edit files).
### Pre-installed Python Libraries
- **Data science**: pandas, numpy, scipy, scikit-learn, statsmodels
- **Visualization**: matplotlib, seaborn
- **File processing**: openpyxl, xlsxwriter, pillow, pypdf, pdfplumber, python-docx, python-pptx
- **Math**: sympy, mpmath
- **Utilities**: tqdm, python-dateutil, pytz, sqlite3
Additional packages can be installed at runtime via `pip install`.
### Supported File Types for Upload
| Type | Extensions |
| ------ | ---------------------------------- |
| Data | CSV, Excel (.xlsx/.xls), JSON, XML |
| Images | JPEG, PNG, GIF, WebP |
| Text | .txt, .md, .py, .js, etc. |
### Container Reuse
Reuse containers across requests to maintain state (files, installed packages, variables). Extract the `container_id` from the first response and pass it to subsequent requests.
### Response Structure
The response contains interleaved text and tool result blocks:
- `text` - Claude's explanation
- `server_tool_use` - What Claude is doing
- `bash_code_execution_tool_result` - Code execution output (check `return_code` for success/failure)
- `text_editor_code_execution_tool_result` - File operation results
> **Security:** Always sanitize filenames with `os.path.basename()` / `path.basename()` before writing downloaded files to disk to prevent path traversal attacks. Write files to a dedicated output directory.
---
## Server-Side Tools: Web Search and Web Fetch
Web search and web fetch let Claude search the web and retrieve page content. They run server-side - just include the tool definitions and Claude handles queries, fetching, and result processing automatically.
### Tool Definitions
```json
[
{ "type": "web_search_20260209", "name": "web_search" },
{ "type": "web_fetch_20260209", "name": "web_fetch" }
]
```
### Dynamic Filtering (Claude Opus 5 / Fable 5 / Opus 4.8 / Opus 4.7 / Opus 4.6 / Sonnet 5 / Sonnet 4.6)
The `web_search_20260209` and `web_fetch_20260209` versions support **dynamic filtering** - Claude writes and executes code to filter search results before they reach the context window, improving accuracy and token efficiency. Dynamic filtering is built into these tool versions and activates automatically; you do not need to separately declare the `code_execution` tool or pass any beta header.
```json
{
"tools": [
{ "type": "web_search_20260209", "name": "web_search" },
{ "type": "web_fetch_20260209", "name": "web_fetch" }
]
}
```
Without dynamic filtering, the previous `web_search_20250305` version is also available.
> **Note:** Only include the standalone `code_execution` tool when your application needs code execution for its own purposes (data analysis, file processing, visualization) independent of web search. Including it alongside `_20260209` web tools creates a second execution environment that can confuse the model.
---
## Server-Side Tools: Programmatic Tool Calling
With standard tool use, each tool call is a round trip: Claude calls, the result enters Claude's context, Claude reasons, then calls the next tool. Chained calls accumulate latency and tokens - most of that intermediate data is never needed again.
Programmatic tool calling lets Claude compose those calls into a script. The script runs in the code execution container; when it invokes a tool, the container pauses, the call executes, and the result returns to the running code (not to Claude's context). The script processes it with normal control flow. Only the final output returns to Claude. Use it when chaining many tool calls or when intermediate results are large and should be filtered before reaching the context window.
For full documentation, use WebFetch:
- URL: `https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling`
---
## Server-Side Tools: Tool Search
The tool search tool lets Claude dynamically discover tools from large libraries without loading all definitions into the context window. Use it when you have many tools but only a few are relevant to any given request. Discovered tool schemas are appended to the request, not swapped in - this preserves the prompt cache (see `agent-design.md` §Caching for Agents).
For full documentation, use WebFetch:
- URL: `https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool`
---
## Mid-conversation tool changes (Beta)
**Beta header `mid-conversation-tool-changes-2026-07-01`; Claude Opus 5 onward.** Normally `tools` is fixed for a conversation's lifetime - editing it changes the very front of the prompt prefix and invalidates the entire cache (see `prompt-caching.md` § Invalidation hierarchy). This feature lets you add and remove tools between turns while the cached prefix survives.
Both operations are content blocks on a `{"role": "system", ...}` message appended to `messages[]`, and both reference a tool by name via a `tool_reference`:
```python
# Removal - must sit immediately before an assistant message, or last in messages.
{"role": "system", "content": [
{"type": "tool_removal", "tool": {"type": "tool_reference", "name": "get_weather"}},
]}
# Addition - surfaces a tool declared up front with defer_loading.
{"role": "system", "content": [
{"type": "tool_addition", "tool": {"type": "tool_reference", "name": "get_forecast"}},
]}
```
**A tool you plan to add must already be declared in `tools[]` with `"defer_loading": True`.** Deferred tools are known to the request but not loaded into the model's context until a `tool_addition` surfaces them:
```python
tools = [
{"name": "get_weather", "description": "Get weather",
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}}},
{"name": "get_forecast", "description": "Get 5-day forecast",
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}},
"defer_loading": True},
]
```
**To change a tool's definition**, do it across two requests: send a `tool_removal` for the old definition on the first, then carry the conversation forward with the updated entry in `tools[]` on the next.
> Warning: Earlier previews used a different beta header and different block shapes; both are deprecated. Use `mid-conversation-tool-changes-2026-07-01` with `tool_addition` / `tool_removal` / `tool_reference`.
SDK typings lag these blocks - pass them as plain dicts in Python, or add a `@ts-expect-error` in TypeScript.
**Choosing between this and tool search:** tool search is for *discovery* - Claude finds what it needs from a large library on its own. Mid-conversation tool changes are for *control* - your application decides the tool set has changed (a mode switch, a resource that became available, a capability you want to revoke) and says so explicitly.
---
## Agent Skills (Messages API)
Agent Skills package task-specific instructions and files that Claude loads when relevant (e.g., the Anthropic pre-built `pptx`, `xlsx`, `pdf`, `docx` skills). On the **Messages API**, skills are enabled via the `container` parameter alongside the code-execution tool - this is **not** the Managed Agents surface and does **not** use `client.beta.agents` / `sessions` / `environments`. Availability: see `shared/platform-availability.md`.
Required on each request:
1. `client.beta.messages.create(...)` with the `code-execution-2025-08-25` beta flag (Skills is out of beta - no `skills-2025-10-02` header needed).
2. `container={"skills": [{"type": "anthropic", "skill_id": "<id>", "version": "latest"}]}` - the skills list selects which skills are available inside the execution container.
3. `tools=[{"type": "code_execution_20260521", "name": "code_execution"}]` - skills execute via code execution in the container.
```python
response = client.beta.messages.create(
model="claude-opus-5", max_tokens=16000,
betas=["code-execution-2025-08-25"],
container={"skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]},
tools=[{"type": "code_execution_20260521", "name": "code_execution"}],
messages=[{"role": "user", "content": "Create a 3-slide presentation on X"}],
)
```
Generated files (`.pptx`, `.xlsx`, ...) are written inside the container; the response carries a file ID for each. Download by passing that ID to the Files API (`client.files.download(file_id)` / `GET /v1/files/{id}/content`).
List available skills via `GET /v1/skills` (no beta header).
---
## MCP Connector (Beta)
The MCP connector lets Claude call tools hosted on a remote MCP server directly from the Messages API - Anthropic makes the MCP connection server-side. Requires beta flag `mcp-client-2025-11-20` on `client.beta.messages.create(...)`. Availability: see `shared/platform-availability.md`.
**Two parameters are required together:**
- `mcp_servers` - array of server connection definitions: `[{"type": "url", "url": "<server URL>", "name": "<server-name>", "authorization_token": "<optional>"}]`
- `tools` - must include an `mcp_toolset` entry that references the server by name: `[{"type": "mcp_toolset", "mcp_server_name": "<server-name>"}]`
The `mcp_server_name` in the toolset must match a `name` in `mcp_servers`. Omitting the `mcp_toolset` entry is rejected as a validation error - every server in `mcp_servers` must be referenced by exactly one toolset.
```python
client.beta.messages.create(
model="claude-opus-5", max_tokens=1024,
betas=["mcp-client-2025-11-20"],
mcp_servers=[{"type": "url", "url": "https://example/sse", "name": "example-mcp"}],
tools=[{"type": "mcp_toolset", "mcp_server_name": "example-mcp"}],
messages=[...],
)
```
Go uses the typed constant `anthropic.AnthropicBetaMCPClient2025_11_20`; the older `...2025_04_04` constant is deprecated.
Optional toolset fields: `default_config` (defaults for all tools, e.g. `{"enabled": false}` for allowlist mode) and `configs` (per-tool overrides keyed by tool name).
---
## Tool Use Examples
You can provide sample tool calls directly in your tool definitions to demonstrate usage patterns and reduce parameter errors. This helps Claude understand how to correctly format tool inputs, especially for tools with complex schemas.
For full documentation, use WebFetch:
- URL: `https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use`
---
## Client-Side Tools: Computer Use
Computer use lets Claude interact with a desktop environment (screenshots, mouse, keyboard). It is a client-side tool - your application provides the environment and executes the actions Claude requests; Anthropic processes the screenshots and action requests in real time but does not host the environment or retain the data.
For full documentation, use WebFetch:
- URL: `https://platform.claude.com/docs/en/agents-and-tools/computer-use/overview`
---
## Context Editing
Context editing clears stale tool results and thinking blocks from the transcript as a long-running agent accumulates turns. Unlike compaction (which summarizes), context editing prunes - the cleared content is removed, not replaced. Use it when old tool outputs are no longer relevant and you want to keep the transcript lean without losing the conversation structure.
**Beta.** Use `client.beta.messages.*` with beta `context-management-2025-06-27`. Configure via `context_management.edits` with a strategy type of `clear_tool_uses_20250919` (clear old tool results; optional `clear_tool_inputs: true` also clears the tool_use params) or `clear_thinking_20251015` (clear thinking blocks). These are **not** the compaction types - `compact_20260112` with beta `compact-2026-01-12` is the separate compaction feature.
For full documentation, use WebFetch:
- URL: `https://platform.claude.com/docs/en/build-with-claude/context-editing`
---
## Server-Side Tools: Advisor (Beta)
The advisor tool pairs a faster, lower-cost **executor** model (the top-level `model` on the request) with a higher-intelligence **advisor** model (the `model` field inside the tool definition) that provides strategic guidance mid-generation. The executor does most of the token generation; the advisor is consulted for planning. Availability: see `shared/platform-availability.md`.
### Tool Definition
```json
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-8"
}
```
Optional fields on the tool definition:
- `max_uses` - cap on advisor consultations per request. Exceeding it makes the `advisor_tool_result` block's `content` the error object `{"type": "advisor_tool_result_error", "error_code": "max_uses_exceeded"}` - the third member of the content union in the payload-shape table below.
- `max_tokens` - bounds the advisor's total output (thinking + text) per call. At the cap the result block carries `stop_reason: "max_tokens"` and a truncation note is appended to the advice the executor sees; the server also emits a remaining-tokens budget block in the advisor's prompt so it self-shapes toward the cap.
- `caching` - cache-control for the advisor's own prompt, same shape as a cache breakpoint: `"caching": {"type": "ephemeral", "ttl": "5m"}` (`ttl` is `"5m"` or `"1h"`, default `"5m"`). Each call writes a cache entry at that TTL so later calls in the conversation read the stable prefix. Omitted = advisor prompt not cached.
**The advisor model must be at least as capable as the executor.** An invalid pairing returns `400 invalid_request_error`. Valid pairs:
| Executor (request `model`) | Valid advisor (tool `model`) |
|---|---|
| `claude-haiku-4-5` / `claude-sonnet-4-6` / `claude-sonnet-5` / `claude-opus-4-6` / `claude-opus-4-7` | `claude-opus-5`, `claude-fable-5-1`, `claude-mythos-5-1`, `claude-fable-5`, `claude-mythos-5`, `claude-opus-4-8`, or `claude-opus-4-7` |
| `claude-opus-4-8` | `claude-opus-5`, `claude-fable-5-1`, `claude-mythos-5-1`, `claude-fable-5`, `claude-mythos-5`, or `claude-opus-4-8` |
| `claude-opus-5` | `claude-opus-5`, `claude-fable-5-1`, `claude-mythos-5-1`, `claude-fable-5`, or `claude-mythos-5` |
| `claude-fable-5` | `claude-fable-5-1`, `claude-mythos-5-1`, `claude-fable-5`, `claude-mythos-5`, or `claude-opus-5` |
| `claude-mythos-5` | `claude-mythos-5-1`, `claude-fable-5-1`, `claude-mythos-5`, `claude-fable-5`, or `claude-opus-5` |
| `claude-fable-5-1` / `claude-mythos-5-1` | `claude-mythos-5-1`, `claude-fable-5-1`, `claude-mythos-5`, `claude-fable-5`, or `claude-opus-5` - and these executors reject forced `tool_choice`, so nudge the advisor call from the prompt (the `-5-1` advisors return the encrypted `advisor_redacted_result`, like claude-opus-5 / claude-fable-5 / claude-mythos-5) |
> Warning: **The advisor's payload shape differs by advisor model.** The response block is always `advisor_tool_result`; what varies is its **`content`**, a discriminated union:
>
> | `content` type | Fields | When |
> |---|---|---|
> | `advisor_result` | `text`, `stop_reason` | Advisor returns plaintext (e.g. Opus 4.8) |
> | `advisor_redacted_result` | `encrypted_content`, `stop_reason` | Advisor returns encrypted output - Claude Opus 5, Claude Fable 5.1, Claude Mythos 5.1, Claude Fable 5, Claude Mythos 5 |
> | `advisor_tool_result_error` | `error_code` | Consultation failed - `max_uses_exceeded`, `prompt_too_long`, `too_many_requests`, `overloaded`, `unavailable`, `execution_time_exceeded`, or `model_not_found` |
>
> So switch on `advisor_tool_result.content` type, not on the block type. Code that reads `.text` unconditionally gets nothing back from an Claude Opus 5 advisor, because the payload is under `encrypted_content` instead - and you cannot read it, only replay it.
Call via `client.beta.messages.create(...)` with `betas=["advisor-tool-2026-03-01"]` (or the `anthropic-beta: advisor-tool-2026-03-01` header). In multi-turn conversations, append the full `response.content` - including any `advisor_tool_result` blocks - back to `messages` on the next turn. If you remove the advisor tool from `tools` on a later turn while the history still contains `advisor_tool_result` blocks, the API returns a 400.
> **Advisor on Managed Agents:** CMA sessions support an advisor too, configured as a `{"type": "advisor", "model"}` entry in the agent's multiagent roster rather than as a tool definition - no `max_uses`/`max_tokens`/`caching` options, and advice is delivered as thread events on the session's event stream rather than `advisor_tool_result` blocks. See `shared/managed-agents-multiagent.md` -> Advisor.
---
## Client-Side Tools: Memory
The memory tool enables Claude to store and retrieve information across conversations through a memory file directory. Claude can create, read, update, and delete files that persist between sessions.
### Key Facts
- Client-side tool - you control storage via your implementation
- Supports commands: `view`, `create`, `str_replace`, `insert`, `delete`, `rename`
- Operates on files in a `/memories` directory
- The Python, TypeScript, and Java SDKs provide helper classes/functions for implementing the memory backend
> **Security:** Never store API keys, passwords, tokens, or other secrets in memory files. Be cautious with personally identifiable information (PII) - check data privacy regulations (GDPR, CCPA) before persisting user data. The reference implementations have no built-in access control; in multi-user systems, implement per-user memory directories and authentication in your tool handlers.
For full implementation examples, use WebFetch:
- Docs: `https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool.md`
---
## Client-Side Tools: Bash and Text Editor
The bash and text editor tools are **Anthropic-defined, schema-less** tools. Declare them by `type` and `name` only - the input schema is built into the model and cannot be modified. **Do not pass an `input_schema`**, and do not define a custom tool that happens to be named `"bash"` - that creates a user-defined tool without the built-in behavior.
Both are **client-executed**: Claude returns a `tool_use` block, your code performs the action locally, and you send back a `tool_result`. The API is stateless; your application maintains the shell session or filesystem between turns.
### Bash tool declaration
```json
{"type": "bash_20250124", "name": "bash"}
```
| Language | Declaration |
|---|---|
| Python / TypeScript / Ruby / cURL | plain object `{"type": "bash_20250124", "name": "bash"}` |
| Go | `anthropic.ToolUnionParam{OfBashTool20250124: &anthropic.ToolBash20250124Param{}}` |
| Java | `.addTool(ToolBash20250124.builder().build())` from `com.anthropic.models.messages` |
| C# | `Tools = [new ToolBash20250124()]` from `Anthropic.Models.Messages` |
| PHP | `tools: [new \Anthropic\Messages\ToolBash20250124()]` |
Claude's `tool_use.input` contains either `{"command": "<string>"}` or `{"restart": true}`. Check for `restart` first (reset the session, return a confirmation string); otherwise run `command` and return combined stdout + stderr.
> **Security - commands are untrusted model output.** Run in an isolated environment (container, VM, or restricted user); apply an **allowlist** of permitted executables and reject shell operators (`&&`, `|`, `;`, `` ` ``, `$()`); set timeouts and resource limits; log every command. A blocklist is not sufficient.
### Text editor tool declaration
```json
{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}
```
Optional field: `max_characters` to cap `view` output. Java exposes a typed `ToolTextEditor20250728` builder (`com.anthropic.models.messages`); other statically-typed SDKs follow the same naming pattern - see the Anthropic-Defined Tools section in `{lang}/claude-api/tool-use.md` for the exact class.
> **Security - `path` is untrusted model output. Confine every file operation to a fixed project root.** Before executing any command, resolve the model-supplied `path` to its canonical form and verify it remains within your project root; reject the request if it escapes (`..`, symlinks, absolute paths outside the root, URL-encoded traversal like `%2e%2e%2f`). Use your language's built-in path utilities (e.g., Python `pathlib.Path.resolve()` then check `.is_relative_to(root)`). Never call `open()` / `writeFile` / `unlink` directly on the raw `path` value.
`tool_use.input.command` is one of:
| `command` | Other inputs | Action |
|---|---|---|
| `view` | `path`, optional `view_range` | Return file contents or directory listing |
| `create` | `path`, `file_text` | Create/overwrite file with `file_text`. Create a backup if the file already exists. |
| `str_replace` | `path`, `old_str`, `new_str` | Replace exactly one occurrence; error if 0 or >1 matches |
| `insert` | `path`, `insert_line`, `insert_text` | Insert `insert_text` after line `insert_line` (0 = beginning of file) |
For both tools, on error return `{"type": "tool_result", "tool_use_id": "...", "content": "<error text>", "is_error": true}` so Claude can recover.
---
## Structured Outputs
Structured outputs constrain Claude's responses to follow a specific JSON schema, guaranteeing valid, parseable output. This is not a separate tool - it enhances the Messages API response format and/or tool parameter validation.
Two features are available:
- **JSON outputs** (`output_config.format`): Control Claude's response format
- **Strict tool use** (`strict: true`): Guarantee valid tool parameter schemas
**Supported models:** Claude Fable 5, Claude Mythos 5, Claude Fable 5.1, Claude Mythos 5.1, Claude Opus 5, Claude Opus 4.8, Claude Sonnet 5, and Claude Haiku 4.5. Legacy models (Claude Opus 4.5, Claude Opus 4.1) also support structured outputs.
> **Recommended:** Use `client.messages.parse()` which automatically validates responses against your schema. When using `messages.create()` directly, use `output_config: {format: {...}}`. The `output_format` convenience parameter is also accepted by some SDK methods (e.g., `.parse()`), but `output_config.format` is the canonical API-level parameter.
### JSON Schema Limitations
**Supported:**
- Basic types: object, array, string, integer, number, boolean, null
- `enum`, `const`, `anyOf`, `allOf`, `$ref`/`$def`
- String formats: `date-time`, `time`, `date`, `duration`, `email`, `hostname`, `uri`, `ipv4`, `ipv6`, `uuid`
- `additionalProperties: false` (required for all objects)
**Not supported:**
- Recursive schemas
- Numerical constraints (`minimum`, `maximum`, `multipleOf`)
- String constraints (`minLength`, `maxLength`)
- Complex array constraints
- `additionalProperties` set to anything other than `false`
The Python and TypeScript SDKs automatically handle unsupported constraints by removing them from the schema sent to the API and validating them client-side.
### Important Notes
- **First request latency**: New schemas incur a one-time compilation cost. Subsequent requests with the same schema use a 24-hour cache.
- **Refusals**: If Claude refuses for safety reasons (`stop_reason: "refusal"`), the output may not match your schema.
- **Token limits**: If `stop_reason: "max_tokens"`, output may be incomplete. Increase `max_tokens`.
- **Incompatible with**: Citations (returns 400 error), message prefilling.
- **Works with**: Batches API, streaming, token counting, extended thinking.
---
## Tips for Effective Tool Use
1. **Provide detailed descriptions**: Claude relies heavily on descriptions to understand when and how to use tools
2. **Use specific tool names**: `get_current_weather` is better than `weather`
3. **Validate inputs**: Always validate tool inputs before execution
4. **Handle errors gracefully**: Return informative error messages so Claude can adapt
5. **Limit tool count**: Too many tools can confuse the model - keep the set focused
6. **Test tool interactions**: Verify Claude uses tools correctly in various scenarios
For detailed tool use documentation, use WebFetch:
- URL: `https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview`
# Claude API - TypeScript
| Feature | Namespace | Key types / call |
|---|---|---|
| User profiles | beta | `client.beta.userProfiles.create(...)` / `.retrieve(id)` / `.list()`. Pass the returned profile id on `client.beta.messages.create`. Requires a beta header - check the SDK's beta-headers reference for the current flag. |
## Installation
```bash
npm install @anthropic-ai/sdk
```
> **Reading local files (ESM):** `__dirname` and `__filename` are **undefined** in ES modules - using either throws `ReferenceError: __dirname is not defined` at runtime. For cwd-relative reads, pass the bare relative path (`fs.readFileSync("./sample.png")`). For script-relative paths, derive the directory from `import.meta.url`: `const here = path.dirname(fileURLToPath(import.meta.url))`. Never write `path.join(__dirname, ...)` in an ESM `.ts` file.
## Client Initialization
```typescript
import Anthropic from "@anthropic-ai/sdk";
// Default - resolves credentials from the environment:
// ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN, or an `ant auth login` profile.
// Prefer this for local dev; don't hardcode a key.
const client = new Anthropic();
// Explicit API key (only when you must inject a specific key)
const client = new Anthropic({ apiKey: "your-api-key" });
```
---
## Basic Message Request
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [{ role: "user", content: "What is the capital of France?" }],
});
// response.content is ContentBlock[] - a discriminated union. Narrow by .type
// before accessing .text (TypeScript will error on content[0].text without this).
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
```
---
## System Prompts
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system:
"You are a helpful coding assistant. Always provide examples in Python.",
messages: [{ role: "user", content: "How do I read a JSON file?" }],
});
```
### Mid-conversation system messages (model-gated)
For operator instructions that arrive mid-conversation (mode switches, injected state), append `{role: "system", ...}` to `messages` instead of editing top-level `system` - this preserves the cached prefix and carries operator authority. Must follow a user message (or an `assistant` message ending in server-tool use), and must be either the last entry in `messages` or be followed by an `assistant` turn; cannot be `messages[0]`. Unsupported models return a 400 (`role 'system' is not supported on this model`). See `shared/prompt-caching.md` for when to use this vs. top-level `system`.
```typescript
// No beta header needed - use regular client.messages.create.
const response = await client.messages.create({
model: MODEL_ID, // must support mid-conversation system messages
max_tokens: 16000,
system: [
{ type: "text", text: STABLE_SYSTEM, cache_control: { type: "ephemeral" } },
],
messages: [
...history,
{ role: "user", content: userMessage },
{ role: "system", content: "Terse mode enabled - keep responses under 40 words." },
],
});
```
---
## Vision (Images)
### URL
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: [
{
type: "image",
source: { type: "url", url: "https://example.com/image.png" },
},
{ type: "text", text: "Describe this image" },
],
},
],
});
```
### Base64
```typescript
import fs from "fs";
const imageData = fs.readFileSync("image.png").toString("base64");
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: [
{
type: "image",
source: { type: "base64", media_type: "image/png", data: imageData },
},
{ type: "text", text: "What's in this image?" },
],
},
],
});
```
---
## Prompt Caching
**Caching is a prefix match** - any byte change anywhere in the prefix invalidates everything after it. For placement patterns, architectural guidance (frozen system prompt, deterministic tool order, where to put volatile content), and the silent-invalidator audit checklist, read `shared/prompt-caching.md`.
### Automatic Caching (Recommended)
Use top-level `cache_control` to automatically cache the last cacheable block in the request:
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
cache_control: { type: "ephemeral" }, // auto-caches the last cacheable block
system: "You are an expert on this large document...",
messages: [{ role: "user", content: "Summarize the key points" }],
});
```
### Manual Cache Control
For fine-grained control, add `cache_control` to specific content blocks:
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system: [
{
type: "text",
text: "You are an expert on this large document...",
cache_control: { type: "ephemeral" }, // default TTL is 5 minutes
},
],
messages: [{ role: "user", content: "Summarize the key points" }],
});
// With explicit TTL (time-to-live)
const response2 = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system: [
{
type: "text",
text: "You are an expert on this large document...",
cache_control: { type: "ephemeral", ttl: "1h" }, // 1 hour TTL
},
],
messages: [{ role: "user", content: "Summarize the key points" }],
});
```
### Verifying Cache Hits
```typescript
console.log(response.usage.cache_creation_input_tokens); // tokens written to cache (~1.25x cost)
console.log(response.usage.cache_read_input_tokens); // tokens served from cache (~0.1x cost)
console.log(response.usage.input_tokens); // uncached tokens (full cost)
```
If `cache_read_input_tokens` is zero across repeated identical-prefix requests, a silent invalidator is at work - `Date.now()` or a UUID in the system prompt, non-deterministic key ordering, or a varying tool set. See `shared/prompt-caching.md` for the full audit table.
---
## Extended Thinking
> **Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking. `budget_tokens` is removed on Fable 5, Claude Opus 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6.
> **Claude Opus 5:** thinking is on by default - omitting `thinking` runs adaptive (`{ type: "adaptive" }` is equivalent), unlike Opus 4.8/4.7 where omitting it meant no thinking. `{ type: "disabled" }` is accepted only at effort `high` or lower; pairing it with `xhigh`/`max` returns a 400.
> **Older models:** Use `thinking: {type: "enabled", budget_tokens: N}` (must be < `max_tokens`, min 1024).
```typescript
// Fable 5 / Claude Opus 5 / Opus 4.8 / 4.7 / 4.6: adaptive thinking (recommended)
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
thinking: { type: "adaptive", display: "summarized" }, // display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Claude Opus 5 / Opus 4.8 / 4.7
output_config: { effort: "high" }, // low | medium | high | xhigh | max
messages: [
{ role: "user", content: "Solve this math problem step by step..." },
],
});
for (const block of response.content) {
if (block.type === "thinking") {
console.log("Thinking:", block.thinking);
} else if (block.type === "text") {
console.log("Response:", block.text);
}
}
```
---
## Error Handling
Use the SDK's typed exception classes - never check error messages with string matching:
```typescript
import Anthropic from "@anthropic-ai/sdk";
try {
const response = await client.messages.create({...});
} catch (error) {
if (error instanceof Anthropic.BadRequestError) {
console.error("Bad request:", error.message);
} else if (error instanceof Anthropic.AuthenticationError) {
console.error("Invalid API key");
} else if (error instanceof Anthropic.RateLimitError) {
console.error("Rate limited - retry later");
} else if (error instanceof Anthropic.APIError) {
console.error(`API error ${error.status}:`, error.message);
}
}
```
All classes extend `Anthropic.APIError` with a typed `status` field. Check from most specific to least specific. See [shared/error-codes.md](../../shared/error-codes.md) for the full error code reference.
---
## Multi-Turn Conversations
The API is stateless - send the full conversation history each time. Use `Anthropic.MessageParam[]` to type the messages array:
```typescript
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "My name is Alice." },
{ role: "assistant", content: "Hello Alice! Nice to meet you." },
{ role: "user", content: "What's my name?" },
];
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: messages,
});
```
**Rules:**
- Consecutive same-role messages are allowed - the API combines them into a single turn
- First message must be `user`
- Use SDK types (`Anthropic.MessageParam`, `Anthropic.Message`, `Anthropic.Tool`, etc.) for all API data structures - don't redefine equivalent interfaces
---
### Compaction (long conversations)
> **Beta, Fable 5, Claude Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6.** When conversations approach the 200K context window, compaction automatically summarizes earlier context server-side. The API returns a `compaction` block; you must pass it back on subsequent requests - append `response.content`, not just the text.
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const messages: Anthropic.Beta.BetaMessageParam[] = [];
async function chat(userMessage: string): Promise<string> {
messages.push({ role: "user", content: userMessage });
const response = await client.beta.messages.create({
betas: ["compact-2026-01-12"],
model: "claude-opus-5",
max_tokens: 16000,
messages,
context_management: {
edits: [{ type: "compact_20260112" }],
},
});
// Append full content - compaction blocks must be preserved
messages.push({ role: "assistant", content: response.content });
const textBlock = response.content.find(
(b): b is Anthropic.Beta.BetaTextBlock => b.type === "text",
);
return textBlock?.text ?? "";
}
// Compaction triggers automatically when context grows large
console.log(await chat("Help me build a Python web scraper"));
console.log(await chat("Add support for JavaScript-rendered pages"));
console.log(await chat("Now add rate limiting and error handling"));
```
---
## Stop Reasons
The `stop_reason` field in the response indicates why the model stopped generating:
| Value | Meaning |
| --------------- | --------------------------------------------------------------- |
| `end_turn` | Claude finished its response naturally |
| `max_tokens` | Hit the `max_tokens` limit - increase it or use streaming |
| `stop_sequence` | Hit a custom stop sequence |
| `tool_use` | Claude wants to call a tool - execute it and continue |
| `pause_turn` | Model paused and can be resumed (agentic flows) |
| `refusal` | Claude refused for safety reasons - check `stop_details` |
### Structured Stop Details
When `stop_reason` is `"refusal"`, the response includes a `stop_details` object with structured information about the refusal:
```typescript
if (response.stop_reason === "refusal" && response.stop_details) {
console.log(`Category: ${response.stop_details.category}`); // e.g. "cyber", "bio", "reasoning_extraction", "frontier_llm", or null - see docs for the full set
console.log(`Explanation: ${response.stop_details.explanation}`);
}
```
### Refusal Fallbacks (Claude Fable 5.1) - opt in by default
Fallbacks are **opt-in**: without them a refused request simply stops. Include the server-side `fallbacks` parameter in `claude-fable-5-1` code by default - on a policy decline the API re-runs the same request on the fallback model inside the same call. A decline before any output isn't billed (a mid-stream decline bills the streamed partial); the rescue bills at the fallback model's own rates, with cache repricing applied automatically.
```typescript
const response = await client.beta.messages.create({
model: "claude-fable-5-1",
max_tokens: 16000,
betas: ["server-side-fallback-2026-06-01"],
fallbacks: [{ model: "claude-opus-4-8" }],
messages: [{ role: "user", content: "..." }],
});
// Switch points: one fallback block per model that ran and declined this turn
for (const block of response.content) {
if (block.type === "fallback") {
console.log(`${block.from.model} declined; ${block.to.model} continued`);
}
}
// Served-by signal - covers sticky turns, which carry no fallback block.
// Pair with stop_reason: the fallback model can itself refuse.
const fallbackRan = (response.usage.iterations ?? []).some(
(entry) => entry.type === "fallback_message",
);
if (fallbackRan && response.stop_reason !== "refusal") {
console.log(`Served by ${response.model}`);
}
```
A `stop_reason: "refusal"` on the final response means the whole chain refused. The header must be exactly `server-side-fallback-2026-06-01` **for this array form**; the newer `fallbacks: "default"` scalar form uses `server-side-fallback-2026-07-01` instead (see `shared/model-migration.md` -> Migrating to Claude Opus 5 -> New API features), and pairing either header with the other form returns a 400. The parameter is rejected on the Batches API and unavailable on Amazon Bedrock, Vertex AI, and Microsoft Foundry - register the client-side `betaRefusalFallbackMiddleware` on the client there instead. Full semantics (sticky routing, billing, streaming, echoing fallback turns back): `shared/model-migration.md` -> Migrating to Claude Fable 5.1 -> `refusal` stop reason.
---
## Cost Optimization Strategies
### 1. Use Prompt Caching for Repeated Context
```typescript
// Automatic caching (simplest - caches the last cacheable block)
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
cache_control: { type: "ephemeral" },
system: largeDocumentText, // e.g., 50KB of context
messages: [{ role: "user", content: "Summarize the key points" }],
});
// First request: full cost
// Subsequent requests: ~90% cheaper for cached portion
```
### 2. Use Token Counting Before Requests
```typescript
const countResponse = await client.messages.countTokens({
model: "claude-opus-5",
messages: messages,
system: system,
});
const estimatedInputCost = countResponse.input_tokens * 0.000005; // $5/1M tokens
console.log(`Estimated input cost: $${estimatedInputCost.toFixed(4)}`);
```
# Message Batches API - TypeScript
The Batches API (`POST /v1/messages/batches`) processes Messages API requests asynchronously at 50% of standard prices.
## Key Facts
- Up to 100,000 requests or 256 MB per batch
- Most batches complete within 1 hour; maximum 24 hours
- Results available for 29 days after creation
- 50% cost reduction on all token usage
- All Messages API features supported (vision, tools, caching, etc.)
---
## Create a Batch
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const messageBatch = await client.messages.batches.create({
requests: [
{
custom_id: "request-1",
params: {
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{ role: "user", content: "Summarize climate change impacts" },
],
},
},
{
custom_id: "request-2",
params: {
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{ role: "user", content: "Explain quantum computing basics" },
],
},
},
],
});
console.log(`Batch ID: ${messageBatch.id}`);
console.log(`Status: ${messageBatch.processing_status}`);
```
---
## Poll for Completion
```typescript
let batch;
while (true) {
batch = await client.messages.batches.retrieve(messageBatch.id);
if (batch.processing_status === "ended") break;
console.log(
`Status: ${batch.processing_status}, processing: ${batch.request_counts.processing}`,
);
await new Promise((resolve) => setTimeout(resolve, 60_000));
}
console.log("Batch complete!");
console.log(`Succeeded: ${batch.request_counts.succeeded}`);
console.log(`Errored: ${batch.request_counts.errored}`);
```
---
## Retrieve Results
```typescript
for await (const result of await client.messages.batches.results(
messageBatch.id,
)) {
switch (result.result.type) {
case "succeeded":
console.log(
`[${result.custom_id}] ${result.result.message.content[0].text.slice(0, 100)}`,
);
break;
case "errored":
if (result.result.error.type === "invalid_request") {
console.log(`[${result.custom_id}] Validation error - fix and retry`);
} else {
console.log(`[${result.custom_id}] Server error - safe to retry`);
}
break;
case "expired":
console.log(`[${result.custom_id}] Expired - resubmit`);
break;
}
}
```
---
## Cancel a Batch
```typescript
const cancelled = await client.messages.batches.cancel(messageBatch.id);
console.log(`Status: ${cancelled.processing_status}`); // "canceling"
```
# Files API - TypeScript
The Files API uploads files for use in Messages API requests. Reference files via `file_id` in content blocks, avoiding re-uploads across multiple API calls.
The Files API is out of beta. In current SDKs `client.beta.files` has breaking shape changes from previous versions, matching the stable `client.files` - migrate per the Files API row in `shared/live-sources.md`. Examples below predate this.
## Key Facts
- Maximum file size: 500 MB
- Total storage: 100 GB per organization
- Files persist until deleted
- File operations (upload, list, delete) are free; content used in messages is billed as input tokens
- Not available on Amazon Bedrock or Google Vertex AI
---
## Upload a File
```typescript
import Anthropic, { toFile } from "@anthropic-ai/sdk";
import fs from "fs";
const client = new Anthropic();
const uploaded = await client.beta.files.upload({
file: await toFile(fs.createReadStream("report.pdf"), undefined, {
type: "application/pdf",
}),
betas: ["files-api-2025-04-14"],
});
console.log(`File ID: ${uploaded.id}`);
console.log(`Size: ${uploaded.size_bytes} bytes`);
```
---
## Use a File in Messages
### PDF / Text Document
```typescript
const response = await client.beta.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: [
{ type: "text", text: "Summarize the key findings in this report." },
{
type: "document",
source: { type: "file", file_id: uploaded.id },
title: "Q4 Report",
citations: { enabled: true },
},
],
},
],
betas: ["files-api-2025-04-14"],
});
console.log(response.content[0].text);
```
---
## Manage Files
### List Files
```typescript
const files = await client.beta.files.list({
betas: ["files-api-2025-04-14"],
});
for (const f of files.data) {
console.log(`${f.id}: ${f.filename} (${f.size_bytes} bytes)`);
}
```
### Delete a File
```typescript
await client.beta.files.delete("file_011CNha8iCJcU1wXNR6q4V8w", {
betas: ["files-api-2025-04-14"],
});
```
### Download a File
```typescript
const response = await client.beta.files.download(
"file_011CNha8iCJcU1wXNR6q4V8w",
{ betas: ["files-api-2025-04-14"] },
);
const content = Buffer.from(await response.arrayBuffer());
await fs.promises.writeFile("output.txt", content);
```
# Streaming - TypeScript
## Quick Start
```typescript
const stream = client.messages.stream({
model: "claude-opus-5",
max_tokens: 64000,
messages: [{ role: "user", content: "Write a story" }],
});
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
process.stdout.write(event.delta.text);
}
}
```
---
## Handling Different Content Types
> **Fable 5 / Claude Opus 5 / Opus 4.8 / Opus 4.7 / Opus 4.6:** Use `thinking: {type: "adaptive"}`. On Claude Opus 5 adaptive is also what you get by omitting `thinking` entirely. On older models, use `thinking: {type: "enabled", budget_tokens: N}` instead.
```typescript
const stream = client.messages.stream({
model: "claude-opus-5",
max_tokens: 64000,
thinking: { type: "adaptive", display: "summarized" }, // display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Claude Opus 5 / Opus 4.8 / 4.7
messages: [{ role: "user", content: "Analyze this problem" }],
});
for await (const event of stream) {
switch (event.type) {
case "content_block_start":
switch (event.content_block.type) {
case "thinking":
console.log("\n[Thinking...]");
break;
case "text":
console.log("\n[Response:]");
break;
}
break;
case "content_block_delta":
switch (event.delta.type) {
case "thinking_delta":
process.stdout.write(event.delta.thinking);
break;
case "text_delta":
process.stdout.write(event.delta.text);
break;
}
break;
}
}
```
---
## Streaming with Tool Use (Tool Runner)
Use the tool runner with `stream: true`. The outer loop iterates over tool runner iterations (messages), the inner loop processes stream events:
```typescript
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const getWeather = betaZodTool({
name: "get_weather",
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City and state, e.g., San Francisco, CA"),
}),
run: async ({ location }) => `72°F and sunny in ${location}`,
});
const runner = client.beta.messages.toolRunner({
model: "claude-opus-5",
max_tokens: 64000,
tools: [getWeather],
messages: [
{ role: "user", content: "What's the weather in Paris and London?" },
],
stream: true,
});
// Outer loop: each tool runner iteration
for await (const messageStream of runner) {
// Inner loop: stream events for this iteration
for await (const event of messageStream) {
switch (event.type) {
case "content_block_delta":
switch (event.delta.type) {
case "text_delta":
process.stdout.write(event.delta.text);
break;
case "input_json_delta":
// Tool input being streamed
break;
}
break;
}
}
}
```
---
## Getting the Final Message
```typescript
const stream = client.messages.stream({
model: "claude-opus-5",
max_tokens: 64000,
messages: [{ role: "user", content: "Hello" }],
});
for await (const event of stream) {
// Process events...
}
const finalMessage = await stream.finalMessage();
console.log(`Tokens used: ${finalMessage.usage.output_tokens}`);
```
---
## Stream Event Types
| Event Type | Description | When it fires |
| --------------------- | --------------------------- | --------------------------------- |
| `message_start` | Contains message metadata | Once at the beginning |
| `content_block_start` | New content block beginning | When a text/tool_use block starts |
| `content_block_delta` | Incremental content update | For each token/chunk |
| `content_block_stop` | Content block complete | When a block finishes |
| `message_delta` | Message-level updates | Contains `stop_reason`, usage |
| `message_stop` | Message complete | Once at the end |
## Best Practices
1. **Always flush output** - Use `process.stdout.write()` for immediate display
2. **Handle partial responses** - If the stream is interrupted, you may have incomplete content
3. **Track token usage** - The `message_delta` event contains usage information
4. **Use `finalMessage()`** - Get the complete `Anthropic.Message` object even when streaming. Don't wrap `.on()` events in `new Promise()` - `finalMessage()` handles all completion/error/abort states internally
5. **Buffer for web UIs** - Consider buffering a few tokens before rendering to avoid excessive DOM updates
6. **Use `stream.on("text", ...)` for deltas** - The `text` event provides just the delta string, simpler than manually filtering `content_block_delta` events
7. **For agentic loops with streaming** - See the [Streaming Manual Loop](./tool-use.md#streaming-manual-loop) section in tool-use.md for combining `stream()` + `finalMessage()` with a tool-use loop
## Raw SSE Format
If using raw HTTP (not SDKs), the stream returns Server-Sent Events:
```
event: message_start
data: {"type":"message_start","message":{"id":"msg_...","type":"message",...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}}
event: message_stop
data: {"type":"message_stop"}
```
# Tool Use - TypeScript
For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md).
## Tool Runner (Recommended)
**Beta:** The tool runner is in beta in the TypeScript SDK.
Use `betaZodTool` with Zod schemas to define tools with a `run` function, then pass them to `client.beta.messages.toolRunner()`:
```typescript
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const getWeather = betaZodTool({
name: "get_weather",
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City and state, e.g., San Francisco, CA"),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
run: async (input) => {
// Your implementation here
return `72°F and sunny in ${input.location}`;
},
});
// The tool runner handles the agentic loop and returns the final message
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-5",
max_tokens: 16000,
tools: [getWeather],
messages: [{ role: "user", content: "What's the weather in Paris?" }],
});
console.log(finalMessage.content);
```
Zod is optional - `betaTool()` from `@anthropic-ai/sdk/helpers/beta/json-schema` accepts a raw JSON Schema `inputSchema` plus a `run` function if you don't want a Zod dependency.
**Key benefits of the tool runner:**
- No manual loop - the SDK handles calling tools and feeding results back
- Type-safe tool inputs via Zod schemas (or raw JSON Schema via `betaTool()`)
- Tool schemas are generated automatically from Zod definitions
- Iteration stops automatically when Claude has no more tool calls
### Server tools with the tool runner
The runner's `tools` array accepts raw server-tool definitions (`web_search_20260209`, `web_fetch_20260209`, code execution) alongside runnable tools - pass the literal tool object; server tools run on Anthropic's servers, so there is no `run` function.
**Caution - the runner does not auto-resume `pause_turn` (as of `@anthropic-ai/sdk` 0.110.0).** A long-running server-tool turn can stop with `stop_reason: "pause_turn"`. The runner only continues after a client tool produces a result, so a paused turn ends the loop and is returned as the final message - no error, no warning, just a silently truncated answer. If you mix server tools into the runner, check `stop_reason` on every iteration and resume by pushing the paused assistant turn back:
```typescript
const params = {
model: "claude-opus-5",
max_tokens: 16000,
tools: [getWeather, { type: "web_search_20260209", name: "web_search", max_uses: 5 }],
messages: [{ role: "user", content: "Compare this week's forecasts for Paris across two sources" }],
};
const runner = client.beta.messages.toolRunner(params);
// Non-streaming: each iteration yields a complete message
for await (const message of runner) {
if (message.stop_reason === "pause_turn") {
runner.pushMessages({ role: "assistant", content: message.content });
}
}
// Streaming alternative - construct the runner with `stream: true` (same
// params as above). Each iteration then yields a stream, not a message - a
// bare `message.stop_reason` check never fires. Resolve the stream first:
const streamingRunner = client.beta.messages.toolRunner({ ...params, stream: true });
for await (const stream of streamingRunner) {
const message = await stream.finalMessage();
if (message.stop_reason === "pause_turn") {
streamingRunner.pushMessages({ role: "assistant", content: message.content });
}
}
```
Each pause-resume consumes a `max_iterations` tick, so a capped run can still end paused - check the final message's `stop_reason` before trusting the result (after the loop, call `.done()` on the runner you iterated to get the final message). Alternatively, use the manual loop below, which handles `pause_turn` explicitly.
---
## Manual Agentic Loop
Prefer the tool runner above. Drop to a manual loop only when you need control the runner does not expose (e.g., a custom transport, request shapes the SDK cannot build, or avoiding a beta dependency - the runner is beta, and it supports per-token streaming via `stream: true`). Human-in-the-loop approval does *not* require a manual loop - gate inside the tool's `run()` function (return a "user declined" result) or inspect pending `tool_use` blocks and call `setMessagesParams()` between iterations.
If you do need a manual loop:
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [...]; // Your tool definitions
let messages: Anthropic.MessageParam[] = [{ role: "user", content: userInput }];
while (true) {
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
tools: tools,
messages: messages,
});
if (response.stop_reason === "end_turn") break;
// Server-side tool hit iteration limit; append assistant turn and re-send to continue
if (response.stop_reason === "pause_turn") {
messages.push({ role: "assistant", content: response.content });
continue;
}
const toolUseBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const tool of toolUseBlocks) {
const result = await executeTool(tool.name, tool.input);
toolResults.push({
type: "tool_result",
tool_use_id: tool.id,
content: result,
});
}
messages.push({ role: "user", content: toolResults });
}
```
### Streaming Manual Loop
Use `client.messages.stream()` + `finalMessage()` instead of `.create()` when you need streaming within a manual loop. Text deltas are streamed on each iteration; `finalMessage()` collects the complete `Message` so you can inspect `stop_reason` and extract tool-use blocks:
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [...];
let messages: Anthropic.MessageParam[] = [{ role: "user", content: userInput }];
while (true) {
const stream = client.messages.stream({
model: "claude-opus-5",
max_tokens: 64000,
tools,
messages,
});
// Stream text deltas on each iteration
stream.on("text", (delta) => {
process.stdout.write(delta);
});
// finalMessage() resolves with the complete Message - no need to
// manually wire up .on("message") / .on("error") / .on("abort")
const message = await stream.finalMessage();
if (message.stop_reason === "end_turn") break;
// Server-side tool hit iteration limit; append assistant turn and re-send to continue
if (message.stop_reason === "pause_turn") {
messages.push({ role: "assistant", content: message.content });
continue;
}
const toolUseBlocks = message.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
messages.push({ role: "assistant", content: message.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const tool of toolUseBlocks) {
const result = await executeTool(tool.name, tool.input);
toolResults.push({
type: "tool_result",
tool_use_id: tool.id,
content: result,
});
}
messages.push({ role: "user", content: toolResults });
}
```
> **Important:** Don't wrap `.on()` events in `new Promise()` to collect the final message - use `stream.finalMessage()` instead. The SDK handles all error/abort/completion states internally.
> **Error handling in the loop:** Use the SDK's typed exceptions (e.g., `Anthropic.RateLimitError`, `Anthropic.APIError`) - see [Error Handling](./README.md#error-handling) for examples. Don't check error messages with string matching.
> **SDK types:** Use `Anthropic.MessageParam`, `Anthropic.Tool`, `Anthropic.ToolUseBlock`, `Anthropic.ToolResultBlockParam`, `Anthropic.Message`, etc. for all API-related data structures. Don't redefine equivalent interfaces.
---
## Handling Tool Results
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
tools: tools,
messages: [{ role: "user", content: "What's the weather in Paris?" }],
});
for (const block of response.content) {
if (block.type === "tool_use") {
const result = await executeTool(block.name, block.input);
const followup = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
tools: tools,
messages: [
{ role: "user", content: "What's the weather in Paris?" },
{ role: "assistant", content: response.content },
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: block.id, content: result },
],
},
],
});
}
}
```
---
## Tool Choice
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
tools: tools,
tool_choice: { type: "tool", name: "get_weather" },
messages: [{ role: "user", content: "What's the weather in Paris?" }],
});
```
---
## Anthropic-Defined Tools
Version-suffixed `type` literals; `name` is fixed per interface. Web search and code execution are server-executed; bash and text editor are client-executed (you handle the `tool_use` locally - see `shared/tool-use-concepts.md`). Pass plain object literals - the `ToolUnion` type is satisfied structurally. **The `name`/`type` pair must match the interface**: mixing `str_replace_based_edit_tool` (20250728 name) with `text_editor_20250124` (which expects `str_replace_editor`) is a TS2322.
**Don't type-annotate as `Tool[]`** - `Tool` is just the custom-tool variant. Let structural typing infer from the `tools` param, or annotate as `Anthropic.Messages.ToolUnion[]` if you must:
```typescript
// Good: let inference work - no annotation
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
tools: [
{ type: "text_editor_20250728", name: "str_replace_based_edit_tool" },
{ type: "bash_20250124", name: "bash" },
{ type: "web_search_20260209", name: "web_search" },
{ type: "code_execution_20260120", name: "code_execution" },
],
messages: [{ role: "user", content: "..." }],
});
// Bad: this is a TS2352 - Tool is the CUSTOM tool variant only
// const tools: Anthropic.Tool[] = [{ type: "text_editor_20250728", ... }]
```
| Interface | `name` | `type` |
|---|---|---|
| `ToolTextEditor20250124` | `str_replace_editor` | `text_editor_20250124` |
| `ToolTextEditor20250429` | `str_replace_based_edit_tool` | `text_editor_20250429` |
| `ToolTextEditor20250728` | `str_replace_based_edit_tool` | `text_editor_20250728` |
| `ToolBash20250124` | `bash` | `bash_20250124` |
| `WebSearchTool20260209` | `web_search` | `web_search_20260209` |
| `WebFetchTool20260209` | `web_fetch` | `web_fetch_20260209` |
| `CodeExecutionTool20260120` | `code_execution` | `code_execution_20260120` |
**Don't mix beta and non-beta types**: if you call `client.beta.messages.create()`, the response `content` is `BetaContentBlock[]` - you cannot pass that to a non-beta `ContentBlockParam[]` without narrowing each element.
---
## Code Execution
### Basic Usage
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content:
"Calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]",
},
],
tools: [{ type: "code_execution_20260120", name: "code_execution" }],
});
```
### Reading Local Files (ESM note)
`__dirname` doesn't exist in ES modules. For script-relative paths use `import.meta.url`:
```typescript
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const pdfBytes = readFileSync(join(__dirname, "sample.pdf"));
```
Or use a CWD-relative path if the script runs from a known directory: `readFileSync("./sample.pdf")`.
### Upload Files for Analysis
```typescript
import Anthropic, { toFile } from "@anthropic-ai/sdk";
import { createReadStream } from "fs";
const client = new Anthropic();
// 1. Upload a file
const uploaded = await client.beta.files.upload({
file: await toFile(createReadStream("sales_data.csv"), undefined, {
type: "text/csv",
}),
});
// 2. Pass to code execution
const response = await client.messages.create(
{
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: [
{
type: "text",
text: "Analyze this sales data. Show trends and create a visualization.",
},
{ type: "container_upload", file_id: uploaded.id },
],
},
],
tools: [{ type: "code_execution_20260120", name: "code_execution" }],
},
);
```
### Retrieve Generated Files
```typescript
import path from "path";
import fs from "fs";
const OUTPUT_DIR = "./claude_outputs";
await fs.promises.mkdir(OUTPUT_DIR, { recursive: true });
for (const block of response.content) {
if (block.type === "bash_code_execution_tool_result") {
const result = block.content;
if (result.type === "bash_code_execution_result" && result.content) {
for (const fileRef of result.content) {
if (fileRef.type === "bash_code_execution_output") {
const metadata = await client.beta.files.retrieveMetadata(
fileRef.file_id,
);
const downloadResponse = await client.beta.files.download(fileRef.file_id);
const fileBytes = Buffer.from(await downloadResponse.arrayBuffer());
const safeName = path.basename(metadata.filename);
if (!safeName || safeName === "." || safeName === "..") {
console.warn(`Skipping invalid filename: ${metadata.filename}`);
continue;
}
const outputPath = path.join(OUTPUT_DIR, safeName);
await fs.promises.writeFile(outputPath, fileBytes);
console.log(`Saved: ${outputPath}`);
}
}
}
}
}
```
### Container Reuse
```typescript
// First request: set up environment
const response1 = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: "Install tabulate and create data.json with sample user data",
},
],
tools: [{ type: "code_execution_20260120", name: "code_execution" }],
});
// Reuse container
// container is nullable - set only when using server-side code execution
const containerId = response1.container!.id;
const response2 = await client.messages.create({
container: containerId,
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: "Read data.json and display as a formatted table",
},
],
tools: [{ type: "code_execution_20260120", name: "code_execution" }],
});
```
---
## Memory Tool
### Basic Usage
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: "Remember that my preferred language is TypeScript.",
},
],
tools: [{ type: "memory_20250818", name: "memory" }],
});
```
### SDK Memory Helper
Use `betaMemoryTool` with a `MemoryToolHandlers` implementation:
```typescript
import {
betaMemoryTool,
type MemoryToolHandlers,
} from "@anthropic-ai/sdk/helpers/beta/memory";
const handlers: MemoryToolHandlers = {
async view(command) { ... },
async create(command) { ... },
async str_replace(command) { ... },
async insert(command) { ... },
async delete(command) { ... },
async rename(command) { ... },
};
const memory = betaMemoryTool(handlers);
const runner = client.beta.messages.toolRunner({
model: "claude-opus-5",
max_tokens: 16000,
tools: [memory],
messages: [{ role: "user", content: "Remember my preferences" }],
});
for await (const message of runner) {
console.log(message);
}
```
For full implementation examples, use WebFetch:
- `https://github.com/anthropics/anthropic-sdk-typescript/blob/main/examples/tools-helpers-memory.ts`
---
## Structured Outputs
### JSON Outputs (Zod - Recommended)
```typescript
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
const ContactInfoSchema = z.object({
name: z.string(),
email: z.string(),
plan: z.string(),
interests: z.array(z.string()),
demo_requested: z.boolean(),
});
const client = new Anthropic();
const response = await client.messages.parse({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content:
"Extract: Jane Doe ([email protected]) wants Enterprise, interested in API and SDKs, wants a demo.",
},
],
output_config: {
format: zodOutputFormat(ContactInfoSchema),
},
});
// parsed_output is null if parsing failed - assert or guard
console.log(response.parsed_output!.name); // "Jane Doe"
```
### Strict Tool Use
```typescript
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
messages: [
{
role: "user",
content: "Book a flight to Tokyo for 2 passengers on March 15",
},
],
tools: [
{
name: "book_flight",
description: "Book a flight to a destination",
strict: true,
input_schema: {
type: "object",
properties: {
destination: { type: "string" },
date: { type: "string", format: "date" },
passengers: {
type: "integer",
enum: [1, 2, 3, 4, 5, 6, 7, 8],
},
},
required: ["destination", "date", "passengers"],
additionalProperties: false,
},
},
],
});
```
---
## Agent Skills
Enable an Anthropic-managed skill (e.g., `pptx`) via `container.skills` + the `code_execution` tool on the beta path. Both beta headers are required. Outputs land as files in the response content - download by file ID via the Files API.
```typescript
const response = await client.beta.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
container: {
skills: [{ type: "anthropic", skill_id: "pptx", version: "latest" }],
},
tools: [{ type: "code_execution_20260521", name: "code_execution" }],
betas: ["code-execution-2025-08-25"],
messages: [{ role: "user", content: "Create a 3-slide deck about X." }],
});
// Find the file_id in response.content, then:
// await client.beta.files.download(fileId)
```
# Managed Agents - TypeScript
> **Bindings not shown here:** This README covers the most common managed-agents flows for TypeScript. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the TypeScript SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK.
> **Agents are persistent - create once, reference by ID.** Store the agent ID returned by `agents.create` and pass it to every subsequent `sessions.create`; do not call `agents.create` in the request path. **Recommended:** define agents and environments as version-controlled YAML applied with the `ant` CLI - see `shared/anthropic-cli.md` (its live-docs URL is in `shared/live-sources.md`). The CLI owns the control plane (create/update); your code owns the data plane (sessions with the stored ID). The examples below show in-code creation for when you must provision programmatically; in production the create call belongs in setup, not in the request path.
## Installation
```bash
npm install @anthropic-ai/sdk
```
## Client Initialization
```typescript
import Anthropic from "@anthropic-ai/sdk";
// Default - resolves credentials from the environment:
// ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN, or an `ant auth login` profile.
// Prefer this for local dev; don't hardcode a key.
const client = new Anthropic();
// Explicit API key (only when you must inject a specific key)
const client = new Anthropic({ apiKey: "your-api-key" });
```
---
## Create an Environment
```typescript
const environment = await client.beta.environments.create(
{
name: "my-dev-env",
config: {
type: "cloud",
networking: { type: "unrestricted" },
},
},
);
console.log(environment.id); // env_...
```
---
## Create an Agent (required first step)
> Warning: **There is no inline agent config.** `model`/`system`/`tools` live on the agent object, not the session. Always start with `agents.create()` - the session only takes `agent: { type: "agent", id: agent.id }`.
### Minimal
```typescript
// 1. Create the agent (reusable, versioned)
const agent = await client.beta.agents.create(
{
name: "Coding Assistant",
model: "claude-opus-5",
tools: [{ type: "agent_toolset_20260401", default_config: { enabled: true } }],
},
);
// 2. Start a session
const session = await client.beta.sessions.create(
{
agent: { type: "agent", id: agent.id, version: agent.version },
environment_id: environment.id,
},
);
console.log(session.id, session.status);
console.log(`Trace: https://platform.claude.com/workspaces/default/sessions/${session.id}`); // swap 'default' for your workspace ID if the API key is not in the Default workspace
```
### With system prompt and custom tools
```typescript
const agent = await client.beta.agents.create(
{
name: "Code Reviewer",
model: "claude-opus-5",
system: "You are a senior code reviewer.",
tools: [
{ type: "agent_toolset_20260401", default_config: { enabled: true } },
{
type: "custom",
name: "run_tests",
description: "Run the test suite",
input_schema: {
type: "object",
properties: {
test_path: { type: "string", description: "Path to test file" },
},
required: ["test_path"],
},
},
],
},
);
const session = await client.beta.sessions.create(
{
agent: { type: "agent", id: agent.id, version: agent.version },
environment_id: environment.id,
title: "Code review session",
resources: [
{
type: "github_repository",
url: "https://github.com/owner/repo",
mount_path: "/workspace/repo",
authorization_token: process.env.GITHUB_TOKEN,
branch: "main",
},
],
},
);
```
---
## Send a User Message
```typescript
await client.beta.sessions.events.send(
session.id,
{
events: [
{
type: "user.message",
content: [{ type: "text", text: "Review the auth module" }],
},
],
},
);
```
> Tip: **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens - stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns).
---
## Stream Events (SSE)
```typescript
// Stream-first: open stream and send concurrently
const [events] = await Promise.all([
collectStream(session.id),
client.beta.sessions.events.send(
session.id,
{ events: [{ type: "user.message", content: [{ type: "text", text: "..." }] }] },
),
]);
// Standalone stream iteration:
const stream = await client.beta.sessions.events.stream(
session.id,
);
for await (const event of stream) {
switch (event.type) {
case "agent.message":
for (const block of event.content) {
if (block.type === "text") {
process.stdout.write(block.text);
}
}
break;
case "agent.custom_tool_use":
// Custom tool invocation - session is now idle
console.log(`\nCustom tool call: ${event.name}`);
console.log(`Input: ${JSON.stringify(event.input)}`);
break;
case "session.status_idle":
console.log("\n--- Agent idle ---");
break;
case "session.status_terminated":
console.log("\n--- Session terminated ---");
break;
}
}
```
---
## Provide Custom Tool Result
```typescript
await client.beta.sessions.events.send(
session.id,
{
events: [
{
type: "user.custom_tool_result",
custom_tool_use_id: "sevt_abc123",
content: [{ type: "text", text: "All 42 tests passed." }],
},
],
},
);
```
---
## Poll Events
```typescript
const events = await client.beta.sessions.events.list(
session.id,
);
for (const event of events.data) {
console.log(`${event.type}: ${event.id}`);
}
```
---
## Full Streaming Loop with Custom Tools
```typescript
function runCustomTool(toolName: string, toolInput: unknown): string {
if (toolName === "run_tests") {
// Your tool implementation here
return "All tests passed.";
}
return `Unknown tool: ${toolName}`;
}
async function runSession(client: Anthropic, sessionId: string) {
while (true) {
const stream = await client.beta.sessions.events.stream(
sessionId,
);
const toolCalls: Anthropic.Beta.Sessions.BetaManagedAgentsAgentCustomToolUseEvent[] = [];
for await (const event of stream) {
if (event.type === "agent.message") {
for (const block of event.content) {
if (block.type === "text") {
process.stdout.write(block.text);
}
}
} else if (event.type === "agent.custom_tool_use") {
toolCalls.push(event);
} else if (event.type === "session.status_idle") {
break;
} else if (event.type === "session.status_terminated") {
return;
}
}
if (toolCalls.length === 0) break;
// Process custom tool calls
const results = toolCalls.map((call) => ({
type: "user.custom_tool_result" as const,
custom_tool_use_id: call.id,
content: [{ type: "text" as const, text: runCustomTool(call.name, call.input) }],
}));
await client.beta.sessions.events.send(
sessionId,
{ events: results },
);
}
}
```
---
## Upload a File
```typescript
import fs from "fs";
const file = await client.beta.files.upload({
file: fs.createReadStream("data.csv"),
purpose: "agent",
});
// Use in a session
const session = await client.beta.sessions.create(
{
agent: { type: "agent", id: agent.id, version: agent.version },
environment_id: environment.id,
resources: [{ type: "file", file_id: file.id, mount_path: "/workspace/data.csv" }],
},
);
```
---
## List and Download Session Files
List files the agent wrote to `/mnt/session/outputs/` during a session, then download them.
```typescript
import fs from "fs";
// List files associated with a session
const files = await client.beta.files.list({
scope_id: session.id,
betas: ["managed-agents-2026-04-01"],
});
for (const f of files.data) {
console.log(f.filename, f.size_bytes);
// Download and save to disk
const resp = await client.beta.files.download(f.id);
const buffer = Buffer.from(await resp.arrayBuffer());
fs.writeFileSync(f.filename, buffer);
}
```
> Tip: There's a brief indexing lag (~1-3s) between `session.status_idle` and output files appearing in `files.list`. Retry once or twice if the list is empty.
---
## Session Management
```typescript
// Get session details
const session = await client.beta.sessions.retrieve("sesn_011CZxAbc123Def456");
console.log(session.status, session.usage);
// List sessions
const sessions = await client.beta.sessions.list();
// Delete a session
await client.beta.sessions.delete("sesn_011CZxAbc123Def456");
// Archive a session
await client.beta.sessions.archive("sesn_011CZxAbc123Def456");
```
---
## MCP Server Integration
```typescript
// Agent declares MCP server (no auth here - auth goes in a vault)
const agent = await client.beta.agents.create({
name: "MCP Agent",
model: "claude-opus-5",
mcp_servers: [
{ type: "url", name: "my-tools", url: "https://my-mcp-server.example.com/sse" },
],
tools: [
{ type: "agent_toolset_20260401", default_config: { enabled: true } },
{ type: "mcp_toolset", mcp_server_name: "my-tools" },
],
});
// Session attaches vault(s) containing credentials for those MCP server URLs
const session = await client.beta.sessions.create({
agent: agent.id,
environment_id: environment.id,
vault_ids: [vault.id],
});
```
See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials.