Skip to content

feat(provider-anthropic): Anthropic Messages API provider worker - #247

Merged
ytallo merged 27 commits into
mainfrom
feat/provider-anthropic
Jun 12, 2026
Merged

ytallo merged 27 commits into
mainfrom
feat/provider-anthropic

Conversation

@ytallo

@ytallo ytallo commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stacked on #241 (feat/llm-router-rs) — first real provider implementing the router's provider protocol from tech-specs/2026-06-agentic/llm-router.md.

provider-anthropic — Anthropic Messages API provider behind llm-router:

  • provider::anthropic::stream: SSE → AssistantMessageEvent state machine with ping watchdog, abort-on-channel-close, and the shared error taxonomy
  • Request assembly: api-key/OAuth auth modes, extended-thinking budget mapping with degradation warnings, prompt-cache markers (system prompt, tools tail, last stable assistant turn; PROVIDER_ANTHROPIC_CACHE=0 kill switch)
  • provider::anthropic::refresh_models: live GET /v1/models ∪ curated capability snapshot (context windows, output ceilings, thinking budgets, pricing), reconciled into the router catalog with no cold-catalog hole
  • Declare-with-backoff registration, registration-token persistence in iii-state (with store retry so a transient state failure at first boot cannot strand the binding), re-declare on router::ready

Streaming robustness:

  • Byte-level UTF-8 buffering across SSE chunks — multi-byte characters split across network reads decode correctly
  • Truncated streams (EOF before message_stop) surface as error frames instead of a clean done
  • Content-block routing keyed by the wire index field; unknown block types cannot corrupt neighboring blocks
  • Cumulative-aware usage merge (no double-counting of message_delta totals)
  • Structured Anthropic error-type classification, including mid-stream SSE error events and router bus errors (registration_rejected is not reported as transient)
  • Empty-content message guards and redacted-thinking round-trip for thinking-enabled tool loops (adds the opaque RedactedThinking content-block variant to llm-router types — the only router-crate change in this PR, +4 lines)

Docs: consumer-facing README per the worker-readme guide, runnable examples/stream_chat.rs demo, provider cross-reference in the router README.

Agent exposure is locked down per iii-permissions.yaml: provider::anthropic::* is never agent-callable — the router invokes it worker-to-worker.

Test plan

  • 77 unit tests passing (SSE state machine, wire adapters, thinking budgets, cache markers, curated merge, error taxonomy, config precedence)
  • Live-engine integration suite (III_ENGINE_BIN=… cargo test --test integration): 5 passed — registration + token persistence, re-declare on router::ready, discovery reconcile, end-to-end chat stream with cost fill, upstream 401 → auth_expired frame (real engine + real router + stub upstream, no external API calls)
  • Live smoke against api.anthropic.com with a configured key (manual)

Summary by CodeRabbit

  • New Features
    • Added Anthropic provider with streaming chat completion support.
    • Implemented live model discovery and dynamic catalog management.
    • Added thinking capability configuration for supported models.
    • Enabled prompt caching for request optimization.
    • Integrated tool/function calling support.
    • Added API key and OAuth credential handling.

@github-actions

github-actions Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 17 skipped (no docs/).

Layer Result
structure ✓
vale ✓
ai ✓
render ✓

Four for four. Nicely done.

@vercel

vercel Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
harness Error Error Jun 12, 2026 6:44pm
workers Ready Ready Preview, Comment Jun 12, 2026 6:44pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e5693ee-cdb0-439c-ab8b-46c8dc859f97

📥 Commits

Reviewing files that changed from the base of the PR and between 6d61ba1 and d9f2445.

⛔ Files ignored due to path filters (1)
  • provider-anthropic/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • llm-router/README.md
  • llm-router/src/types/content.rs
  • provider-anthropic/.gitignore
  • provider-anthropic/Cargo.toml
  • provider-anthropic/README.md
  • provider-anthropic/build.rs
  • provider-anthropic/iii-permissions.yaml
  • provider-anthropic/iii.worker.yaml
  • provider-anthropic/src/config.rs
  • provider-anthropic/src/curated.rs
  • provider-anthropic/src/discovery.rs
  • provider-anthropic/src/errors.rs
  • provider-anthropic/src/lib.rs
  • provider-anthropic/src/main.rs
  • provider-anthropic/src/manifest.rs
  • provider-anthropic/src/register.rs
  • provider-anthropic/src/request.rs
  • provider-anthropic/src/router_client.rs
  • provider-anthropic/src/sse.rs
  • provider-anthropic/src/state.rs
  • provider-anthropic/src/stream_fn.rs
  • provider-anthropic/src/thinking.rs
  • provider-anthropic/src/upstream.rs
  • provider-anthropic/src/wire/cache.rs
  • provider-anthropic/src/wire/messages.rs
  • provider-anthropic/src/wire/mod.rs
  • provider-anthropic/src/wire/names.rs
  • provider-anthropic/src/wire/tools.rs
  • provider-anthropic/tests/integration.rs

📝 Walkthrough

Walkthrough

This PR adds a ContentBlock enum to the llm-router for atomic message content representation, and introduces a complete Anthropic provider worker that implements the router/provider protocol. The provider handles credential resolution, live model discovery, request/response streaming, caching, error classification, and state persistence, with comprehensive integration tests validating end-to-end behavior.

Changes

Router and Anthropic provider implementation

Layer / File(s) Summary
Router ContentBlock enum
llm-router/src/types/content.rs
Introduces serde-serializable ContentBlock enum with variants for Text, Image, Thinking, RedactedThinking, FunctionCall, and FunctionResult, including per-field serialization rules for optional fields.
Provider workspace and build setup
provider-anthropic/Cargo.toml, build.rs, .gitignore, iii.worker.yaml, iii-permissions.yaml
Defines crate metadata, dependencies (iii-sdk, reqwest, tokio), Rust binary/library targets, build-time TARGET forwarding, and worker/permissions manifests blocking direct agent access to provider actions.
Provider entrypoint and exports
provider-anthropic/src/{lib.rs,main.rs,manifest.rs}
Library module surface exporting config/discovery/request/streaming/wire modules; async main supporting --manifest flag for introspection and worker initialization with registration and Ctrl-C shutdown.
Provider configuration and auth
provider-anthropic/src/config.rs
AuthMode enum (ApiKey/OauthBearer), AnthropicConfig struct carrying resolved credentials/auth/model/tokens/api_url, credential extraction from router response, max_tokens and api_url precedence rules.
Live model discovery and pricing
provider-anthropic/src/{discovery.rs,curated.rs}
models_url derives /v1/models?limit=1000 from configured endpoint; parse_live_models filters legacy thinking-only models, extracts capabilities/limits with conservative defaults; pricing_for provides pricing via base-ID matching; refresh_models orchestrates fetch and reconcile.
Error classification
provider-anthropic/src/errors.rs
Maps Anthropic JSON error payloads and HTTP status codes into router ErrorKind taxonomy; handles auth expiry, rate limiting, context overflow (with message-based detection), transient 5xx, and permanent 4xx errors; exports error constructors with worker-prefixed codes.
Provider registration lifecycle
provider-anthropic/src/{register.rs,state.rs}
declaration() describes provider identity/defaults; declare_once() registers with token load/save; declare_with_backoff() retries with exponential backoff; declare_and_refresh() combines registration and live discovery; register_provider() wires router-ready handler; load_token/store_token persist via state scope.
Request construction
provider-anthropic/src/request.rs
BodyArgs struct captures model/max_tokens/messages/tools/thinking/effort/cache; build_body() assembles Anthropic Messages JSON with cache anchoring and thinking config; auth_header() maps credentials to x-api-key or Bearer; build_headers() includes anthropic-version and content-type.
Router protocol client
provider-anthropic/src/router_client.rs
Async wrappers around router::provider::resolve, router::models::reconcile, router::models::get, and router::provider::register with JSON payload construction, deserialization, and error mapping.
Thinking configuration
provider-anthropic/src/thinking.rs
ThinkingConfig struct and ADAPTIVE constant for output_config; effort_for maps ThinkingLevel to Anthropic effort strings; build_thinking_config conditionally enables thinking, drops when unsupported, degrades XHigh to High when unavailable.
SSE event streaming and parsing
provider-anthropic/src/sse.rs
PartialState accumulates SSE block content with wire-order tracking; handle_sse_event parses message_start/content_block_*/message_delta/message_stop/error events, emitting corresponding frames; build_partial converts accumulated state into final AssistantMessage with ContentBlock emission, usage, and native stop reason.
Upstream HTTP adapter
provider-anthropic/src/upstream.rs
spawn_upstream opens async HTTP stream, processes response bytes with UTF-8-safe chunk appending and CRLF normalization; drain_sse_blocks extracts \n\n-delimited blocks; run_upstream handles request/status/stream errors, emits Start/Done/Error frames with partial content preservation.
Streaming function and pump
provider-anthropic/src/stream_fn.rs
make_stream factory deserializes input, resolves config, builds request, spawns upstream; pump forwards events to sink with periodic ping during silence, stops on terminal/error, handles sink write failures.
Wire conversion: cache control
provider-anthropic/src/wire/cache.rs
cache_enabled() reads env toggle; build_system_field() wraps prompts above threshold with cache_control; apply_tools_cache_control() marks last tool; apply_messages_cache_anchor() finds stable assistant and marks final eligible block (skipping thinking/redacted_thinking).
Wire conversion: messages
provider-anthropic/src/wire/messages.rs
content_block_to_wire() converts ContentBlock variants to Anthropic JSON; format_function_result_content() and function_result_to_wire() handle tool results with error flags and image/text switching; to_wire_messages() orchestrates tool-ID pre-pass, result merging, orphan placeholders, deduplication, empty assistant omission.
Wire conversion: tools and names
provider-anthropic/src/wire/{names.rs,tools.rs,mod.rs}
encode_tool_name/decode_tool_name perform :: ↔ __ substitution; functions_to_wire() converts AgentFunction to Anthropic tools JSON with encoded names and input_schema passthrough.
Documentation
llm-router/README.md, provider-anthropic/README.md
Router README identifies provider-anthropic as reference implementation; provider README covers installation, API key setup, thinking_level degradation, configuration, model catalog merge, operational notes (structured output, error mapping, credential binding), and test instructions.
Integration tests
provider-anthropic/tests/integration.rs
Engine bootstrap with WS polling, consumer_channel for frame capture, TCP stub upstream for models/SSE, test coverage for model catalog/token persistence, streaming end-to-end, auth failures, refresh_models reconciliation, and router restart rebinding.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • iii-hq/workers#241: The Anthropic provider implementation reuses router protocol types introduced in this PR, especially the ContentBlock enum and related router protocol documentation.

Suggested reviewers

  • sergiofilhowz

Poem

🐰 Hops into Anthropic's messages flow,
ContentBlocks dancing in SSE's glow,
Cache marks and thinking, from upstream to router,
This provider hops swift—no hiccup, no troubter! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-anthropic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ytallo
ytallo changed the base branch from main to feat/llm-router-rs June 11, 2026 20:56
@ytallo
ytallo changed the base branch from feat/llm-router-rs to main June 11, 2026 20:56
@ytallo
ytallo force-pushed the feat/provider-anthropic branch from 94683cb to d520fd3 Compare June 11, 2026 20:57
@ytallo ytallo changed the title feat: llm-router and provider-anthropic workers feat(provider-anthropic): Anthropic Messages API provider worker Jun 11, 2026
@ytallo
ytallo changed the base branch from main to feat/llm-router-rs June 11, 2026 20:57
@ytallo ytallo added the stacked Depends on another open PR — merge the base PR first label Jun 11, 2026
@ytallo

ytallo commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Stacked on #241 — do not merge before it lands. If #241 is squash-merged, this branch needs a rebase onto main before merging.

ytallo added 21 commits June 12, 2026 15:43
…s, quickstart guide, and detailed configuration options
…al mapping

Delete code orphaned or made redundant by the hardening pass: the
write-only PartialState.open_block field, the build_final alias of
build_partial, classify_sse_error (identical to classify(None, _)),
the duplicated empty_assistant constructor (tests now use the router's
empty_partial), merge_usage's unobservable cumulative flag, and
build_content's unreachable seen-bitvec fallback loops. The delta arms
bind the slot index directly in the match pattern instead of
re-destructuring.

The Credential -> (secret, auth mode) mapping and the auth header pair
now exist once (config::credential_parts, request::auth_header), shared
by the streaming and discovery paths.

One behavior change: the content_block_stop slot clear is get_mut-
guarded, so a stop for a never-started index is a no-op instead of an
out-of-bounds panic.
Opus 4.7/4.8 and Fable/Mythos 5 hard-reject thinking.type "enabled" —
the upstream 400s with 'use thinking.type.adaptive and output_config.effort'.
Every model this provider serves with thinking (Sonnet 4.6 onward) accepts
adaptive, so the legacy budget path is dropped entirely: thinking_level maps
to {type: adaptive, display: summarized} plus output_config.effort
(minimal/low → low, medium, high, xhigh; xhigh still degrades to high when
the catalog says unsupported). Budget arithmetic, thinking_budgets seeding,
and the interleaved-thinking beta header (adaptive interleaves natively) go
with it, and the curated snapshot gains claude-opus-4-8.
… truth

Drop the hand-maintained capability snapshot: live discovery now reads ids,
display names, context/output limits, and the capabilities tree (thinking,
effort.xhigh, image_input) straight from the API, and the registration
declaration carries no static models — a refresh fires right after
registering, so the slice fills from Anthropic within seconds of boot
(reconcile-to-empty still prunes when the key is missing or revoked).
The only local data left is pricing, which the API does not expose.
… the catalog

The live capability tree cleanly splits Anthropic's generations:
thinking.types.adaptive.supported is true from Opus 4.6/Sonnet 4.6 onward
and explicitly false for everything older. Since this provider only
implements adaptive thinking, a model that can think but not adaptively
(Sonnet 4.5, Haiku 4.5, Opus 4.5 and older) would 400 the moment a
thinking_level arrives — those rows are now dropped at parse time, so the
picker offers only models every feature works on. Rows without capability
data stay (permissive for future shapes), and models that cannot think at
all stay with thinking gated off. supports_thinking now reflects adaptive
support, the operative flag for this provider.
…hinking gated off

Haiku is the cheap tier and has no adaptive-generation release yet, so the
legacy-thinking filter now exempts claude-haiku-*: the row stays in the
slice with supports_thinking false, and a thinking_level on it degrades
with a warning instead of erroring. The rest of the non-adaptive
generation stays excluded.

This branch had an error being deployed

1 failed (outdated) and 1 active deployments
Preview – workers — d9f24459 Deployed Jun 12, 2026 by vercel[bot]
Preview – harness — cc4b2aef Deployed Jun 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stacked Depends on another open PR — merge the base PR first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants