Clients and registries¶
Package-level llm.Stream and llm.Complete use one global default Client. That client combines an adapter registry with a built-in provider registry. Most applications only need a side-effect import for the protocol adapter and the package-level entry points.
Use an explicit Client when you need to:
- isolate provider overrides by subsystem or tenant;
- inject an
http.Client, transport, proxy, or TLS configuration; - avoid mutating package-global state in tests;
- register only approved protocols;
- install a custom
ProtocolAdapter.
The three registries¶
| Type | Stores | Primary key | Request role |
|---|---|---|---|
AdapterRegistry |
ProtocolAdapter values |
Protocol |
Selects a wire-protocol implementation from Model.Protocol |
ProviderRegistry |
Provider configuration and overrides | Model.Provider |
Resolves key, URL, and headers |
ModelRegistry |
Model metadata |
provider + model ID | Supports discovery; Client does not depend on it directly |
The registries do not replace one another. Registering a model does not register a protocol adapter or provider credentials.
Default client¶
llm/default.go creates the following package state:
defaultRegistry = NewAdapterRegistry()
defaultProviderRegistry = NewBuiltInProviderRegistry()
defaultClient = NewClient(defaultRegistry, defaultProviderRegistry)
llm/openai and llm/anthropic register themselves with the default adapter registry from init. Only imported protocols appear in SupportsProtocol.
import (
"github.com/ktsoator/or/llm"
_ "github.com/ktsoator/or/llm/openai"
)
response, err := llm.Complete(ctx, model, input, options)
DefaultProviderRegistry returns the provider registry used by the default client. SetOverride on that registry affects later package-level requests for that provider throughout the process.
Custom client¶
An explicit client combines application-owned adapter and provider registries:
adapters := llm.NewAdapterRegistry()
if err := adapters.Register(openai.NewAdapter(httpClient)); err != nil {
log.Fatal(err)
}
client := llm.NewClient(adapters, llm.NewBuiltInProviderRegistry())
NewClient(adapters, nil) skips the provider registry but still applies the legacy environment-variable mapping for Model.Provider. Pass StreamOptions.APIKey when a custom provider has no legacy mapping.
See the explicit-client task guide for complete imports, Transport configuration, and a request.
Custom HTTP client¶
Both built-in adapters accept *http.Client. This fragment shows connection
pool settings only; the complete client construction is not repeated here:
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.MaxIdleConns = 100
transport.MaxIdleConnsPerHost = 20
httpClient := &http.Client{Transport: transport}
adapters := llm.NewAdapterRegistry()
if err := adapters.Register(openai.NewAdapter(httpClient)); err != nil {
log.Fatal(err)
}
client := llm.NewClient(adapters, llm.NewBuiltInProviderRegistry())
openai.NewAdapter(nil) uses http.DefaultClient. anthropic.NewAdapter(nil) lets the Anthropic SDK use its default client. Request-level StreamOptions.Timeout still applies at the SDK request layer.
Share an http.Client to reuse connection pools. Do not create a new transport per model request.
ProviderRegistry¶
Construction¶
empty := llm.NewProviderRegistry()
builtIn := llm.NewBuiltInProviderRegistry()
defaultRegistry := llm.DefaultProviderRegistry()
NewProviderRegistryreturns an empty registry.NewBuiltInProviderRegistryderives provider configuration from the embedded catalog.DefaultProviderRegistryreturns the instance used by the package-level client.
Query and mutation¶
| Method | Purpose |
|---|---|
Register(provider) |
Add or replace a provider |
Get(providerID) |
Look up a provider |
Providers() |
Return providers sorted by ID |
SetOverride(providerID, override) |
Override URL, key, headers, or environment |
ClearOverride(providerID) |
Remove an override |
AuthStatus(providerID, env) |
Inspect credential resolution |
ResolveRequest(model, options) |
Compute the model and options seen by the adapter |
ProviderRegistry protects its maps with a read/write lock. Registration and overrides clone their inputs. Requests that have already resolved their configuration are unaffected by later changes.
ModelRegistry¶
ModelRegistry is an optional application-owned model catalog:
models := llm.NewModelRegistry()
if err := models.Register(llm.Model{
ID: "local-model",
Name: "Local Model",
Provider: "local",
Protocol: llm.ProtocolOpenAICompletions,
BaseURL: "http://localhost:8080/v1",
Input: []llm.ModelInput{llm.ModelInputText},
}); err != nil {
log.Fatal(err)
}
model, ok := models.Get("local", "local-model")
| Method | Purpose |
|---|---|
Register(model) |
Validate and add or replace a model |
Get(provider, modelID) |
Return a defensive copy |
Providers() |
Return provider IDs in lexical order |
Models(provider) |
Return models ordered by model ID |
Package-level LookupModel and GetModel always query the built-in catalog, not an application-created ModelRegistry. A model obtained from a custom registry can still be passed directly to either client form.
Concurrency and lifecycle¶
AdapterRegistry,ProviderRegistry, andModelRegistrysupport concurrent reads and mutations.- Reuse the adapter's
http.Clientfor the client lifetime. Clienthas noClosemethod. Each adapter closes its request stream when the consumer goroutine exits.- With
Stream, continue receiving until the event channel closes. If business logic no longer needs deltas, still drain the channel. - Provider overrides are in-memory configuration and are not persisted.
DefaultProviderRegistryis shared global state. Tests that modify it should callClearOverride, or use an explicit client.
Selection guide¶
| Scenario | Recommended form |
|---|---|
| One application with fixed providers | Package-level Stream/Complete |
| Per-request tenant key | Either client form with StreamOptions.APIKey |
| Per-tenant URL or headers | One ProviderRegistry and Client per tenant |
| Custom proxy, TLS, or transport | Explicit adapter with a custom http.Client |
| Unit tests or mock servers | Explicit client to avoid default-registry mutation |
| New wire protocol | Custom ProtocolAdapter |