provider-anthropic
v1.2.23Anthropic Messages API provider worker; implements provider::anthropic::stream and provider::anthropic::refresh_models behind llm-router.
Install
iii trigger compose::add worker=provider-anthropic@1.2.23Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.
provider-anthropic
Claude models behind llm-router. Install this worker next
to the router, give it an API key, and the Anthropic catalog — streaming,
extended thinking, tool use, vision, automatic prompt caching — appears
behind the router's single front door (router::chat / router::complete).
You never call this worker directly: the router invokes it worker-to-worker,
and iii-permissions.yaml blocks agent access to provider::anthropic::*.
Install
iii trigger compose::add worker=provider-anthropicThe provider does nothing on its own — it plugs into the router:
iii trigger compose::add worker=llm-routeriii trigger compose::add resolves each worker and its dependencies, writes
exact declarations to worker-compose.yaml, and reconciles the Compose project.
Quickstart
Give the router a credential: paste a key into the anthropic slice of the
engine's llm-router configuration entry, or set ANTHROPIC_API_KEY in the
router's environment.
{ "providers": { "anthropic": { "api_key": "sk-ant-…" } } }The router picks up the change and kicks model discovery; Claude models land
in the catalog (router::models::list). Then make a first call — Node shown,
any SDK works:
const res = await iii.trigger('router::complete', {
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }], timestamp: Date.now() }],
}, { timeout_ms: 320_000 }); // outer timeout ≥ the router's 300s stream budget
// res: { message, usage, provider, model } — message is the final AssistantMessageFor token-by-token streaming, call router::chat with an iii channel — the
walkthrough lives in llm-router's Quickstart.
Request extended thinking with thinking_level; the worker maps the level
to an Anthropic thinking budget from the model's catalog record. xhigh
degrades to high on models that don't support it, and the level is
dropped on models that don't support thinking at all.
Configuration
All operator configuration lives in the router's llm-router entry — this
worker keeps no config of its own:
"anthropic": {
"api_key": "sk-ant-…", // or ANTHROPIC_API_KEY in the router's env
"api_url": "https://api.anthropic.com/v1/messages", // override for proxies / gateways
"max_tokens": 8192 // output ceiling when a request sets none
}Worker-side environment variables:
| Variable | Default | Meaning |
|---|---|---|
PROVIDER_ANTHROPIC_CACHE |
enabled | 0/false disables automatic prompt-cache markers |
PROVIDER_ANTHROPIC_CACHE_TTL |
1h |
TTL of the shared-prefix markers on the sectioned path; 5m restores the default cache |
PROVIDER_ANTHROPIC_PREFIX_MISMATCH |
drop_block |
thinking.block_binding.prefix_mismatch_behavior sent (with the thinking-binding-controls-2026-08-01 beta) on every thinking request: drop_block drops a replayed thinking block whose prefix was edited, error returns the 400 instead, off (also 0/false/none) sends neither the field nor the beta (for gateways that reject it). Case-insensitive; any other value logs one warning and uses drop_block. On accounts created before 2026-08-31, sending the field is what turns enforcement on: after a prefix edit, replayed thinking blocks that sit after the edited element and were minted before the edit (every such block, for a system/tools edit), which still reached the model without the field, are now dropped on every later request; blocks before the edited element and blocks minted after the edit survive; off restores the old behaviour. Dropped blocks become one anthropic dropped replayed thinking blocks: […] warning on the message and a worker-log warn with the session id and model when the list arrives (aborted and errored responses included; grep the reason, e.g. prefix_binding_mismatch) |
PROVIDER_ANTHROPIC_CAPTURE_DIR |
unset | Debug only, absolute path (a relative one logs one warning and captures nothing): append every request that has a session id to (bytes outside [a-z0-9.-], _ and uppercase included, written as _xx hex; the body plus anthropic-beta; auth headers are never written; on Unix the dir it creates is 0700 and each file 0600). The files hold user content, tool output, and images: keep them out of the repo and delete them after use |
III_WS_URL |
ws://127.0.0.1:49134 |
engine WebSocket to attach to when --url is not set |
The binary also takes the standard worker CLI flags: --url (engine
WebSocket), --manifest (print the registry manifest and exit), and
--config (accepted but ignored with a warning — provider config comes
from the llm-router configuration entry).
Prompt caching needs no setup: markers go on the system prompt, the tools
tail, and the last stable assistant turn whenever the prefix is big enough
to be worth a cache write. When the router forwards system_sections, the
system field becomes one text block per section and the marker moves to the
block flagged cache_boundary (the frozen agent-profile prefix) whatever its
size — Anthropic applies its per-model token minimum over the whole prefix,
tools included, and silently skips a short one — so the per-session tail
after it no longer invalidates the shared entry. At most two system blocks are marked, keeping
the total at Anthropic's four. On that sectioned path the boundary block (and
the tools marker ahead of it) use the 1-hour cache (ttl: "1h", 2x base on
the one write, reads unchanged) so a profile stays warm across sessions up to
an hour apart; the per-turn messages anchor keeps the 5-minute default, which
also satisfies the longer-before-shorter TTL rule. PROVIDER_ANTHROPIC_CACHE_TTL=5m
goes back to 5 minutes everywhere.
Models
The catalog slice is live GET /v1/models: model ids, display names,
context windows, output ceilings, and the capability flags (adaptive
thinking, xhigh effort, vision) all come from the API on every refresh, so
a newly shipped model (Opus 5.5, Sonnet 5, Fable 5.1, …) appears in the
picker as soon as the API lists it — no code or SDK change required. Rows the
API marks as thinking-capable but not adaptive-capable (the pre-4.6
generation) are dropped because this provider only implements adaptive
thinking; the haiku family is the exception and stays with thinking gated off.
The API does not publish pricing, so that is the one hand-maintained table:
src/curated.rs, USD per MTok keyed by base model id
(date suffixes stripped). A model missing there still routes and shows up;
it only loses cost enrichment, and the harness refuses max_cost_usd
budgets on it. Update the table against
platform.claude.com/docs/en/about-claude/pricing
when Anthropic ships or reprices a model.
Notes
- Token counting:
provider::anthropic::count_tokens(behindrouter::count_tokens) posts the assembled prompt to the messages endpoint'scount_tokenssibling for an exact provider-metered count; it never runs the model and costs nothing. - Structured output: the Messages API has no native JSON mode; every
catalog record declares
supports_structured_output: false, and a forwardedresponse_formatis reported inwarningsand ignored. - Errors: 401/403 →
auth_expired, 429 →rate_limited, 413/context →context_overflow, 5xx/network →transient, other 4xx →permanent. The worker never retries — the router owns retry policy. - Credentials: resolved per request via
router::provider::resolve(config slice →ANTHROPIC_API_KEYon the router → none). Bothapi_key(x-api-key) andoauth(Bearer) shapes work; v1 performs no OAuth refresh. - Identity binding: the router issues a
registration_tokenon first registration, persisted in state (scopeprovider-anthropic). If that state is lost the router rejects re-registration — clear the binding on the router side to re-pair.
Tests
cargo test # unit (pure modules + TCP stubs)
III_ENGINE_BIN=$(which iii) cargo test --test integration -- --test-threads=1The integration suite spawns a real engine, the real router (path dep), this provider, and a local stub upstream — no external API calls anywhere.