What this module does not do¶
Documentation usually describes what software does. This page is the other half:
what chat is not for, which combinations do not work, and where a value you
set is ignored. Each item is a decision or a known gap, not an oversight waiting
to be reported.
One client talks to one provider¶
A ChatClient is built for exactly one provider and one model. There is no way
to ask one client to use Claude for prose and Gemini for images, or to route
different calls to different providers by content. Construct one client per
provider and choose between them in your own code.
A fallback composite is not an exception to this. It uses one provider at a time and moves to the next only when the active one fails; it is a resilience mechanism, not a router.
There is no image generation, embedding or audio synthesis¶
The module sends text and attachments to a chat completion endpoint and returns text. Images, PDFs and (on Gemini) audio and video are input only, described in Providers.
There is no API here for generating an image, producing embeddings, transcribing
audio or synthesising speech, and no plan to add one behind ChatClient. Those
are different endpoints with different shapes, and squeezing them into a
chat interface would make the interface worse without making them usable.
Configuration is read once, at construction¶
chat.New reads Config and hands the resolved values to the provider. Nothing
re-reads them. A host that hot-reloads its own configuration does not change
a live client's provider, model, timeout or credentials. Build a new client and
swap it, which also avoids mutating conversation state mid-session.
Nothing bounds a retry sequence as a whole¶
Config.RequestTimeout bounds a single request and Config.MaxRetries bounds
the number of attempts, but there is no setting for "give up after N seconds
total". A provider returning long Retry-After hints, retried twice, can occupy
the caller for considerably longer than the request timeout suggests. Cancel the
context if you need a wall-clock bound.
The module does not pace calls or track quota¶
There is no rate limiter, token-budget accounting or spend cap. Usage() and
Config.UsageObserver report what was consumed after the fact; they do not
refuse a call. When a provider rate-limits, its own retry runs and, if that is
exhausted, the failure surfaces as chat.ErrThrottled. Pacing and budgets are
the caller's, and this is a deliberate boundary: the module has no view of a
caller's other traffic against the same key.
Structured output is not checked at construction¶
Config.ResponseSchema has no marker interface and no construction-time check.
Every provider module shipped here does honour it (including claude-local,
via the CLI's --json-schema) but a custom provider that ignores the field is
not detected, and chat.New will hand back a client that returns unstructured
text. This is unlike Config.Stateless, Config.Effort and Config.CacheTTL,
each of which is type-asserted and fails construction when the provider cannot
carry it.
If you register your own provider and accept a schema, honour it or return an error from your factory. Nothing upstream will catch it for you.
claude-local is missing more than it has¶
ProviderClaudeLocal shells out to the claude CLI, which constrains it
sharply. It does not support:
| Capability | Behaviour on claude-local |
|---|---|
| Tool calling | SetTools returns an error; MCP-based tools are a future release |
| Streaming | does not implement chat.StreamingChatClient; use Chat |
| Persistence | does not implement chat.PersistentChatClient; there is no internal state to snapshot |
| Prompt caching | does not implement chat.CachingChatClient; setting Config.CacheTTL is a construction error |
| Sampling | no --temperature or --top-p flag exists at any CLI version, so setting either is a construction error |
| Media | accepts no attachments; any chat.Media is rejected with chat.ErrMediaUnsupported |
| Token accounting | reports usage only when the binary does; otherwise Usage{Known: false} (do not cost a run from it) |
Four Config fields are also ignored rather than refused, because the CLI has
nowhere to put them: MaxTokens, RequestTimeout, HTTPClient and
MaxRetries. The subprocess is bounded only by the context you pass, so a
claude-local call has no timeout of its own, so pass a context.WithTimeout
if you need one.
What it does support is Config.Effort (all five levels map to the CLI's
--effort), Config.Stateless, Config.SystemPrompt, Config.Model and
Config.ResponseSchema.
A fallback composite carries less than you might expect¶
Four constraints, all consequences of composing clients through the public interface rather than reaching inside them:
- It is never safe for concurrent use, even when every underlying client is stateless and individually safe. The composite advances its active provider mid-call. Give each worker its own composite.
- Replay is lossy. Only user turns are recorded and replayed into a
fallback provider. Assistant turns and the tool-call/tool-result interleaving
cannot be injected through
ChatClient, so a conversation that did heavy tool use resumes with less context than it had.WithStrictToolContextturns that into a fast failure instead. - Streaming fails over only before the first visible event. Once an
EventTextDeltaorEventToolCallStarthas reached your callback it cannot be un-emitted, so any later error is terminal. - Model, token, credentials and base URL do not carry across members. On the
config-driven paths each provider self-resolves those, so a single global
model name is not applied to the whole chain. Pin a model per provider by
passing explicit
Configvalues.
Capability tables lag new models¶
chat.CapabilitiesFor answers from a table each provider module generates by
querying and probing the vendor. A model released after that table was generated
reports SupportUnknown, which the module treats as "proceed and let the
provider answer", so a stale table costs nothing, but it cannot warn you
either. Only a confident SupportNo changes behaviour.
Two of the five providers can never do better than Unknown: claude-local is
a CLI with no models endpoint, and openai-compatible points at an arbitrary
server.
Prompt caching is a hint, never a guarantee¶
AddCached asks a provider to cache a block. Every provider declines below a
per-model minimum (1,024 tokens on Gemini and OpenAI, between 512 and 4,096 on
Claude) and most decline silently: the call succeeds, the content is sent,
nothing is cached, and no error is returned. Usage().CachedTokens staying at
zero is the only reliable signal. Claude also accepts at most four
cached blocks per request.
Media formats are limited by the sniffer, not the vendor¶
An attachment's type is sniffed from its bytes with the standard library's
http.DetectContentType, and only types that sniffer can positively identify
are on the allowlist. Formats it cannot name (mov, flv, wmv, 3gpp,
flac, m4a) are rejected with chat.ErrMediaRejected even where the
vendor would accept them. A declared Media.MIMEType cannot override this;
it is only ever a cross-check against the sniffed family.
Provider modules must match the core's minor version¶
The core and the three provider modules version independently but are not
independently compatible: the provider-authoring API (ResolveAPIKey,
UsageTracker, DispatchToolExecution, ValidateMediaSet, the capability
markers) may change in a minor release while the module is pre-1.0.
The failure mode that bites is a core newer than the provider. Go's
minimum-version selection will raise the core when your module (or any other
dependency) requires a newer chat than the provider modules do, and a
provider built against the older authoring API may then fail to compile.
Requesting the core at @latest while pinning providers is the usual way to
arrive there. Install the provider and let it choose the core, rather than
requiring chat directly.
Specs are not documentation¶
Some pages link to a numbered spec in the project wiki. Those are point-in-time decision records: they say what was decided and what was rejected on the day, and they are not updated as the code moves. If a spec and this site disagree, this site is the one maintained against the code. If this site and the code disagree, the code wins and the page is wrong.
Related¶
- Providers & the per-provider module pattern: the capability matrix these limits qualify.
- Configuration fields: what each setting does when it is supported.
- Errors and sentinels: how each refusal is reported.