# amplifier-module-provider-anthropic **Repository Path**: mirrors_microsoft/amplifier-module-provider-anthropic ## Basic Information - **Project Name**: amplifier-module-provider-anthropic - **Description**: Reference implementation for an Anthropic provider for the Amplifier project - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-10-24 - **Last Updated**: 2026-11-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Amplifier Anthropic Provider Module Claude model integration for Amplifier via Anthropic API. ## Prerequisites Offline contracts execute without real keys. Provision with `uv sync --all-extras --dev`, then `uv pip install "amplifier-foundation @ git+https://github.com/microsoft/amplifier-foundation@a34b770ca604b88230449ac728d41dc23a2863a9"`. Run `uv run python -I -S tests/run_offline.py -q`. The runner sanitizes inherited credentials, supplies a fresh owned test home (including Windows profile variables), and creates the default pytest temp parent on a clean checkout. The test home is separate from pytest's temp tree and does not reuse inherited home/config locations. It stays alive through later dependency finalizers; shutdown then removes it and their home-scoped cache writes before final accounting. After isolation and environment sanitation, Windows first performs the real stdlib OS-version query to populate its own platform cache. Its completion and process count are separate bootstrap facts, not vendor requests or a zero-process guarantee; no OS identity or command output is recorded. The runner then installs a Python audit deny-and-record hook **before** site initialization, real Core/Foundation imports and pytest collection. From that boundary, audited DNS/socket attempts and unguarded subprocesses fail the run, even when provider fallback catches the exception. The sole INET connect exception is Windows' exact stdlib socketpair implementation connecting its own IPv4 stream socket to its live loopback listener's exact address (needed for asyncio's self-pipe). Receipts count this as `internal_socketpair_connect`, not a vendor request. Ordinary loopback connects, DNS and datagram sends remain denied; Linux predicate or fallback checks do not establish native Windows qualification. Pure Windows predicate tests use explicit socket facts; native fallback observations retain safe rejecting-clause/exception-type diagnostics and fail closed on missing listener capabilities. Guarded children use an explicit exact Python executable; Windows serialized audit commands must round-trip canonically with the same isolated no-site runner prefix. Shell or ambiguous commands remain denied. This is isolation for **trusted tests**, not a tamper-proof security sandbox for hostile Python, forks or native code outside Python's audit surface. Receipts are written to `ai_working/tmp/offline-receipt.json`; they contain local source paths and are private diagnostics, not public CI artifacts. Messages-only unit mocks receive explicitly synthetic, unspecified Models metadata only with the SDK-constructed default HTTP client and unchanged transports. Explicit caller clients (even the same SDK wrapper/transport types), SDK transport overrides and metadata/error mocks remain authoritative. Inherited mount contracts use scoped nonfunctional credentials. The real image-understanding test is a separate **required live gate**, not a synthetic test. Offline inventory deselects it and records it as **UNMET**. After separate authorization for a paid vendor call and credential provisioning, run `uv run pytest --required-live -q`. Missing credentials fail closed, rather than silently skipping. Offline green does not satisfy that live requirement. Family discovery sorts semantic versions before snapshots, so multi-digit minor releases cannot be hidden from routing by lexical menu filtering. - **Python 3.11+** - **[UV](https://github.com/astral-sh/uv)** - Fast Python package manager ### Installing UV ```bash # macOS/Linux/WSL curl -LsSf https://astral.sh/uv/install.sh | sh # Windows powershell -c "irm https://astral.sh/uv/install.ps1 | iex" ``` ## Purpose Provides access to Anthropic's Claude models (Claude 4 series: Sonnet, Opus, Haiku) as an LLM provider for Amplifier. ## Contract **Module Type:** Provider **Mount Point:** `providers` **Entry Point:** `amplifier_module_provider_anthropic:mount` ## Supported Models ### Canonical model listing `await provider.list_models()` returns `list[ModelInfo]` from the bound endpoint/account's Models API. The default recommendations select the latest active numeric version in each Opus, Sonnet, Haiku and Fable family, plus a distinct **newer** preview. Matching aliases win over snapshots; snapshot-only ties use the newest date. Both `YYYYMMDD` and `YYYY-MM-DD` suffixes are recognized. Unknown families/spellings are conservatively omitted from recommendations. ```python await provider.list_models() # Frozen provider defaults await provider.list_models(filters={}) # All actual IDs, including old/unknown IDs await provider.list_models(filters={"include": ["CLAUDE-HAIKU-*"]}) await provider.list_models(filters={"include": []}) # No recommendations ``` An explicit mapping **replaces**, not merges with, configured defaults. Common keys are `include`, `exclude`, and `hide_dated`; this provider also has `latest_per_family` and `lifecycle`. Patterns use case-insensitive, OS-independent globs; exclusion wins, original IDs remain exact. `hide_dated` suppresses a snapshot only if its matching alias survived the include/exclude selection. `config.model_filters` takes precedence over legacy `filtered`; otherwise `filtered: false` means unfiltered and `filtered: true` uses the recommendation defaults. Config and call filters are copied immutably. `get_model_list_info()` exposes fresh v1 metadata, including `filters_schema`, `default_filters`, `unfiltered: {}`, and `override: "replace"`. The SDK floor and lock are `anthropic==1.12.0` (dependency range remains `<2`). Its native `lifecycle` filter accepts `active`, `deprecated`, and `retired`; its `limit` range is 1–1000. Defaults request `lifecycle=["active"]`, `limit=1000`, and follow every validated `after_id` cursor. `{}` explicitly requests **all three** stages: omitting the native argument would hide retired records. Custom mappings also default to all stages unless they specify `lifecycle`. Raw lifecycle values are validated and checked locally before recommendations, even if a proxy ignores the native filter. These are catalog lifecycle claims, not proof of account inference entitlement. The original bound client and SDK header behavior are preserved, including configured workspace headers. Each call fetches afresh. Genuine empty is `[]`; malformed raw JSON, inconsistent or repeated cursors, and failures raise typed, sanitized kernel errors, never a partial result or cached/static fallback. Raw validation precedes SDK coercion. The candidate-only `discover_models()` API is superseded by `list_models`. The existing fallback-family cache updates only after complete success, using active stable known-family entries independently of include/exclude filters; listing never reads that cache. Selection-only inference settings and explicitly chosen older models remain usable and unchanged. Known-model capability mapping and `enable_1m_context` policy remain intact. `source: "provider-api"` describes inventory provenance, not the source of every metadata value. For opaque IDs with no known capability mapping, supplied API limits are retained; absent limits use `0` to denote **unknown**, not a usable budget, with empty capabilities/defaults. No Sonnet capabilities are inferred. ### Model support - `claude-sonnet-5-5` - Claude Sonnet 5.5 (default; explicit model pins remain unchanged) - `claude-sonnet-5` - Claude Sonnet 5 (previous default; still supported) - `claude-opus-5` - Claude Opus 5 (most capable) - `claude-opus-5-5` - Claude Opus 5.5 (adaptive thinking; $4/$20 input/output per MTok) - `claude-haiku-4-5` - Claude Haiku 4.5 (previous Haiku; manual thinking) - `claude-haiku-5-5` - Claude Haiku 5.5 (adaptive thinking; exact fixed model ID) ### Haiku 5.5 support boundary Haiku 5.5 supports text, vision, ordinary function tools, streaming, a native 1M context ceiling, 128K normal output, and a 512-token cache minimum. The existing `enable_1m_context` policy remains opt-in: without it the provider advertises a 200K context budget; it does not change the vendor's native limit. The Sonnet 5.5 global default and all explicit model/effort pins remain unchanged. No dated Haiku 5.5 alias is assumed. Thinking defaults to adaptive **on**, with the vendor's **medium** effort when effort is omitted. `low`, `medium`, `high`, `xhigh`, and `max` reach `output_config.effort` without a manual budget. `extended_thinking: false` sends genuine `thinking: {type: disabled}` at `high` or below; `xhigh`/`max` with disabled thinking fails locally. Manual `thinking_type: enabled`, `thinking_budget_tokens`, and Sonnet's `between_tools` mode are unsupported. Normal sampling is omitted. Expert overrides reintroducing sampling or forbidden thinking controls fail before transport. Assistant prefill also fails locally: use structured outputs or system instructions and append a user turn, rather than changing signed history. Forced `required`/`any` and named **ordinary function** tool choices are permitted and suppress thinking for that turn; a later auto/default turn resumes adaptive thinking. Ordered thinking (including empty signed blocks), tool calls/results, and images are retained. Synthetic offline signatures prove storage fidelity, not vendor/account replay acceptance. The model's native computer capability is `computer_toolset_20260801`, **not** Haiku 4.5's legacy computer type. Explicit neutral v1 intent can compile to this GA toolset on the exact first-party HTTPS endpoint. Legacy native declarations still fail locally on Haiku 5.5; ordinary function tools, even one named `computer`, are unaffected. Offline adapter contracts do not establish live vendor/account replay or installed executor readiness. #### Haiku pricing is unavailable, not zero Published first-party USD/MTok rates, checked **2026-10-07**: | Prompt length | Input | Output | Cache write 5m / 1h | Cache read | | --- | ---: | ---: | ---: | ---: | | Up to 100,000 tokens | $0.10 | $0.50 | $0.125 / $0.20 | $0.01 | | Above 100,000 tokens | $0.50 | $2.50 | $0.625 / $1.00 | $0.05 | The cached/server-added threshold predicate and full-versus-excess charging remain unverified. Consequently **all Haiku 5.5 scalar costs are `None`**, not a guessed lower-tier amount or a legacy Haiku rate. Response metadata and the `llm:response` event expose `anthropic_cost_unavailable: haiku55_prompt_tier_unverified`; JSON cost is `null`. The provider's registered `session.cost` contribution reports `cost_usd: null` after any unknown-cost call, including when later calls have known costs. Its known subtotal is not reported as a complete total. External consumers that sum only known costs may still show an unchanged or incomplete total: provider accounting alone does not establish consumer/UI completeness or a free call. Costs do not determine model discovery ranking. Sonnet 5.5's documented fixed ID uses its reduced $0.10/MTok cache-read rate; existing per-TTL write accounting and legacy model prices remain unchanged. Sources: [Haiku overview](https://platform.claude.com/docs/en/models/haiku-5-5/overview), [migration](https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide), [thinking](https://platform.claude.com/docs/en/build-with-claude/thinking), and [pricing](https://platform.claude.com/docs/en/about-claude/pricing). Haiku 4.5's manual-thinking behavior is unchanged. ### Opus 5.5 support boundary Opus 5.5 supports normal text, vision, function tools, streaming, effort, and prompt-cost tracking. Its thinking is always adaptive: an unset effort uses the vendor's `medium` effort, and `extended_thinking: false` cannot disable model thinking (the provider warns and preserves any selected effort and output cap). Only on Anthropic's first-party endpoint, Opus 5.5 adapts one explicit native computer declaration (the documented legacy `computer_20241022`, `computer_20250124`, or `computer_20251124`, or `computer_toolset_20260801`) to `computer_toolset_20260801`. The adapter preserves the ToolSpec name as the executor dispatch alias and maps the native action member into `input.action`; ordinary function tools, including one named `computer`, remain ordinary function tools. The actual request assembly guards this first-party-only boundary: custom/proxy endpoints fail locally rather than claiming vendor-native toolset support, even though recognized Opus 5.5 capabilities advertise the native path. This adapter is not a claim that the new toolset is fully equivalent to prior native-computer contracts. Native computer actions are single-action responses: the request uses `tool_choice: auto` with the **request-wide** `disable_parallel_tool_use: true`; forced/named choice remains unsupported on Opus 5.5. `key.repeat` is passed through when the model emits it. An old executor without repeat support presses once and reports success even when `repeat > 1`. Update the computer-action executor to honor `key.repeat` before, or together with, activating this native Opus 5.5 path. Provider-derived headers do not add a legacy computer-use beta for the new toolset; an explicitly configured beta header is retained. As elsewhere in this provider, expert `extra_request_params.tool_choice` is an explicit wire-level override and wins over this derived default. This does not change the default model. ### Provider-neutral computer-use v1 Tools opt in through an explicit `ToolSpec.computer_use` extra, transported by the existing native-tool-spec mechanism. Names do not opt in. `ToolSpec.name` remains the local dispatch alias; `ToolSpec.parameters` remains the canonical function argument schema. ```python ToolSpec( name="desktop", parameters=canonical_schema, type="function", computer_use={ "version": 1, "operations": ["screenshot", "mouse_move", "left_click", "key"], "image_space": {"width": 1280, "height": 720, "unit": "pixel"}, "unavailable_operations": {}, }, ) ``` Supply actual cached screenshot dimensions, not physical monitor dimensions or estimates. Only the exact Sonnet/Haiku 5.5 IDs and the existing recognized Opus 5.5 IDs use this provider's GA native compiler. It explicitly disables every member not listed in `operations`; unavailable operations cannot also be enabled. No computer-use beta header, legacy display declaration, concrete computer-bundle import, peer-provider import or native capability discovery is needed. Unknown image dimensions (`image_space: None`), oversized screenshots, unverified models and unsupported endpoints use the original ordinary function schema with intent/diagnostics removed. Optional `computer_use.native_required: True` refuses those cases instead of falling back. Malformed intent, ambiguous aliases and mixed neutral/legacy declarations always fail locally. Model-only expert overrides are compiled using the final effective model; overrides of tools/messages/system/tool choice are refused for neutral requests. Use public request fields instead. The native path uses `auto`/`none` only and disables parallel tool use request-wide. One vendor member becomes one local-alias **singleton canonical batch**: ```json {"version":1,"actions":[{"action":"left_click","coordinate":[426,87]}],"capture_after":true} ``` Coordinates stay in full-screenshot pixels; durations stay in seconds and scroll amounts in wheel notches. The adapter supports the 17 documented GA member names when explicitly enabled, but refuses unqualified modifiers, repeated key presses other than `repeat: 1`, path variants, missing drag starts, noninteger native durations/amounts and invalid geometry. It does not guess alternative semantics. Pending safety requirements or unknown native member metadata are refused before returning executable calls; no acknowledgement is fabricated. Native replies without the declared family are refused unless they name a separately declared ordinary function (for example, an ordinary `screenshot` tool). Both canonical content and compatibility tool calls retain provider-owned provenance. Replay reconstructs the original vendor input, never the executor batch, and binds tool results by call ID. Alias/image-space/provenance changes refuse replay rather than silently downgrading it. Text, images and `is_error` survive count/create/stream and second-turn projection without mutating the caller. Old installed tools without neutral intent keep their existing flat native inputs; they do not receive the new batch contract. Protocol sources: [computer tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool), [toolset reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference#client-toolsets), and [Haiku migration](https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide#computer-use-toolset). Repository-native offline tests use real Core and SDK types with transport fakes. Paid native vendor checks, signed/account replay, actual final screenshots, platform/backend input and the required live-vision gate remain separate qualification requirements. Haiku pricing is unchanged and still unavailable. ## Configuration ```toml [[providers]] module = "provider-anthropic" name = "anthropic" config = { default_model = "claude-sonnet-5-5", max_tokens = 8192, temperature = 1.0, # Silently ignored by models without sampling support (sonnet-5, opus-4.7+) raw = false # Include full request/response payloads in llm:* events } ``` See the [Configuration Reference](#configuration-reference) table below for every surviving key. ### Reasoning Effort The `reasoning_effort` config key (canonical — it matches the kernel's portable `request.reasoning_effort` field) sets a session-level default reasoning effort applied to **every** request, so you can opt into stronger reasoning once instead of supplying it per-request. The legacy `effort` key remains a working alias; when both are set, `reasoning_effort` wins (a warning is logged). ```yaml providers: - module: provider-anthropic config: default_model: claude-opus-4-8 reasoning_effort: xhigh # legacy alias: effort ``` **This enables extended thinking.** In this provider, `effort` (like the kernel's portable `request.reasoning_effort`) deliberately maps to Anthropic **extended thinking** — setting it engages thinking and controls its depth, the same way OpenAI's reasoning effort engages its reasoning. At the Anthropic API level `effort` and `thinking` are independent primitives; coupling them is Amplifier's "reason harder" product semantics. Because it turns thinking on for every call, **leave `effort` blank unless you want stronger (and more expensive) reasoning by default.** **Accepted values:** | Value | Meaning | Availability | | --- | --- | --- | | `low` | Minimal thinking, most token-efficient | All thinking-capable models | | `medium` | Balanced | All thinking-capable models | | `high` | High intensity (not the omitted default on Haiku 5.5 or Opus 5.5) | All thinking-capable models | | `xhigh` | Extended capability for long-horizon agentic/coding work | Opus 4.7+, Haiku 5.5 | | `max` | Maximum capability, no effort-based token constraint | Opus 4.8+, Haiku 5.5 (normal output caps still apply) | **Precedence** (highest wins): per-call `effort` kwarg → `request.reasoning_effort` (set by the orchestrator) → this `effort` config default. Note the per-call `effort` kwarg is an `output_config.effort`-only override; the `reasoning_effort` chain is what enables thinking. **Notes**: - Invalid values (e.g. `ultra`, `EXTRA HIGH`) are normalised (trimmed/lowercased) and, if still unrecognised, ignored with a warning — they never silently turn thinking on. - `output_config.effort` is only emitted for models the capability matrix marks as supporting it (including **Opus 4.7+ and Haiku 5.5**). On other thinking-capable models the extended-thinking mapping still applies. Broadening this to Sonnet 4.6 and Opus 4.5/4.6 (which Anthropic also supports) is tracked as a follow-up. - `xhigh`/`max` are omitted from `output_config.effort` on models whose capability matrix doesn't list them (a warning is logged), falling back to adaptive thinking. - This key is exposed through `amplifier provider use` (shown for thinking-capable models), so it can be set interactively without hand-editing YAML. ### Thinking Budget `thinking.budget_tokens` is the *entire* reasoning dial on models without `output_config.effort` support -- Haiku 4.5 among them, where `effort` never reaches the wire at all. Two config keys control it: ```yaml providers: - module: provider-anthropic config: default_model: claude-haiku-4-5 extended_thinking: true # turn thinking on WITHOUT choosing an effort thinking_budget_tokens: 8000 # explicit budget, honored as written ``` **Precedence** (highest wins): per-call `thinking_budget_tokens` kwarg -> `thinking_budget_tokens` config -> the budget implied by `reasoning_effort` (`low` -> 4096, everything else -> the model default) -> the model default. An **explicit** budget outranks the effort-implied one, because the effort ladder is a derived default and a configured number is caller intent. Setting only `reasoning_effort` is unchanged: you still get 4096 for `low` and the model default otherwise. **Turning thinking on:** `reasoning_effort` enables thinking as a side effect, but it also selects a thinking depth and, on Opus 4.7+, sets `output_config.effort`. `extended_thinking: true` enables thinking *and nothing else*, which is what you want when the budget is the only dial you care about. `extended_thinking: false` is an explicit opt-out that overrides a configured `reasoning_effort`; a per-call `extended_thinking` kwarg overrides both. **It is never silently discarded.** If an explicitly requested budget does not reach `thinking.budget_tokens`, a warning names the key, the value you asked for, the value actually sent, and why. That happens when: | Situation | What is sent | Remedy | | --- | --- | --- | | Thinking is not enabled | no `thinking` block at all | set `reasoning_effort` or `extended_thinking: true` | | Model has no thinking support (e.g. Haiku 3.5) | no `thinking` block at all | use a thinking-capable model | | Resolved `thinking_type` is `adaptive` | `{"type": "adaptive"}` -- the API forbids `budget_tokens` here; your value sizes `max_tokens` instead | set `thinking_type: enabled` | | Value is outside the model's limits | the clamped value (min 1024, max the model's output ceiling) | pick a value inside the range | A non-integer value is ignored with a warning and the resolved default is used -- a typo does not raise out of every request. ### Debug / Raw Payload Capture `debug` and `raw_debug` never existed as real keys -- this section documented them in error for a long time. The real key is `raw` (boolean, default `false`): when `true`, full request/response payloads are attached to the `llm:request` / `llm:response` events for inspection. ### Retry and Error Handling The provider disables SDK built-in retries (`max_retries=0`) and manages retries itself via `amplifier_core.utils.retry.retry_with_backoff()`. This gives the provider full control over backoff timing, retry-after header honoring, and per-error-class delay scaling. #### Error Translation SDK exceptions are translated to kernel errors before the retry loop sees them. All translations preserve the original exception as `__cause__` for debugging. | SDK Exception | Condition | Kernel Error | Status | Retryable | | --- | --- | --- | --- | --- | | `RateLimitError` | 429 | `RateLimitError` | 429 | Yes | | `OverloadedError` | 529 | `ProviderUnavailableError` | 529 | Yes (10× backoff) | | `InternalServerError` | 5xx | `ProviderUnavailableError` | 5xx | Yes | | `AuthenticationError` | 401 | `AuthenticationError` | 401 | No | | `BadRequestError` | prompt/context window overflow (e.g. `prompt is too long: ... tokens > ... maximum`) | `ContextLengthError` | 400 | No | | `BadRequestError` | safety / content filter / blocked | `ContentFilterError` | 400 | No | | `BadRequestError` | other | `InvalidRequestError` | 400 | No | | `APIStatusError` | 403 | `AccessDeniedError` | 403 | No | | `APIStatusError` | 404 | `NotFoundError` | 404 | No | | `APIStatusError` | other non-5xx | `LLMError` | — | No | | `asyncio.TimeoutError` | — | `LLMTimeoutError` | — | Yes | | Other | — | `LLMError` | — | Yes | #### Backoff Formula Each retry delay is computed as follows: ``` base_delay = min_retry_delay × 2^(attempt - 1) capped_delay = min(base_delay, max_retry_delay) scaled_delay = capped_delay × delay_multiplier # 1.0 for most errors, 10.0 for 529 final_delay = max(scaled_delay, retry_after) # server retry-after as floor sleep = final_delay ± (final_delay × jitter) # randomised ± jitter fraction ``` **Example: 529 Overloaded (10× multiplier, defaults)** | Attempt | base_delay | capped | ×10 | Sleep | | --- | --- | --- | --- | --- | | 1 | 1s | 1s | 10s | 10s | | 2 | 2s | 2s | 20s | 20s | | 3 | 4s | 4s | 40s | 40s | | 4 | 8s | 8s | 80s | 80s | | 5 | 16s | 16s | 160s | 160s | Total wait ≈ 310s (~5 min) before the request is abandoned. #### Retry Configuration ```yaml providers: - module: provider-anthropic config: max_retries: 5 min_retry_delay: 1.0 max_retry_delay: 60.0 retry_jitter: true overloaded_delay_multiplier: 10.0 ``` | Key | Default | Description | | --- | --- | --- | | `max_retries` | `5` | Maximum retry attempts before giving up | | `min_retry_delay` | `1.0` | Base delay in seconds for the first retry | | `max_retry_delay` | `60.0` | Cap on the base delay (before multiplier) | | `retry_jitter` | `true` | Randomise each retry delay to avoid thundering-herd retries. **Boolean.** A numeric value such as `0.2` is parsed as `false` and disables jitter -- use `true`/`false` | | `overloaded_delay_multiplier` | `10.0` | Multiplier applied to delays for 529 Overloaded errors | #### Events A `provider:retry` event is emitted before each retry sleep with the following fields: | Field | Description | | --- | --- | | `provider` | Provider name (`"anthropic"`) | | `model` | Model being called | | `attempt` | Current retry attempt number | | `max_retries` | Configured maximum retries | | `delay` | Computed sleep duration in seconds | | `retry_after` | Server retry-after value (or `null`) | | `error_type` | Kernel error class name | | `error_message` | Error description | ### Adaptive Input Budgeting and Overflow Recovery The provider exposes optional asynchronous `request_budget()` and `recover_context_overflow()` methods for a retention-aware context manager. `request_budget()` uses Anthropic's count endpoint for the existing shared assembled request, then adds a fixed 4,096-token safety reserve. It projects only count-endpoint-compatible fields: the resolved model, messages, system, tools, tool choice, thinking, output configuration, cache control, and required beta headers. The dispatch-only `max_tokens` value is never sent to count. Count calls have a five-second deadline and no retry. Unknown model windows, count errors (including 429), timeouts, and malformed count responses return `None`; no byte-ratio, character heuristic, cached count, or other estimate is used. Each unavailable reason emits one safe warning per resolved model, without request or response data. An explicit `ChatRequest.max_output_tokens` remains the wire output cap even when adaptive thinking would otherwise enlarge an implicit cap. When this endpoint returns a valid count, `request_budget()` keeps its existing budget fields and additively includes: ```python "measurement": { "kind": "provider_count", "source": "anthropic.messages.count_tokens", "input_tokens": 12345, # raw count-endpoint input_tokens value } ``` The provider advertises this optional provenance through `request_budget:provider_count`. Consumers must negotiate that capability and still treat an absent or malformed `measurement` as unavailable; existing budget-only callers continue to use the unchanged budget fields. Recovery is narrower still. Only a structured Anthropic `invalid_request_error` HTTP 400 whose request id and exact input-only overflow counts are present can authorize it. A strict documented combined input/`max_tokens` grammar is also eligible only when its positive arithmetic derives a lower input limit. The private feedback is bound to the same unchanged request, options, and assembled wire payload; it is consumed once and permits one smaller context retry. Other joint input/output limit errors, malformed responses, and any stream that has produced its first SDK event are never eligible. The provider never retries a context error by itself. ## Conversation-Region Prompt Caching The provider places up to four Anthropic cache breakpoints per request: one on the system prompt, one on the last tool definition, and up to two rolling ones inside the conversation. The conversation breakpoints are the ones that matter for a long agent loop, because they are the only ones that grow with the transcript. Placing them safely requires knowing which trailing messages are *regenerated per request* (a `` with a live clock, a git-status blob) rather than appended to history. A cached prefix ending in regenerated content can never be reproduced, so it is written every turn and never read -- strictly worse than not caching, because writes are billed at 1.25x. The provider answers that question two ways, in this order: 1. **Declared** -- `Message.metadata = {"ephemeral": True}` (and `"persisted"` for content that *is* written into canonical history). Orchestrators that populate this get the most precise placement. 2. **Observed** -- `cache_infer_stability_from_history` (default `true`). The provider fingerprints each request's message array and, on the next request in the same conversation, takes `len(previous) - longest_common_prefix` as the measured unstable-suffix length. The observation is used as a **floor** with the declared value, never a replacement: it can only ever make placement more conservative. ```yaml providers: - module: provider-anthropic config: enable_prompt_caching: true # default: true cache_infer_stability_from_history: true # default: true ``` **Why the default matters.** Before observation existed, a deployment whose orchestrator never populated `Message.metadata` got *no* conversation-region breakpoint at all -- ever. Measured on one coding-agent node visit (164 provider calls, 60 minutes): `cache_read_input_tokens` frozen at exactly 10,995 (the system + tools blocks) on every single call while the prefix grew from 24K to 228K tokens. 1.79M cache_read against 24.7M billed input -- a **6.8%** hit ratio. Reproduced and fixed under a 12-call tool-using loop against `claude-haiku-4-5` (`scripts/prompt_cache_live_probe.py`, no metadata anywhere, regenerated tail): | | calls 2-12 hit ratio | uncached tokens, calls 2-12 | | --- | ---: | ---: | | before | 71.5% (cache_read frozen at 11,180) | 44,208 | | after | **95.0%** | **411** | Two things follow from measuring rather than declaring: - **The first request in a conversation places nothing.** There is no prior request to compare against, so there is no evidence yet. Caching engages from the second request and the first hit lands on the second or third. That warm-up is the price of not guessing. - **A conversation whose *leading* message is regenerated per request is detected and reported.** Anthropic matches a cached prefix from the start of the request, so nothing downstream of a volatile first message can ever hit. The provider logs a warning naming the likely cause and places no conversation breakpoint, rather than billing a write every turn for a prefix that can never be read. Set `cache_infer_stability_from_history: false` to revert to strict metadata-only behaviour. > **Minimum cacheable prefix.** Anthropic silently ignores a breakpoint whose > prefix is below the model's minimum. Measured for `claude-haiku-4-5` > (2026-09-06): a 4,369-token system+tools prefix produced *no* cache write on > two consecutive identical requests, while 8,659 tokens cached immediately -- > i.e. the effective floor is nearer 4096 than the 2048 the docs list for > Haiku. A small system prompt therefore gets no system-block caching at all, > in any version of this provider. ## Prompt Cache TTL By default, prompt-cache breakpoints use Anthropic's standard 5-minute TTL. The `cache_stable_region_ttl_1h` config key opts into a 1-hour TTL for the two most stable cache breakpoints only: ```yaml providers: - module: provider-anthropic config: enable_prompt_caching: true # required -- see below cache_stable_region_ttl_1h: true ``` **What it covers**: the **system prompt and tool-definition** breakpoints only. It deliberately never applies to conversation-region breakpoints, which move every turn on a rolling basis and would pay the higher write premium (below) on content that's about to be superseded anyway. Extending the conversation region to a longer TTL is a separate, currently-unimplemented idea tracked upstream in [microsoft/amplifier#337](https://github.com/microsoft/amplifier/issues/337). See the design comment above `self.cache_stable_region_ttl_1h` in `amplifier_module_provider_anthropic/__init__.py` for the full rationale. **The economics**: Anthropic bills 1-hour TTL cache *writes* at 2x the base input-token rate, versus 1.25x for the default 5-minute TTL. That's a real up-front cost -- but a longer TTL means the write survives more calls before it needs to be repeated. Once a cache entry is reused across roughly two or more reads inside the 1-hour window that a 5-minute TTL would have missed (because the previous call was more than 5 minutes ago), the 1h TTL comes out ahead. It helps most for: - **Long-running sessions** with gaps between calls longer than 5 minutes (e.g. a human pausing between turns, a scheduled/cron-triggered agent). - **Stable system prompts and tool sets** that don't change turn-to-turn -- exactly the two regions this knob targets. If your calls are consistently more frequent than every 5 minutes, the default 5-minute TTL already keeps the cache warm at the cheaper 1.25x write rate, and enabling this knob only adds cost. **Requires prompt caching itself.** Because this knob only affects existing cache breakpoints, it is a no-op when `enable_prompt_caching` is `false` (or left at its `false` default) -- the provider will not append the `extended-cache-ttl-2025-04-11` beta header in that case, and logs a one-line notice explaining why. Enable both together. **Left unset by default** (rather than defaulting to `false`): this makes "no opinion, use the provider's own 5-minute default" a real, distinguishable third state in the config wizard, separate from an explicit opt-out. ## Beta Headers Anthropic provides experimental features through beta headers. Enable these features by adding the `beta_headers` configuration field. ### Configuration **Single beta header:** ```yaml providers: - module: provider-anthropic config: default_model: claude-sonnet-4-5 beta_headers: "context-1m-2025-08-07" # Enable 1M token context window ``` **Multiple beta headers:** ```yaml providers: - module: provider-anthropic config: default_model: claude-sonnet-4-5 beta_headers: - "context-1m-2025-08-07" - "future-feature-header" ``` ### 1M Token Context Window 1M context is **generally available, on by default, and billed at standard pricing** on the previously supported models that have it (Opus 5/4.8/4.7/4.6, Sonnet 5/4.6, Fable 5/5.1, Mythos 5/Preview). No beta header is required, and there is no long-context price premium ([Anthropic: Context windows](https://platform.claude.com/en/docs/build-with-claude/context-windows), verified 2026-08-29). Those models cap output at **128K tokens** per request regardless of context size. Haiku 5.5 also has a native 1M window, but has prompt-length pricing tiers; see its unavailable-cost boundary above. The `enable_1m_context` config key does **not** change what the API accepts. It only sets the context window this provider *advertises* to Amplifier's context manager (200K vs 1M), which determines how much conversation history is kept per request -- a cost decision. It defaults to `false`. ### Notes - Beta features are experimental and subject to change - Check [Anthropic's documentation](https://docs.anthropic.com) for available beta headers - Beta headers are optional - existing configurations work unchanged - Invalid beta headers will cause API errors (fail fast) - Beta header usage is logged at initialization for observability ## Configuration Reference House-style key reference. ✅ = wizard-visible ConfigField, ⚙️ = settings-only (fully functional, not asked in the wizard). | Key | Default | Surface | Description | |---|---|---|---| | `api_key` | *(env `ANTHROPIC_API_KEY`)* | ✅ | Anthropic API key | | `base_url` | `https://api.anthropic.com` | ✅ | Custom endpoint | | `default_model` | `claude-sonnet-5-5` | *(model picker)* | Model used when a request does not name one | | `reasoning_effort` | *(unset)* | ✅ | `low`\|`medium`\|`high`\|`xhigh`\|`max`. Enables extended thinking. Legacy alias: `effort` (deprecated) | | `extended_thinking` | *(unset)* | ⚙️ | Turn extended thinking on/off without choosing an effort. Overrides the `reasoning_effort` implication; a per-call kwarg overrides this | | `thinking_budget_tokens` | *(model default)* | ⚙️ | Explicit `thinking.budget_tokens`. Outranks the effort-implied budget; warns if it can't reach the wire | | `thinking_budget_buffer` | `8192` | ⚙️ | Headroom added to the budget when sizing `max_tokens` | | `thinking_type` | `adaptive` | ⚙️ | `adaptive`\|`enabled`. `adaptive` lets the model manage its own budget (and forbids `budget_tokens`); falls back to `enabled` on models without adaptive support | | `enable_1m_context` | `false` | ✅ | Advertise the 1M context window (more history kept = higher cost) | | `cache_stable_region_ttl_1h` | *(unset)* | ✅ | 1h cache TTL for system prompt + tools. 2x write cost, fewer writes | | `enable_prompt_caching` | `true` | ⚙️ | Place cache breakpoints | | `cache_infer_stability_from_history` | `true` | ⚙️ | Measure the regenerated-per-request tail by comparing consecutive requests, when `Message.metadata` is not populated. Used as a floor with the declared value, never a replacement. `false` = strict metadata-only | | `max_tokens` | *(model ceiling)* | ⚙️ | Output token cap | | `temperature` | `0.7` | ⚙️ | Ignored by non-sampling models (Sonnet 5, Opus 4.7+) | | `timeout` | `null` | ⚙️ | Optional request deadline, seconds; default waits for completion | | `close_timeout` | `5.0` | ⚙️ | Hard bound on closing the HTTP client at session teardown, seconds. `httpx`'s `aclose()` has no deadline of its own and blocks forever on a half-closed (CLOSE-WAIT) connection; on timeout the client is abandoned with a WARNING and the socket is reclaimed at process exit | | `priority` | `100` | ⚙️ | Provider selection priority (lower wins) | | `raw` | `false` | ⚙️ | Include full request/response payloads in `llm:request`/`llm:response` events | | `fallback_on_overload` | `false` | ⚙️ | Downgrade one ladder rung (fable/mythos → opus → sonnet → haiku) after persistent 529s | | `fallback_retry_count` | `2` | ⚙️ | 529 retries before downgrading | | `fallback_cooldown_seconds` | `300` | ⚙️ | How long a downgrade window stays open | | `fallback_models` | *(unset)* | ⚙️ | Per-family target overrides, e.g. `{opus: claude-opus-4-8}` | | `persist_fallback_state` | `false` | ⚙️ | Share downgrade windows across processes | | `refusal_fallback_enabled` | `true` | ⚙️ | Retry once on `finish_reason="refusal"`, via the same ladder as overload | | `extra_request_params` | *(unset)* | ⚙️ | Escape hatch for Messages API params this provider doesn't model; merged last, user-wins | An instance selected for Haiku may legitimately inherit `fallback_on_overload` from a higher-tier parent. Haiku has no lower tier, so overload downgrade is unavailable for that request; its normal retry policy is unchanged. This is an informational diagnostic, not a warning to remove the parent's setting. The option remains available for higher-tier requests on the same instance. Removed keys (still recognized, warn with a migration message): `fallback_sonnet_model`, `fallback_haiku_model` (use `fallback_models`), `refusal_fallback_model` (refusal fallback now follows the same ladder as overload -- see Fallback section). ## Environment Variables ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` ## Usage ```python # In amplifier configuration [provider] name = "anthropic" default_model = "claude-sonnet-5-5" ``` ## Features - Streaming support - Tool use (function calling) - Vision capabilities (on supported models) - Token counting and management - **Message validation** before API calls (defense in depth) ## Graceful Error Recovery The provider implements automatic repair for incomplete tool call sequences: **The Problem**: If tool results are missing from conversation history (due to context compaction bugs, parsing errors, or state corruption), the Anthropic API rejects the entire request, breaking the user's session. **The Solution**: The provider automatically detects and repairs missing tool_results by injecting synthetic results: 1. **Repair before validation** - Detects missing tool_results and injects synthetic ones 2. **Make failures visible** - Synthetic results contain `[SYSTEM ERROR: Tool result missing]` messages 3. **Maintain conversation validity** - API accepts repaired messages, session continues 4. **Enable recovery** - LLM acknowledges error and can ask user to retry 5. **Provide observability** - Emits `provider:tool_sequence_repaired` event with repair details 6. **Validate remaining** - After repair, strict validation catches any remaining inconsistencies **Example**: ```python # Anthropic format (after _convert_messages) messages = [ { "role": "assistant", "content": [ {"type": "tool_use", "id": "toolu_123", "name": "get_weather", "input": {...}} ] }, # MISSING: {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_123", ...}]} {"role": "user", "content": "Thanks"} ] # Provider repairs by injecting synthetic result: # Either appends to existing user message or inserts new one { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "toolu_123", "content": "[SYSTEM ERROR: Tool result missing]\n\nTool: get_weather\n..." }] } ``` **Observability**: Repairs are logged as warnings and emit `provider:tool_sequence_repaired` events for monitoring. **Philosophy**: This is **graceful degradation** following kernel philosophy - errors in other modules (context management) don't crash the provider or kill the user's session ## Dependencies - `amplifier-core>=1.0.0` - `anthropic>=1.0.0,<2.0.0` ## Contributing > [!NOTE] > This project is not currently accepting external contributions, but we're actively working toward opening this up. We value community input and look forward to collaborating in the future. For now, feel free to fork and experiment! Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com). When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ### Waiting for model work Completion and streaming requests have no elapsed-time or read-idle deadline by default. They wait for completion, explicit cancellation, or a provider/transport error. Set `timeout` (seconds) to opt into a request deadline; `null` leaves model work unbounded. Connection and pool acquisition remain bounded to 5 seconds, and existing `close_timeout` cleanup limits are unchanged.