chatopenai

package module
v0.17.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 21 Imported by: 0

README

chat-openai

OpenAI (and OpenAI-compatible) provider for the chat client

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Docs: chat.go.phpboyscout.uk


gitlab.com/phpboyscout/go/chat-openai is the OpenAI provider for the gitlab.com/phpboyscout/go/chat multi-provider AI chat client. It registers two providers with the core:

Provider constant Backend
chat.ProviderOpenAI OpenAI's hosted API
chat.ProviderOpenAICompatible Any OpenAI-compatible endpoint (Ollama, vLLM, …) — requires Config.BaseURL and Config.Model

It is a thin adapter over OpenAI's official Go SDK (openai/openai-go/v3). It links no tokenizer, no other vendor AI SDK and not the go-tool-base framework, and not the MCP SDK — a depfootprint_test.go guard enforces that. codex-local's tools need the MCP tool bridge, which a program links itself with import _ "gitlab.com/phpboyscout/go/chat-mcptools".

Install

go get gitlab.com/phpboyscout/go/chat-openai

Usage

Activate the provider with a blank import; the package's init() registers openai and openai-compatible with the core registry, and a status extractor so cross-provider fallback can classify OpenAI HTTP errors.

import (
    "gitlab.com/phpboyscout/go/chat"
    _ "gitlab.com/phpboyscout/go/chat-openai" // activate the OpenAI provider
)

client, err := chat.New(ctx, chat.Settings{
    Config: chat.Config{Provider: chat.ProviderOpenAI, Token: apiKey},
})
if err != nil {
    return err
}
answer, err := client.Chat(ctx, "Summarise this changelog in one line.")
OpenAI-compatible backends

Point at any OpenAI-compatible server by selecting chat.ProviderOpenAICompatible and supplying both a BaseURL and a Model:

client, err := chat.New(ctx, chat.Settings{
    Config: chat.Config{
        Provider: chat.ProviderOpenAICompatible,
        BaseURL:  "https://ollama.example/v1",
        Model:    "llama3.2",
    },
})

Its key comes from Config.Token or Config.Credentials, and with neither set no key is sent. It never reads OPENAI_API_KEY, or any other OpenAI account setting the SDK would pick up from the environment (OPENAI_ADMIN_KEY, OPENAI_ORG_ID, OPENAI_PROJECT_ID, OPENAI_CUSTOM_HEADERS): those belong to api.openai.com, not to the server you've pointed it at.

Keeping up with OpenAI

Two generators, both run deliberately rather than automatically applied.

internal/genmodels regenerates the per-model capability table. OpenAI's /v1/models reports nothing about what a model accepts — no sampling, no effort, no limits — so the generator establishes it by probing each model with a real request:

OPENAI_API_KEY=... go run ./internal/genmodels > models_generated.go

internal/detectmodels watches for OpenAI shipping something chat.DefaultModelOpenAI does not reflect, and raises an issue in this project when it does. Because the listing carries no capability data, it can only report that a model exists — and it says so, rather than letting silence read as "nothing changed".

OPENAI_API_KEY=... go run ./internal/detectmodels -dry-run

It runs weekly — 03:00 Europe/London on Mondays — from a DETECT_MODELS=true scheduled pipeline. Neither -dry-run nor -update-snapshot touches the forge — only a plain run raises. internal/detectmodels/snapshot.json is the committed baseline, and it moves when a human resolves the issue, not when CI runs.

Credentials

The provider resolves an API key through the core's precedence chain (Config.Token → Credentials.Env var → Credentials.Key literal → the well-known OPENAI_API_KEY fallback, under openai only). See the core client's credential docs for the full resolution order.

Capabilities

  • ReAct tool-calling loop with automatic tool dispatch (sequential or parallel)
  • Streaming via StreamChat (text deltas, tool-call start/end, completion)
  • Structured output through Ask and JSON-schema response formats
  • Image and PDF media parts (image → image_url, PDF → file part)
  • Conversation Save/Restore snapshots
  • Stateless one-shot calls via Config.Stateless — each call carries only its own turns, so one client can serve a batch of independent documents across goroutines instead of re-sending the accumulated prefix every time
  • Token-usage accounting via client.Usage()

Documentation

Version compatibility

chat-openai requires the core version named in its own go.mod, and is built and tested against exactly that version. Do not compare the two version numbers — each module releases when that module changes, so they move independently.

Install this module and let it bring the core with it:

go get gitlab.com/phpboyscout/go/chat-openai
go mod tidy

See version compatibility for what a mismatch does, and why requiring chat directly at @latest is the way to break it.

Licence

MIT — see LICENSE.

Documentation

Index

Constants

View Source
const EnvOpenAIKey = "OPENAI_API_KEY"

EnvOpenAIKey is the well-known unprefixed environment variable used as the ecosystem fallback when no explicit credential is configured.

Variables

View Source
var DefaultCodexEnvironment = []string{"PATH", "HOME", "CODEX_HOME"}

DefaultCodexEnvironment is what the subprocess inherits unless a caller adds to it with WithCodexEnvironment.

An allowlist rather than a denylist, for the reason spec 0021 D5 records: a CLI reads its own authentication from the environment, and naming the variables to remove is a denylist against a third party's binary that fails open. CODEX_HOME joins PATH and HOME because it is where codex keeps its credentials and its configuration, including the permission profile D5 deliberately honours.

Functions

func New added in v0.9.0

func New(ctx context.Context, settings chat.Settings, opts ...Option) (chat.ChatClient, error)

New builds an OpenAI (or OpenAI-compatible) client, accepting options this provider owns. Reach for it when you need one; otherwise chat.New with the provider name does the same job through the registry.

It goes through chat.NewWithFactory rather than calling build directly, so it gets every guard chat.New applies: the endpoint is validated before credentials reach it, the logger this package logs through is resolved, provider defaults are applied, unapplicable settings are dropped and reported, and the built client is asserted able to carry what survived.

Calling build directly is what this used to do. An http:// endpoint was accepted with a nil error and the API key then went over the wire in cleartext, and a call with no Logger panicked. See https://gitlab.com/phpboyscout/go/chat-openai/-/issues/2 and spec 0015.

func NewCodexLocal added in v0.15.0

func NewCodexLocal(ctx context.Context, settings chat.Settings, opts ...CodexLocalOption) (chat.ChatClient, error)

NewCodexLocal builds a client backed by a locally installed, signed-in codex CLI. No API key is required, and none is passed: the subprocess receives DefaultCodexEnvironment only.

Reach for it directly when you need one of the CodexLocalOption knobs; otherwise chat.New with Provider set to chat.ProviderCodexLocal does the same job through the registry.

Types

type CodexLocal added in v0.15.0

type CodexLocal struct {
	chat.UsageTracker
	// contains filtered or unexported fields
}

CodexLocal implements chat.ChatClient by driving a locally installed codex CLI, the way ClaudeLocal drives claude.

func (*CodexLocal) Add added in v0.15.0

func (c *CodexLocal) Add(_ context.Context, prompt string, media ...chat.Media) error

Add buffers a user message to be prepended to the next Chat or Ask call.

func (*CodexLocal) Ask added in v0.15.0

func (c *CodexLocal) Ask(ctx context.Context, question string, target any, media ...chat.Media) error

Ask sends a question and unmarshals the structured response into target, using --output-schema when the config carries one.

func (*CodexLocal) Chat added in v0.15.0

func (c *CodexLocal) Chat(ctx context.Context, prompt string, media ...chat.Media) (string, error)

Chat sends a message to the local codex binary and returns the text response.

func (*CodexLocal) History added in v0.15.0

func (c *CodexLocal) History() chat.History

History reports what this client is carrying. Known is false: the conversation lives in codex's own thread, resumed by id, so this client cannot enumerate the far side's turns.

func (*CodexLocal) SetTools added in v0.15.0

func (c *CodexLocal) SetTools(tools []chat.Tool) error

SetTools registers the tools the model may call.

The handlers stay in this process and are published per call on a loopback MCP server; codex is pointed at it with a -c override. Registering an empty set turns the feature off. Spec 0009 and 0022.

func (*CodexLocal) SupportsStateless added in v0.15.0

func (c *CodexLocal) SupportsStateless()

SupportsStateless marks CodexLocal as honouring Config.Stateless: a stateless call omits the resume subcommand and discards the thread id.

type CodexLocalOption added in v0.15.0

type CodexLocalOption func(*codexLocalOptions)

CodexLocalOption configures a chat.ProviderCodexLocal client at construction. These knobs live here rather than on chat.Config because only one provider reads them (spec 0011 D10).

func WithCodexBinaryLookup added in v0.15.0

func WithCodexBinaryLookup(lookPath func(string) (string, error)) CodexLocalOption

WithCodexBinaryLookup overrides how the codex binary is located, replacing exec.LookPath. It is the seam for testing the not-installed path.

func WithCodexCommand added in v0.15.0

func WithCodexCommand(command func(context.Context, string, ...string) *exec.Cmd) CodexLocalOption

WithCodexCommand overrides how the codex subprocess is built, replacing exec.CommandContext. It is the seam that drives the provider against a fake subprocess.

func WithCodexEnvironment added in v0.15.0

func WithCodexEnvironment(names ...string) CodexLocalOption

WithCodexEnvironment names further environment variables to pass to the subprocess, on top of DefaultCodexEnvironment.

func WithIgnoredUserConfig added in v0.15.0

func WithIgnoredUserConfig() CodexLocalOption

WithIgnoredUserConfig discards the user's own codex configuration, so the client behaves the same on every machine.

Off by default, and the default is the interesting half. A codex user's restrictions live in their own config, which is what the CLI's /permissions command edits; passing --ignore-user-config throws that away and runs with codex's defaults instead. For claude the equivalent flag hardens, because the restriction comes from the flag. For codex it does the opposite.

Reach for it when you would rather have identical behaviour everywhere than inherit a machine's configuration, and understand that what you are making reproducible is the absence of that machine's restrictions. See spec 0022 D5.

func WithToolServer added in v0.17.0

func WithToolServer(server chat.ToolServer) CodexLocalOption

WithToolServer gives this client its own server for the tools passed to SetTools, in place of the program's registered one. It works with none registered, so a test fake or a wrapper that audits one client's tool calls needs no program-wide change. Spec 0028 D8.

type OpenAI

type OpenAI struct {
	chat.UsageTracker
	// contains filtered or unexported fields
}

func (*OpenAI) Add

func (a *OpenAI) Add(ctx context.Context, prompt string, media ...chat.Media) error

func (*OpenAI) AddCached added in v0.5.0

func (a *OpenAI) AddCached(ctx context.Context, prompt string, media ...chat.Media) error

AddCached appends a user turn. OpenAI caches eligible prefixes automatically and reliably — measured caching on every call after the first, covering 88% of a ~4,400-token prefix on gpt-5.4 and 99.7% on gpt-5.6 — so there is no annotation to send and this is Add with a documented intent.

The intent is not wasted: it tells the client a stable prefix exists, which is what prompt_cache_key is derived from.

func (*OpenAI) ApplyPolicyNow added in v0.11.0

func (a *OpenAI) ApplyPolicyNow(ctx context.Context, policy chat.HistoryPolicy) (int, error)

ApplyPolicyNow bounds the retained conversation immediately, rather than waiting for the next request to do it.

It is the same sequence this client runs before every request — describe the turns, ask the policy, rewrite from the result — with two differences. There is no pending turn to hold out of the budget, because nothing is about to be answered. And the system turn is held out as it always is: it lives in the message list on this API but it is configuration, and a command changes what is in a conversation rather than how the client is configured.

A nil policy means the configured one. With none configured there is nothing to apply and it returns zero rather than an error.

func (*OpenAI) Ask

func (a *OpenAI) Ask(ctx context.Context, question string, target any, media ...chat.Media) error

Ask sends a question to the OpenAI chat client and expects a structured response which is unmarshalled into the target interface.

func (*OpenAI) Chat

func (a *OpenAI) Chat(ctx context.Context, prompt string, media ...chat.Media) (string, error)

Chat sends a message and returns the response content. It handles tool calls internally.

func (*OpenAI) History added in v0.9.0

func (a *OpenAI) History() chat.History

History reports the conversation this client will re-send, counting the committed turns and anything buffered by Add and not yet sent. A call still in flight counts once it commits (spec 0026 D8).

The system turn is included when there is one: it is re-sent on every call and costs input tokens like any other turn, so a caller watching for growth should see it.

Known is true — this provider owns its transcript, so the count is authoritative.

func (*OpenAI) Model added in v0.6.0

func (o *OpenAI) Model() string

Model implements chat.ModelIdentifier.

func (*OpenAI) Provider added in v0.6.0

func (o *OpenAI) Provider() chat.Provider

Provider implements chat.ModelIdentifier, reporting which of the two registered providers this client was built for.

func (*OpenAI) Restore

func (a *OpenAI) Restore(snapshot *chat.Snapshot) error

Restore replaces the current conversation state with a previously saved snapshot.

func (*OpenAI) Save

func (a *OpenAI) Save() (*chat.Snapshot, error)

Save captures the current OpenAI conversation state as a snapshot.

A stateless client retains no conversation, so its snapshot carries only the system prefix it starts every call from.

func (*OpenAI) SetTools

func (a *OpenAI) SetTools(tools []chat.Tool) error

SetTools configures the tools available to the AI.

func (*OpenAI) StreamChat

func (a *OpenAI) StreamChat(ctx context.Context, prompt string, callback chat.StreamCallback, media ...chat.Media) (string, error)

StreamChat implements StreamingChatClient.

func (*OpenAI) SupportsEffort added in v0.3.0

func (a *OpenAI) SupportsEffort()

SupportsEffort marks OpenAI as able to carry Config.Effort, via reasoning_effort. OpenAI's ladder is a superset of the neutral one, so all five rungs map directly.

func (*OpenAI) SupportsSampling added in v0.3.0

func (a *OpenAI) SupportsSampling()

SupportsSampling marks OpenAI as able to carry Config.Temperature and TopP.

Structural only: gpt-5.4 accepts both, but reasoning models in general are where vendors have been withdrawing sampling controls, so a model that refuses yields chat.ErrModelRejectedParameter at request time.

func (*OpenAI) SupportsStateless added in v0.2.0

func (a *OpenAI) SupportsStateless()

SupportsStateless marks OpenAI as honouring chat.Config.Stateless.

type Option added in v0.9.0

type Option func(*options)

Option configures an OpenAI client at construction.

These are knobs only this provider understands. The core's Config carries what every provider means the same way; anything one provider owns belongs to that provider's own constructor, where the compiler can see it. See https://gitlab.com/phpboyscout/go/chat/-/wikis/specs/0011-chat-provider-conformance D10.

func WithExternalAuth added in v0.12.0

func WithExternalAuth(opts ...option.RequestOption) Option

WithExternalAuth declares that the caller is supplying authentication, and applies the request options it is given.

It does two things that have to happen together. This module stops sending credentials of its own, and Config.Token stops being required. Both are necessary to reach a backend whose authentication is not a bearer token: Azure OpenAI takes an api-key header or an Entra ID token, and the SDK refuses either alongside option.WithAPIKey with "Azure authentication cannot be combined with option.WithAPIKey or option.WithAdminAPIKey".

It is one option rather than two because the halves are never useful apart. A caller that injected options while leaving the default key in place would build a client that fails on its first request rather than at construction.

A Config.Token set alongside it is refused rather than ignored, because a credential that silently does nothing is worse than an error naming it.

This is the seam the Azure sibling module delegates through, so that reaching Azure costs this module no dependency on the Azure SDK. See spec 0019 D3 and D10.

One limit worth knowing. Under openai, the SDK still falls back to OPENAI_API_KEY from the environment unless one of the options disables that, and the switch for it (requestconfig.WithEnvironmentDefaultsDisabled) is internal to the SDK, so this module cannot apply it. azure.WithEndpoint carries it already, so the Azure path is unaffected, and under openai-compatible this module clears it. A caller passing only headers under openai, in an environment exporting OPENAI_API_KEY, can still inherit it.

func WithSeed added in v0.9.0

func WithSeed(seed int64) Option

WithSeed pins the sampling seed, asking the backend for reproducible-ish completions. Unset, no seed is sent and the backend samples normally.

It replaces chat.Config.Seed, removed from the core in v0.10.0: it was an OpenAI-only field that every other provider carried and none could read.

Directories

Path Synopsis
internal
detectmodels command
Command detectmodels reports when OpenAI has shipped something the module's default model does not reflect.
Command detectmodels reports when OpenAI has shipped something the module's default model does not reflect.
genmodels command
Command genmodels regenerates the per-model capability table.
Command genmodels regenerates the per-model capability table.
modelfilter
Package modelfilter selects the general-purpose chat models from OpenAI's model listing.
Package modelfilter selects the general-purpose chat models from OpenAI's model listing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL