Providers & the per-provider module pattern¶
This page is the provider reference: the constants, the capability matrix, the provider→module map, per-provider notes, multimodal support, and how provider-neutral token usage maps onto each vendor's counts.
Provider constants¶
| Constant | String | Module (blank-import to activate) | API key |
|---|---|---|---|
chat.ProviderClaude |
claude |
chat-anthropic |
ANTHROPIC_API_KEY |
chat.ProviderOpenAI |
openai |
chat-openai |
OPENAI_API_KEY |
chat.ProviderOpenAICompatible |
openai-compatible |
chat-openai |
backend-dependent (Token) |
chat.ProviderGemini |
gemini |
chat-gemini |
GEMINI_API_KEY |
chat.ProviderGeminiVertex |
gemini-vertex |
chat-gemini |
application default credentials |
chat.ProviderClaudeLocal |
claude-local |
core (no import) | none (local claude CLI) |
When Config.Provider is empty and the AI_PROVIDER environment variable is
unset, the default provider is ProviderClaude. The single chat-openai
import registers both openai and openai-compatible.
Prompt caching differs by provider too. claude and gemini implement
chat.CachingChatClient with real explicit control; openai implements it but
caches automatically anyway; claude-local does not implement it at all, so the
type assertion tells a caller the truth. Every provider silently declines to
cache content below a per-model minimum. See
Cache a large stable prompt.
Generation controls (Temperature, TopP, Effort) are the one axis where
support is per model rather than per provider. claude-sonnet-4-5 accepts
temperature and refuses effort, while claude-opus-5 does the exact reverse.
A provider with no such concept at all fails at construction; a model refusing
one it structurally supports yields chat.ErrModelRejectedParameter when the
request is made. See
Control sampling & reasoning effort.
All five honour Config.Stateless (they implement chat.StatelessCapable), so
any of them can serve a batch of independent calls from one client. A provider
module older than the flag does not, and chat.New returns an error rather than
building a client that ignores it. See
Process a batch of documents.
The per-provider modules are thin SDK adapters, too tightly coupled to the core to warrant their own documentation sites. Each ships a detailed README, and all provider documentation lives here on the core docs site.
Version compatibility¶
A provider module requires the core version it was built and tested against,
and names it in its own go.mod. That is the whole compatibility statement.
gitlab.com/phpboyscout/go/chat-anthropic v0.9.1
└── requires gitlab.com/phpboyscout/go/chat v0.10.1 <- the version that matters
Do not compare the four version numbers. They move independently: each
module releases when that module changes, so a provider's own version says
nothing about which core it needs. Read its go.mod instead. That is the
statement, and Go enforces it.
The four used to be described as released in lockstep at a shared minor. That was never what protected anyone, and it drifted the moment the dependency bumps were automated. See spec 0012.
How to install a matched pair¶
Ask for the provider module and let it bring the core with it. Each
provider's go.mod requires its matching core, so this can only ever give you a
pair that was built and tested together:
go get gitlab.com/phpboyscout/go/chat-anthropic
go mod tidy # promotes the core to a direct requirement at the chosen version
Asking for gitlab.com/phpboyscout/go/chat@latest separately is the way to end
up broken. It direct-requires a core that may be a minor ahead of every released
provider, and Go's minimum-version selection then keeps that newer core.
What a mismatch actually does¶
Two different failures, depending on which side is ahead.
An older provider, newer core: a compile error. The provider-authoring API
(ResolveAPIKey, UsageTracker, DispatchToolExecution, ValidateMediaSet,
the shape of Config) may change in a minor release while the module is
pre-1.0. v0.8.0 typing Config.ResponseSchema as *jsonschema.Schema is an
example: a provider that still asserts cfg.ResponseSchema.(*jsonschema.Schema)
now fails to build, because a type assertion on a non-interface type is illegal:
That error, or an "imported and not used" for jsonschema beside it, means the
minors have diverged, and nothing about your own code is wrong.
A provider missing a capability marker: a construction error. Several minors have added a marker interface the core type-asserts, and an older provider simply does not implement it. The core then refuses to build a client that asks for the missing capability, rather than accepting the request and quietly not honouring it:
| Added in | Marker | A too-old provider means |
|---|---|---|
v0.2.0 |
StatelessCapable |
Config.Stateless is a construction error |
v0.3.0 |
SamplingCapable, EffortCapable |
Config.Temperature/TopP/Effort are construction errors |
v0.6.0 |
the capability gate over CachingChatClient |
Config.CacheTTL is a construction error |
This class is loud, at construction, and narrow. Everything else keeps working, so it only bites callers using the newer feature. That is the deliberate pattern: a silently ignored control cannot be told apart from one that does nothing.
Go's minimum-version selection (MVS) makes one direction safe and the other the trap:
- You can never end up with an older core than a provider requires. Each provider requires its matching core, and MVS raises the core to satisfy it.
- You can be dragged to a newer core than a provider was built for: if
your module (or any other dependency) directly requires a newer
chatthan the provider modules do, MVS selects that newer core.
So ask for the provider and let it pull the core. That is the one habit worth keeping: it is what makes the safe direction the default, and it does not depend on anyone comparing version numbers.
Capability matrix¶
| Provider | Tool calling | Parallel tools | Structured output | Streaming | Persistence |
|---|---|---|---|---|---|
| Claude | ✓ | ✓ | ✓ tool-based | ✓ | ✓ |
| OpenAI | ✓ | ✓ | ✓ JSON Schema | ✓ | ✓ |
| OpenAI-compatible | ✓ | ✓ | ✓ JSON Schema | ✓ | ✓ |
| Gemini | ✓ | ✓ | ✓ JSON Schema | ✓ | ✓ |
| Claude Local | ✗ | ✗ | ✓ --json-schema |
✗ | ✗ |
Feature-detect the optional capabilities (streaming, persistence) with a type
assertion rather than assuming them, so a claude-local configuration degrades
gracefully.
That table is per provider. Capability is really per model. Two models
from one vendor routinely disagree (claude-sonnet-4-5 accepts temperature
and refuses effort, claude-opus-5 does the reverse) and the difference does
not follow from a version number. Since v0.6.0 each provider module ships a
measured table and the core exposes it:
info := chat.CapabilitiesFor(chat.ProviderOpenAI, "gpt-5.4")
info.Capabilities.Support(chat.CapSampling) // Yes / No / Unknown
Support is three-valued because two of the five providers publish no capability data at all, so "nobody could say" is a real and common answer rather than an edge case. See Check what a model supports for the task, and spec 0006 for why the vocabulary is closed.
Default models¶
When Config.Model is empty, each provider uses its default. The values, with
the constants that carry them, are in
Defaults and limits:
stated once there so they cannot drift apart from the code or from each other.
This page covers why those particular models.
How these are chosen¶
One rule, applied identically to every provider:
The default is the most capable model a provider makes generally available that supports the module's baseline capabilities.
"Most capable" rather than a named tier, because tier names are vendor
nomenclature that does not generalise. OpenAI's current generation ships as
sol/terra/luna, which is not a tier ordering at all. "Generally available"
excludes preview and experimental models: a default must not point at something
a vendor may withdraw. Baseline capabilities are structured outputs, tool use
and multimodal input, all of which this module exposes.
The defaults favour capability over cost. A default is what you get having
expressed no preference, and the reason to keep it current is improved capability
and reasoning. If cost matters, set Config.Model. That is what the field is
for.
Three consequences worth knowing:
Sampling controls no longer work on two of the three defaults.
claude-opus-5 deprecates temperature, and gpt-5.6-sol permits only its
default of 1 and rejects top_p outright, where the previous OpenAI default
gpt-5.4 accepted both. Only Gemini's default still takes them. This is the
generational handover from sampling to reasoning controls, and it is the cost of
staying current: use Config.Effort, which every provider supports, or set
Config.Model to an older model.
You will not discover this in production. Because each provider module ships a
measured table of what its models accept, chat.New drops the setting and names
it in the error it returns, while still handing back a working client. That is
why this default change waited on the capability work rather than shipping ahead
of it: raising the default without it would have moved a working configuration
to a request-time failure. See
Control sampling & reasoning effort.
Gemini's default is a step below the other two. Google ships no
generally-available Pro-tier model: gemini-3.1-pro-preview is preview-only, and
both gemini-3-pro-preview and gemini-2.5-pro have been withdrawn. So
gemini-3.7-flash is the most capable Gemini available at GA, and the gap is
real. Defaulting onto a preview would trade a known capability gap for an
availability risk that has already materialised twice.
gpt-5.6-sol is a judgement, not a derivation. The three gpt-5.6 variants
are indistinguishable in OpenAI's models API (same creation date, same owner)
so the rule cannot pick between them. sol is a recorded choice, revisited if
the variants turn out to be specialised rather than peers.
Defaults move as vendors release, and a move ships in a minor with the change
called out in the changelog so you can see what shifted underneath you. Pin
Config.Model if you would rather they did not.
Claude Local¶
ProviderClaudeLocal routes requests through a locally installed, pre-authenticated
claude CLI binary instead of the Anthropic API, which is valuable where outbound HTTPS
to api.anthropic.com is blocked but the binary is permitted. It ships in the
core, needs no blank import and no API key, and maintains multi-turn continuity
via session IDs captured from the CLI's JSON output (passed back with
--resume). Its trade-offs: no tool calling (SetTools returns an error; MCP
tools are planned), no streaming, no persistence, and no guaranteed token usage.
Setup steps are in Choose & configure a provider.
OpenAI-compatible endpoints¶
ProviderOpenAICompatible targets any backend that speaks the OpenAI API:
Ollama, Groq, Fireworks AI, Together AI, LM Studio, vLLM, and others. Both
BaseURL and Model are required (model names are backend-specific, so there is
no default). Token chunking falls back to the cl100k_base encoding for
unrecognised model names, so non-OpenAI model names are handled gracefully.
Multimodal input¶
Add, Ask, Chat, and StreamChat take a trailing variadic of chat.Media:
images (and, on Gemini, PDF and audio/video) sent alongside the text prompt.
A text-only call passes no media and is unchanged. Each attachment's type is
sniffed from its bytes (never a caller-supplied filename), cross-checked
against any declared MIMEType, allowlisted, and checked against the selected
provider's support before any network call; disguised or unsupported content
is rejected with ErrMediaRejected or ErrMediaUnsupported. Support is
per provider:
| Provider | Images | Audio / video | |
|---|---|---|---|
| Gemini | ✓ | ✓ | ✓ |
| Claude | ✓ | ✓ | ✗ |
| OpenAI | ✓ | ✓ | ✗ |
| Claude Local | ✗ | ✗ | ✗ |
Token usage & cost¶
Every provider surfaces token usage in a provider-neutral chat.Usage struct:
you never touch a vendor SDK's usage type. Read it two ways:
ChatClient.Usage()returns the cumulative total across every provider round-trip made by that client since construction.Config.UsageObserveris an opt-infunc(Usage)fired once per round-trip (synchronously, so keep it fast), for emitting a metric or event.
A single Chat/Ask/StreamChat may make several round-trips (a ReAct loop
makes one per step) and usage is summed across the whole call, which is the
figure you want for cost accounting. Each provider module maps its vendor's
counts onto the neutral shape:
| Provider | Source | Mapping |
|---|---|---|
| Claude | Message.Usage (and the streaming message_delta) |
input_tokens→InputTokens, output_tokens→OutputTokens, cache_read_input_tokens→CachedTokens; TotalTokens computed. |
| OpenAI / compatible | ChatCompletion.Usage |
prompt_tokens→InputTokens, completion_tokens→OutputTokens, total_tokens→TotalTokens, plus cached/reasoning detail. Streaming opts into the final usage chunk automatically. |
| Gemini | GenerateContentResponse.UsageMetadata |
promptTokenCount→InputTokens, candidatesTokenCount→OutputTokens, totalTokenCount→TotalTokens, cached/thoughts mapped. |
| Claude Local | optional usage block of the CLI JSON output |
Surfaced when the binary reports it; otherwise Usage{Known: false}. Do not rely on claude-local for cost accounting. |
A freshly-constructed client, and any provider that reports nothing for a call,
returns a zero-valued Usage with Known == false. Always check Known before
treating counts as authoritative.
Extending the set¶
Any package can register a new provider (and, for fallback, an HTTP-status
extractor) from init(). See
Register a custom provider. The registry is the
seam that keeps the core provider-agnostic while letting the provider set grow.