Skip to content

Model providers

A provider is anything satisfying the async LLMClient protocol — one method, async def complete(request) -> LlmResponse, transport only: no retry, no caching, no breaker. Everything resilient sits around the provider in the runtime's facade, which is why writing a new one is small and why every provider ships with a paired decoder rather than a vendor SDK.

RunAgent gets its provider from AgentConfig:

from beam_agents import AgentConfig
from beam_agents.model import AnthropicProvider, anthropic_decode

config = AgentConfig(
    provider_factory=lambda: AnthropicProvider(api_key=...),
    decode=anthropic_decode,
)

provider_factory is a zero-argument callable built once per worker — never a client instance, which would have to survive pickling. decode is the provider's paired response decoder; it is optional, and unset means "token counts unknown": LLM_CALL traces then omit their usage attributes rather than report zeros (token budgets require it, so max_tokens_per_activation without decode is a construction-time ValueError).

The shipped providers

All four live in beam_agents.model and need no extra beyond the core install — the HTTP providers ride the core httpx dependency, deliberately with no vendor SDK. Only the vLLM sidecar has an extra, for the engine itself.

Provider Endpoint Decode
AnthropicProvider Anthropic Messages API anthropic_decode
OpenAICompatProvider any /chat/completions-shaped endpoint openai_compat_decode
VllmEndpointProvider a separately served vLLM OpenAI endpoint openai_compat_decode
VllmSidecarProvider an in-worker vLLM engine (beam-agents[vllm]) openai_compat_decode

AnthropicProvider — non-streaming, single-shot: one POST per complete call, the raw response body returned verbatim as LlmResponse.response. The cache payload must be byte-identical to what the provider sent, so there is no SDK and no re-serialization anywhere in the path.

OpenAICompatProvider — the same shape over /chat/completions. Covers OpenAI, vLLM's OpenAI server, and any compatible gateway by base URL alone; it differs from the Anthropic provider only in endpoint shape and auth header. There is no Vertex/Gemini-specific provider; an OpenAI-compatible gateway in front of one works today, and the ADK adapter routes google-genai clients through this same replay-cached path.

vLLM, two ways (details) — VllmEndpointProvider is a preset over the OpenAI-compatible provider for a vLLM server you run separately: base_url is required (no OpenAI default to mis-hit) and the API key is optional, matching unauthenticated vLLM deployments. VllmSidecarProvider runs the engine inside the Beam worker as one worker-local shared singleton (vllm_sidecar_factory builds it); construction runs a bounded readiness probe so a broken engine fails worker setup before any element. The sidecar serializes engine output into canonical chat-completions-shaped bytes, so the one decoder serves both modes. The vllm extra (torch/CUDA-heavy, GPU-oriented) is needed only for the sidecar; endpoint mode and import beam_agents.model never import it.

FakeLLM: the scripted double

FakeLLM is a deterministic in-process LLMClient — ordered first-match-wins rules mapping a request matcher (match_any, match_contains, match_model_id) to a behavior (respond_with, raise_error, fail_then_succeed), recording every request. An unmatched request raises UnmatchedRequestError rather than serving a default: a test that reaches it has a gap.

It is the default model in every test tier but the nightly smoke run, and it is what makes the examples runnable offline. Its latency is an injected hook, never a wall clock, so nothing driven by it flakes.

The replay cache, and why bytes are opaque

Every provider call goes through the cache-first facade path, against the per-key ReplayCache persisted in keyed state: the request's content hash (compute_cache_key — one canonical JSON document over model id, messages, tools schema, sampling params, and the activation's entity_key/seq) is looked up before any transport is touched. This is correctness invariant 3 — a retried bundle adds zero provider calls and commits byte-identical results — and it is a release gate (tests/semantics/test_retry_determinism.py), which is why providers must return the provider's bytes verbatim: the cache stores and replays exactly what was received.

Bounds, enforced by the cache itself: 64 entries per key (LRU), a 6-hour TTL, and a 100 KiB blob cap with a digest-only fallback that preserves divergence detection for oversized responses. The same cache is what powers state export and replay — a replayed activation's model calls are served from the snapshot's llm_cache, and reaching a real transport is a cache miss.

Around a cache miss, the facade adds the resilience the protocol keeps out of providers: the typed provider-error taxonomy (ProviderTimeout, RateLimitError, ServerError retryable; ProviderRequestError not), full-jitter exponential backoff with a Retry-After floor, a worker-local per-endpoint circuit breaker, and the per-activation token budget. All of it is configuration on AgentConfig, none of it is provider code.

Writing your own

Implement complete, return the provider's raw bytes, map failures onto the error taxonomy, and pair it with a Decode that extracts text and TokenUsage from those bytes. The two vLLM modes are the worked example of both directions — a preset over an existing provider, and a from-scratch transport that reuses an existing decoder.