title: Choose & configure a provider description: The chat.Config fields, how each provider is activated and credentialed, and the two that need extra setup: openai-compatible and claude-local. tags: [how-to, configuration, providers, credentials]
Choose & configure a provider¶
This guide covers the chat.Config fields, how each provider is activated and
credentialed, and the two providers that need extra setup: openai-compatible
(a required BaseURL and Model) and claude-local (a local CLI binary).
The fields that choose a provider¶
chat.Config carries provider behaviour and is passed inside chat.Settings to
chat.New. Only Provider is effectively required, and even that defaults;
everything else has a working zero value.
Five fields decide which provider you get and how it authenticates:
| Field | Type | Description |
|---|---|---|
Provider |
Provider |
The provider constant. Defaults to ProviderClaude when unset and AI_PROVIDER is not set. |
Model |
string |
Model name. Falls back to a per-provider default when empty. Required for ProviderOpenAICompatible. |
Token |
string |
API key. Optional when resolvable from config references or a well-known env var. |
Credentials |
CredentialConfig |
Credential references (env-var name, keychain ref, literal) supplied by the host. Token wins when both are set. |
BaseURL |
string |
API endpoint override. Required for ProviderOpenAICompatible. Validated by ValidateBaseURL. |
Every other field (sampling, effort, caching, timeouts, the tool loop, the test seams) is in Configuration fields, with what each one defaults to and what happens when the value cannot be applied. Signatures are on pkg.go.dev.
Activate a provider by blank import¶
Each API provider lives in its own module. Blank-import it once (typically in
main) to register it with the core; then reference it by its Provider
constant.
| Provider constant | String | Blank import | Credential env var |
|---|---|---|---|
chat.ProviderClaude |
claude |
_ "gitlab.com/phpboyscout/go/chat-anthropic" |
ANTHROPIC_API_KEY (chatanthropic.EnvClaudeKey) |
chat.ProviderOpenAI |
openai |
_ "gitlab.com/phpboyscout/go/chat-openai" |
OPENAI_API_KEY (chatopenai.EnvOpenAIKey) |
chat.ProviderOpenAICompatible |
openai-compatible |
_ "gitlab.com/phpboyscout/go/chat-openai" |
backend-dependent (Token) |
chat.ProviderGemini |
gemini |
_ "gitlab.com/phpboyscout/go/chat-gemini" |
GEMINI_API_KEY (chatgemini.EnvGeminiKey) |
chat.ProviderGeminiVertex |
gemini-vertex |
_ "gitlab.com/phpboyscout/go/chat-gemini" |
application default credentials, plus Project and Location |
chat.ProviderBedrock |
bedrock |
chat-bedrock, not yet released |
the AWS credential chain, plus Location |
chat.ProviderClaudeLocal |
claude-local |
_ "gitlab.com/phpboyscout/go/chat-anthropic" |
none, the claude binary carries its own login |
The openai and openai-compatible providers are both registered by the single
chat-openai import.
Resolve a credential¶
Every API provider resolves its key through one shared five-step cascade
(chat.ResolveAPIKey). The first non-empty source wins; each step is trimmed so
a half-configured value cannot mask a fully-configured one below it:
- Direct token.
Config.Token. - Env-var reference.
Config.Credentials.Envnames an env var; the value is read withos.Getenv. The secret stays out of any config file. - Keychain reference.
Config.Credentials.Keychainis a"service/account"pair resolved through the host-injectedConfig.Credentials.Lookupfunc. With no lookup wired, this step is skipped. - Literal.
Config.Credentials.Key, an inline value (legacy; least preferred). - Well-known env var.
ANTHROPIC_API_KEY/OPENAI_API_KEY/GEMINI_API_KEY, the ecosystem fallback used by most SDKs and CI platforms.
The simplest path is to export the well-known env var and leave Token and
Credentials empty:
import (
"gitlab.com/phpboyscout/go/chat"
_ "gitlab.com/phpboyscout/go/chat-anthropic"
)
// ANTHROPIC_API_KEY is set in the environment; nothing else needed.
client, err := chat.New(ctx, chat.Settings{
Config: chat.Config{Provider: chat.ProviderClaude},
})
A host that keeps secrets in a config file or keychain populates
Config.Credentials instead. See credential security.
Using go-tool-base? Its adapter maps the framework's
ai.provider,AI_PROVIDER, and providerapiconfig sections onto these fields for you; that adapter lives in go-tool-base, not this module.
OpenAI-compatible endpoints¶
ProviderOpenAICompatible targets any backend that speaks the OpenAI API:
Ollama, xAI, Fireworks AI, Together AI, LM Studio, vLLM, and others. It is
registered by the chat-openai import. Both BaseURL and Model are
required (there is no default model, because names are backend-specific), and the
BaseURL must pass endpoint validation.
import (
"os"
"gitlab.com/phpboyscout/go/chat"
_ "gitlab.com/phpboyscout/go/chat-openai"
)
// Ollama (local)
ollama, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderOpenAICompatible,
BaseURL: "http://localhost:11434/v1",
Model: "llama3.2",
Token: "ollama", // Ollama ignores the token; any non-empty value works
// A localhost HTTP endpoint is non-HTTPS; permitted only in tests.
AllowInsecureBaseURL: true,
}})
// xAI (cloud, HTTPS)
grok, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderOpenAICompatible,
BaseURL: "https://api.x.ai/v1",
Model: "grok-4.3",
Token: os.Getenv("XAI_API_KEY"),
}})
Both of these have been run for real, not just against the test stub: Ollama
and xAI each pass chat, streaming, the tool loop, structured output and
schema-less Ask through this provider. The evidence, with dates and versions,
is in snippet 6049842.
The other names above are the same wire protocol and nothing more; treat them as
plausible until one is run.
Non-HTTPS endpoints.
ValidateBaseURLrejectshttp://unlessAllowInsecureBaseURLis set, which is intended forhttptest.Servertargets only. For a plaintext local model, prefer an HTTPS proxy in production; the flag exists so tests can point at a local server, not as a way to disable TLS generally.
Token chunking falls back to the cl100k_base encoding for model names the
tokenizer does not recognise, so Ollama and other non-OpenAI model names are
handled gracefully.
Cloud-credentialled providers (Bedrock, Vertex)¶
Two providers do not take an API key at all. They authenticate through their
cloud's credential chain, which is a resolution order rather than a value, so
there is no Config.Token to set and supplying one is an error rather than an
alternative.
AWS Bedrock¶
client, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderBedrock,
Location: "eu-west-2", // or AWS_REGION, or anything the AWS chain resolves
}})
Credentials come from the standard AWS chain: environment, shared profile, instance role, SSO, or a web identity token in CI. Nothing needs configuring here if the chain already works.
Config.Location is the region. An explicit value wins; an empty one falls back
to the chain. Construction fails when neither supplies one, rather than
guessing.
Three things behave differently from the API-key providers:
- Model access is granted per account and per region, so a model that works
for one caller returns
AccessDeniedExceptionfor another. That error has several unrelated causes, including an organisation policy rather than model access, so read the message rather than assuming which. - Capability varies per model, not per provider. Ask
Capabilities(); a model that cannot honourResponseSchema, tools or media refuses at construction rather than mid-request. - There is no
BaseURL. Bedrock addresses by region, andConfig.Modelcarries whichever form your region needs: a model ID, an inference profile ID, or an ARN.
Gemini on Vertex AI¶
client, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderGeminiVertex,
Project: "acme-prod", // or GOOGLE_CLOUD_PROJECT
Location: "europe-west2", // or GOOGLE_CLOUD_LOCATION
}})
Authentication is Google application default credentials. Both project and location are required: an explicit field wins, the standard variable is the fallback, and construction fails when neither supplies one.
Setting GOOGLE_GENAI_USE_VERTEXAI while configured for ProviderGemini is
refused at construction. The SDK reads that variable and would switch backend
silently, which is a configuration the module could not report or validate.
Azure OpenAI¶
Azure takes a caller-owned endpoint, a dated api-version, and a deployment name in place of a model name.
import (
"os"
"gitlab.com/phpboyscout/go/chat"
_ "gitlab.com/phpboyscout/go/chat-openai-azure"
)
client, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderAzureOpenAI,
BaseURL: "https://my-resource.openai.azure.com",
APIVersion: "2024-10-21",
Model: "my-deployment-name", // the deployment, not the model
Token: os.Getenv("AZURE_OPENAI_API_KEY"),
}})
All four fields are required, and each fails at construction rather than on the first request.
Config.Model is the deployment name. On Azure you deploy a model and give
that deployment a name, and the API addresses the name. It is frequently not the
model's name, so my-gpt4-prod is an ordinary deployment of gpt-4o. There is
no default, because only you know what you called it.
Config.APIVersion has no default and never will. Azure's versions are
dated, and which models exist varies between them, so a default would be correct
today and quietly wrong later. Use the version your deployment expects.
Config.Token is the Azure api-key, sent as an api-key header rather than
a bearer token. Unlike every other provider here, no environment variable is
consulted: an OPENAI_API_KEY in your shell is neither read nor sent.
Entra ID instead of an api-key¶
Pass a credential to the module's own constructor:
import chatazure "gitlab.com/phpboyscout/go/chat-openai-azure"
client, err := chatazure.New(ctx, settings, chatazure.WithTokenCredential(cred))
cred is any azcore.TokenCredential. The module takes one rather than
resolving one, which is what keeps azidentity out of its dependencies. For
ambient discovery, resolve it with
azureclient and pass the
result in.
Set the api-key or the credential, not both. Both together is refused at construction, and so is neither.
What Azure reports about capability¶
Azure delegates capability reporting to openai-compatible, and from
chat-openai v0.12.1 that answers SupportUnknown for everything
model-dependent: tools, structured output, multimodal, sampling, effort and
caching. A deployment name resolves to no known model, so nothing better is
available.
Nothing is refused on that basis, since unknown proceeds untouched. What you lose is being warned in advance; what you gain is not being promised support nobody checked. See where capability answers come from.
Claude Local (the claude CLI)¶
ProviderClaudeLocal routes requests through a locally installed, signed-in
claude CLI binary instead of the Anthropic API. It is valuable in secure or
air-gapped environments where outbound HTTPS to api.anthropic.com is blocked
but the binary is permitted. It is registered by the chat-anthropic import,
alongside ProviderClaude, and takes no API key of its own.
Requirements:
- The
claudeCLI installed and onPATH. - Signed in once (
claude login).
import (
"gitlab.com/phpboyscout/go/chat"
_ "gitlab.com/phpboyscout/go/chat-anthropic"
)
client, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderClaudeLocal,
Model: "claude-sonnet-4-6", // optional; uses claude's default if empty
SystemPrompt: "You are a helpful assistant.",
}})
Which account the CLI bills¶
The subprocess is given PATH and HOME and nothing else, so the CLI
authenticates with the login you established with claude login.
That is deliberate. The CLI prefers an ANTHROPIC_API_KEY found in its
environment over its own stored login, so inheriting the host's environment let
an unrelated variable decide which account paid, silently. Around thirty of the
CLI's environment variables route authentication or billing, and the set grows
between versions, which is why the provider passes an allowlist rather than
removing names from one.
If your CLI needs more, name what it needs:
import chatanthropic "gitlab.com/phpboyscout/go/chat-anthropic"
client, err := chatanthropic.NewClaudeLocal(ctx, settings,
chatanthropic.WithClaudeEnvironment("HTTPS_PROXY", "ANTHROPIC_CONFIG_DIR"),
)
Multi-turn continuity is maintained via session IDs captured from the CLI's JSON
output and passed back with --resume. Two limitations set claude-local apart:
it does not stream, and its tool calling takes a different route (see below). It also reports no reliable token
usage. See Providers.
Tools you register¶
SetTools works, and the handlers stay in your process. Each call publishes them
on a loopback MCP server, points the CLI at it with --mcp-config, and takes it
down again when the call returns, so nothing needs closing and no port is held
between calls. Dispatch, panic recovery and result marshalling are the core's, so
a tool behaves exactly as it would on an API provider.
The server is reachable only from 127.0.0.1, on an ephemeral port, and only
with a per-call bearer token that lives in an 0600 file rather than on the
command line where ps would show it. Each registered tool is named in
--allowedTools, so the pre-authorisation is exactly as wide as what you
registered.
It needs a claude CLI that supports --mcp-config; verified against 2.1.260.
Codex Local and Agy Local¶
Two more CLIs are driven the same way, each registered by the module that already carries its vendor's API:
import (
"gitlab.com/phpboyscout/go/chat"
_ "gitlab.com/phpboyscout/go/chat-openai" // registers codex-local
_ "gitlab.com/phpboyscout/go/chat-gemini" // registers agy-local
)
codex, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderCodexLocal,
}})
agy, err := chat.New(ctx, chat.Settings{Config: chat.Config{
Provider: chat.ProviderAgyLocal,
}})
Neither takes a model by default: each CLI owns its own and names models this
module has no catalogue for. Set Config.Model to name one yourself.
codex-local accepts image attachments, which no other local-CLI provider
does, and it carries tools you register. agy-local carries neither. agy has
no per-invocation MCP configuration, so SetTools returns an error and
Capabilities reports CapTools: SupportNo; use chat.ProviderGemini for tool
calling against the same models. agy-local does report the fullest token usage
of the three, and is the only one whose Usage.Known is true.
codex-local honours your own codex configuration, including the permissions
you set through the CLI's /permissions command. Pass
chatopenai.WithIgnoredUserConfig() for identical behaviour on every machine
instead, understanding that what becomes reproducible is the absence of that
machine's restrictions.
What the model on the far side can reach¶
SetTools returning an error does not mean the model cannot act. The claude
CLI is an agentic tool and ships its own, so this provider runs it with
--restricted --strict-mcp-config: no command execution, no WebFetch, no user,
project or local settings files, and none of the MCP servers the machine has
configured. Measured against claude 2.1.260:
| default | WithUnrestrictedCLI() |
|
|---|---|---|
| read a file in the working directory | yes | yes |
| read a file outside it | no | no |
| run a shell command | no | yes |
The residual is the working directory. The CLI's file tools still read inside it, so do not run a local-CLI provider from a directory whose contents the prompt's author should not see. This matters because a chat prompt is often not trusted: every other provider sends text to somebody else's API, where the worst case is a bad answer.
And claude is the only one of the three that can be told to give up command
execution at all. codex-local and agy-local pass the strongest restraint
their CLIs offer and a prompt can still run commands as you. The comparison, with
what each provider passes, is in
what a local-CLI provider can and cannot promise.
To run the CLI as it behaves in a terminal, opt back in:
Related¶
- Architecture & the seams: how
chat.Newdispatches to a provider. - Providers: the full capability matrix and per-provider notes.
- Provider-endpoint & credential security: BaseURL validation and credential handling.