close
Skip to content

chat

A unified, multi-provider AI chat client for Go: Anthropic Claude, a locally installed claude CLI, OpenAI (and any OpenAI-compatible endpoint), and Google Gemini, behind one small interface, as a light, dependency-inverted library.

chat is the same client behind gtb ai, extracted so any project can embed a production chat client, with a ReAct tool-calling loop, streaming, cross-provider fallback, token accounting, and encrypted conversation persistence, without pulling in the go-tool-base framework, and without linking every vendor AI SDK.

import "gitlab.com/phpboyscout/go/chat"

The light-footprint promise

The core module graph is deliberately tiny. go.mod declares phpboyscout/go/errors, invopop/jsonschema, google/uuid, and spf13/afero. No vendor AI SDK, no web framework, no go-tool-base. A depfootprint_test.go guard fails the build if any forbidden dependency (a vendor SDK, Viper/Cobra, Charm, OpenTelemetry, the AWS SDK, or go-tool-base) ever enters the graph.

Each vendor SDK lives in its own module, activated by a blank import, so a Claude-only tool never compiles the OpenAI or Gemini SDKs:

Provider Module (blank-import to activate) Underlying SDK
claude-local gitlab.com/phpboyscout/go/chat (in the core) none (the local claude CLI)
claude gitlab.com/phpboyscout/go/chat-anthropic anthropics/anthropic-sdk-go
openai, openai-compatible gitlab.com/phpboyscout/go/chat-openai openai/openai-go
gemini gitlab.com/phpboyscout/go/chat-gemini google.golang.org/genai

The SDK-free claude-local provider ships in the core and needs no adapter module. It is not the default provider: leave Config.Provider unset and the client asks for claude, which fails with unsupported provider: claude unless chat-anthropic is blank-imported. Set Config.Provider to chat.ProviderClaudeLocal to use it.

The library leans on the standard library at every seam:

  • *slog.Logger for logging (nil ⇒ a discard logger),
  • *http.Client for provider transport (nil ⇒ a plain bounded stdlib client),
  • a KeychainLookup func for credential resolution (nil ⇒ the keychain step is skipped),
  • afero.Fs for conversation persistence.

See Dependency inversion for why.

Who it is for

  • CLI and service authors who want an agentic chat client (tool calling, structured output, streaming) without adopting a framework.
  • Any non-framework Go project that needs to talk to an LLM behind a stable, provider-neutral interface.
  • Cost-conscious builds that want exactly one vendor SDK linked, not four.

Quick start

Blank-import one provider module to activate it, then construct a client from package-owned Settings:

import (
    "context"

    "gitlab.com/phpboyscout/go/chat"
    _ "gitlab.com/phpboyscout/go/chat-anthropic" // activate the Claude provider
)

func summarise(ctx context.Context, apiKey, changelog string) (string, error) {
    client, err := chat.New(ctx, chat.Settings{
        Config: chat.Config{Provider: chat.ProviderClaude, Token: apiKey},
    })
    if err != nil {
        return "", err
    }

    return client.Chat(ctx, "Summarise this changelog in one line:\n"+changelog)
}

With no Token set, the client resolves a credential from config references or the well-known ANTHROPIC_API_KEY env var. See Choose & configure a provider.

Where to go next

The documentation follows the Diátaxis framework:

Using go-tool-base? The framework ships a thin adapter that maps its Props/Viper config (the ai.*, ai.fallback.*, and provider api sections) into the chat.Settings this module owns. That adapter lives in go-tool-base, not here; this module is config-system-agnostic.

Further reading

The blog carries a curated route through this subject: Building with AI collects everything written about it, ordered so you can start at the beginning rather than newest-first.

Ask phpbotscout

phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.

Join the Discord