Skip to content

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 newer chat than 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. all v0.1.x) until the API stabilises at v1.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 noneModel 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 PDF 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.UsageObserver is an opt-in func(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_tokensInputTokens, output_tokensOutputTokens, cache_read_input_tokensCachedTokens; TotalTokens computed.
OpenAI / compatible ChatCompletion.Usage prompt_tokensInputTokens, completion_tokensOutputTokens, total_tokensTotalTokens, plus cached/reasoning detail. Streaming opts into the final usage chunk automatically.
Gemini GenerateContentResponse.UsageMetadata promptTokenCountInputTokens, candidatesTokenCountOutputTokens, totalTokenCountTotalTokens, 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.