Architecture overview¶
About these pages
The Internals section walks through how the llm package works inside,
for contributors and the curious. The public API is documented under
LLM; this section is about the implementation.
or/llm is a stateless translation layer. It decides what to send for one
request and how to interpret the streamed response, and leaves history storage,
context compaction, and tool-loop orchestration to the caller. The same
conversation can target any model on any supported wire protocol, and the target can
change between turns; the library re-adapts the history for each request.
Package layout¶
There is no separate "facade" and "core": the public types and the
implementation live in one package, llm. Protocol adapters live in their own
sub-packages and register themselves on import, so an application links only the
vendor SDKs it actually uses.
| Path | Role |
|---|---|
llm/ |
The whole neutral core and public API: models, messages, options, streaming, transform, the adapter and provider registries, and the default client |
llm/openai/ |
The openai-completions and openai-responses adapters; registers both from init |
llm/openai/internal/chatcompletions/ |
Chat Completions request conversion, compatibility dialects, and stream state |
llm/openai/internal/responses/ |
Responses input-item conversion and event state machine |
llm/openai/internal/transport/ |
HTTP client setup, request hooks, and shared SSE filtering |
llm/anthropic/ |
The anthropic-messages adapter; registers itself from init |
llm/all/ |
Blank-imports both provider packages, for callers that want every built-in protocol |
llm/internal/ |
jsonx (lenient JSON helpers) and genmodels (the catalog generator) |
An adapter is pulled in for its side effects:
import (
"github.com/ktsoator/or/llm"
_ "github.com/ktsoator/or/llm/anthropic" // registers anthropic-messages
)
The registries, adapter, and client¶
Dispatch is built from a few small pieces, all in the core package:
ProtocolAdapter— an interface withProtocol()(its registry key) andStream()(the request lifecycle for one protocol). See adapters.AdapterRegistry— a concurrency-safemap[Protocol]ProtocolAdapter. Providerinitfunctions callllm.Registerto add themselves to the package default registry; a caller that prefers explicit wiring builds its own withNewAdapterRegistryandAdapterRegistry.Register.ProviderRegistry— a concurrency-safe map of per-vendor configuration: credential sources, static headers, and anyProviderOverride. ItsResolveRequestfills the API key and applies overrides to each request before dispatch; the default isNewBuiltInProviderRegistry, populated from the catalog. See providers.Client— holds both registries and routes each request: it resolves provider configuration through theProviderRegistry, then dispatches to the adapter for the model's protocol.llm.Streamandllm.Completeare thin wrappers over a default client bound to the default registries.
flowchart LR
subgraph core["package llm"]
R["AdapterRegistry"]
P["ProviderRegistry"]
C["Client"]
end
OA["llm/openai · init()"] -->|Register| R
AN["llm/anthropic · init()"] -->|Register| R
C -->|"ResolveRequest(model, options)"| P
C -->|"Get(model.Protocol)"| R
Request data flow¶
flowchart TD
A["llm.Complete / Stream"] --> B["Client.Stream"]
B --> V["options.Validate(protocol, tools)"]
V --> RR["providers.ResolveRequest<br/>key · override · headers"]
RR --> C{"adapters.Get(model.Protocol)"}
C -->|anthropic-messages| D["Anthropic adapter"]
C -->|openai-completions| E["OpenAI adapter"]
C -->|openai-responses| F["OpenAI Responses adapter"]
D --> T["TransformMessages → convert → SDK request"]
E --> T
F --> T
T --> G["StreamWriter: Emit / Done / Fail"]
G --> H["chan Event → caller"]
The Protocol field on the model is the discriminator: Client.Stream uses it
to pick an adapter from the registry. Everything left of the adapter is
provider-neutral; everything inside it may speak one concrete wire protocol.
Reading a request end to end¶
- Protocol-specific options are checked against the target protocol before any HTTP request is built, so a mismatch fails fast.
ProviderRegistry.ResolveRequestfills the API key — fromStreamOptions, a provider override, or the provider's environment variables, in that order — and applies any per-provider base-URL and header override. A model whose provider is not registered falls back to the legacy environment lookup.Protocolselects the adapter. The same conversation can target either protocol; the library re-adapts the history per request.
Source: llm/client.go,
llm/adapters.go,
llm/default.go.
Where to go next¶
- Models and protocols — the
Model, its capabilities, and the catalog. - Message types — the provider-neutral conversation model.
- Protocol adapters — how a protocol is translated and registered.
- Streaming internals — events and the
StreamWritermachinery. - Switching models — adapting history with
TransformMessages.