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.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.
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¶
The core chat module and its provider modules are released in lockstep: a
provider module vX.Y.Z is built and tested against core chat vX.Y.Z, and each
provider's go.mod requires that exact core minor.
Core chat |
chat-anthropic |
chat-openai |
chat-gemini |
|---|---|---|---|
v0.1.x |
v0.1.x |
v0.1.x |
v0.1.x |
Keep all four at the same minor version. While the module is pre-1.0 the
provider-authoring API a provider module depends on (ResolveAPIKey,
UsageTracker, DispatchToolExecution, ValidateMediaSet, …) may change in a
minor release, so a provider module will not necessarily compile against a core
of a different minor.
Go's minimum-version selection (MVS) makes one direction safe and the other a trap worth knowing about:
- 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 newerchatthan the provider modules do, MVS selects that newer core, and a provider built against an older provider-authoring API may fail to compile. Pin matching minors (e.g. allv0.1.x) until the API stabilises atv1.0.
Bumping is mechanical: move the four together.
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.
Default models¶
When Config.Model is empty, each provider uses its default:
| Provider | Default model |
|---|---|
| Claude | claude-opus-4-8 |
| OpenAI | gpt-5.4 |
| Gemini | gemini-3.5-flash |
| OpenAI-compatible | none — Model is required |
| Claude Local | the claude CLI's own default |
Claude Local¶
ProviderClaudeLocal routes requests through a locally installed, pre-authenticated
claude CLI binary instead of the Anthropic API — 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 — 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.