AI Chat¶
The multi-provider AI chat client has been extracted into the standalone
gitlab.com/phpboyscout/go/chat
module (plus per-provider modules: chat-anthropic, chat-openai,
chat-gemini, chat-bedrock, chat-openai-azure). Its full
documentation: the ChatClient API, the ReAct tool-calling loop, streaming,
cross-provider fallback, conversation persistence, multimodal input, token-usage
accounting, and the provider capability matrix. Now lives at:
go-tool-base consumes the module through a thin adapter in
pkg/chat; this page documents only that adapter. See the
migration note for the
module map and how to consume the light client directly.
What the GTB adapter adds¶
The module is deliberately config-system-agnostic. pkg/chat layers GTB's
framework integration on top:
- Props construction.
chat.NewFromProps(ctx, p, cfg)andchat.NewWithFallbackFromProps(...)map aPropsinstance and GTB's layered config (read through a pinnedprops.Config.View()) into the module's typedchat.Settings, then call the module constructor.chat.SettingsFromPropsexposes just the mapping. - The GTB config-key schema. The adapter owns the config keys and their precedence; the module knows nothing about them:
| Provider | Literal key | Env-var-reference key | Keychain key | Ecosystem fallback env |
|---|---|---|---|---|
| Claude | anthropic.api.key |
anthropic.api.env |
anthropic.api.keychain |
ANTHROPIC_API_KEY |
| OpenAI | openai.api.key |
openai.api.env |
openai.api.keychain |
OPENAI_API_KEY |
| Gemini | gemini.api.key |
gemini.api.env |
gemini.api.keychain |
GEMINI_API_KEY |
| Azure OpenAI | azure.api.key |
azure.api.env |
azure.api.keychain |
AZURE_OPENAI_API_KEY |
openai-compatible shares OpenAI's root but never reads OPENAI_API_KEY,
which belongs to api.openai.com. gemini-vertex authenticates with Google
application default credentials and takes no key. The local CLIs
(claude-local, codex-local, agy-local) and bedrock carry no GTB
credential. The provider is chosen by ai.provider (or AI_PROVIDER when
that is unset); fallback is configured under ai.fallback.*.
- One credential walk, shared with doctor. The adapter hands each
provider a chat.CredentialSource over the same walk doctor reports from:
env-var reference → OS keychain → literal → ecosystem env var. So doctor
and the client cannot disagree about where a key comes from. Nothing is read
until the provider asks. A keychain entry that cannot be read, with nothing
below it, fails chat.New with the reason rather than reading as a missing
key, and one with a literal below it is refused as a secure-store
regression (see credentials).
A caller's own Config.Token or Config.Credential is left alone. The
recommended path (env-var reference) keeps the literal secret out of the
config file.
- The whole ai: section reaches the client. ai.model, ai.base_url,
ai.api_version, ai.project and ai.location fill the matching
chat.Config fields when the caller left them empty, so openai-compatible,
azure-openai, gemini-vertex and bedrock are configurable from a file.
They apply to ai.provider only. A fallback chain is built by the module's
NewWithFallbackSettings: the primary keeps its model and addressing, every
other member resolves its own, and GTB supplies each member's credential
source through WithProviderCredentialSources. GTB used to derive the chain itself with a
denylist that cleared the primary's model and would have carried addressing
to every member.
- Three layers, three owners. A generated tool's chat configuration is
three things that used to blur together. The author's defaults (default
provider, model, endpoint) live in the manifest's chat.default block and
ship as an embedded defaults bundle beside cmd/<name>/chat.go, the lowest
config layer. The end user's overrides live in their own config file,
written by init ai or by hand, and win over the author's. Credentials
are only ever the end user's, in their file or their environment; nothing
the author records carries one. See spec 0196 and the
generate reference.
- Hardened HTTP seam. The adapter injects the hardened transport from
go/httpclient, so GTB tools get the framework's security posture; the
module core carries none of it.
- No provider registered here. A provider is a blank import in the binary
that ships it (spec 0194): cli/cmd/gtb/providers.go links every module, and
a generated tool links the ones its manifest selects. A hand-wired tool adds
the imports itself; see the
migration note.
- Provider to module table. chat.ProviderModule(p) and
chat.ProviderModules() name the module whose blank import registers each
chat.Provider. The generator, doctor and error hints read this one table.
When chat.New fails because a provider is not registered
(gochat.ErrProviderNotRegistered), the adapter adds a hint naming the
import to add.
- The tool bridge. claude-local and codex-local serve their tools to
the CLI over MCP through chat.ToolServerModule (go/chat-mcptools), and
refuse tools unless the binary blank-imports it. chat.NeedsToolServer(p)
says which providers need it; the generator emits the import for them
unless the manifest sets chat.tool_bridge: false, and
cli/cmd/gtb/providers.go links it.
- Linked providers are a feature-set question. chat.LinkedProviders(set)
is how init ai and doctor ask what this binary ships. A generated tool
declares its manifest's providers as link-kind features
(props.DeclareLinks(props.ChatLinkPrefix, ...) in its chat.go), so the
answer is the author's choice rather than every provider a linked module
registers; a declared provider the registry lacks is reported as unlinked.
A tool declaring none is read from gochat.RegisteredProviders alone, the
same way the forge side falls back to forge.Registered
(#81).