Skip to main content

AI Contracts

The ai package defines provider-neutral contracts for chat models, tools, agents, messages, and streams. The framework does not ship a concrete model provider by default; applications or provider packages register implementations through the public registry.

Messages

type Message struct {
Role Role
Content string
ToolCalls []ToolCall
ToolResult *ToolResult
Usage *TokenUsage
}

Roles are exported constants: RoleSystem, RoleUser, RoleAssistant, and RoleTool. Helper constructors cover the common cases:

messages := []*ai.Message{
ai.NewSystemMessage("Answer briefly."),
ai.NewUserMessage("Summarize this invoice."),
}

Use NewAssistantMessageWithToolCalls(...) for assistant tool requests and NewToolMessage(callID, content) for tool results; NewAssistantMessage(...) builds a plain assistant response. Message.IsSystem, IsUser, IsAssistant, IsTool, and HasToolCalls are convenience predicates for routers and tests.

Helper constructors populate exact fields:

HelperFields set
NewSystemMessage(content)Role: RoleSystem, Content: content
NewUserMessage(content)Role: RoleUser, Content: content
NewAssistantMessage(content)Role: RoleAssistant, Content: content
NewAssistantMessageWithToolCalls(content, toolCalls)Role: RoleAssistant, Content: content, ToolCalls: toolCalls
NewToolMessage(callID, content)Role: RoleTool, ToolResult: &ToolResult{CallID: callID, Content: content}; top-level Message.Content is not populated

Models

type ChatModel interface {
Generate(ctx context.Context, messages []*Message, opts ...Option) (*Message, error)
Stream(ctx context.Context, messages []*Message, opts ...Option) (MessageStream, error)
}

type ToolableChatModel interface {
ChatModel
WithTools(tools ...Tool) ToolableChatModel
}

WithTools follows an immutable pattern: it returns a model instance with tools bound and does not mutate the current one.

Runtime options are functional:

reply, err := model.Generate(ctx, messages,
ai.WithTemperature(0.2),
ai.WithMaxTokens(512),
ai.WithStopSequences("\n\n"),
ai.WithMeta("trace_id", traceID),
)

NewOptions() creates the default option accumulator; Options.Apply(...) folds functional options into it. Most application code passes options directly to Generate, Stream, Run, or Invoke instead of touching the accumulator. Apply mutates and returns the same accumulator, applies options in argument order, and later options can overwrite fields set by earlier options.

Runtime option fields and helpers:

Field or helperContract
Options.Temperature / WithTemperature(t)optional temperature pointer set to t
Options.MaxTokens / WithMaxTokens(n)optional maximum-token pointer set to n
Options.StopSequences / WithStopSequences(seqs...)stop sequence slice replaced with seqs
Options.Meta / WithMeta(key, value)string metadata map; NewOptions() initializes it and WithMeta creates it if needed

Model metadata and configuration fields:

StructPublic fields
ModelConfigProvider, Model, APIKey, BaseURL, Temperature, MaxTokens, Timeout
ModelInfoProvider, Model, MaxTokens, Temperature

Provider Registry

Model providers implement:

type ModelProvider interface {
Name() string
CreateModel(ctx context.Context, cfg *ModelConfig) (ToolableChatModel, error)
}

Register them once during application startup or package initialization:

ai.RegisterModelProvider(openaiProvider)

model, err := ai.NewChatModel(ctx, &ai.ModelConfig{
Provider: "openai",
Model: "gpt-4.1-mini",
APIKey: apiKey,
})

RegisterModelProvider panics on duplicate names with ai: model provider "{name}" already registered; RegisterAgentFactory uses ai: agent factory "{name}" already registered. NewChatModel returns ai.ErrProviderNotFound wrapped with the requested provider name when no provider is registered for cfg.Provider. NewAgentBuilder does the same with ai.ErrAgentNotFound and the requested agent type. cfg, provider instances, and factory instances are non-nil preconditions; the registry does not add nil guards before calling Name() or reading cfg.Provider.

Agent factories follow the same pattern with RegisterAgentFactory(...), NewAgentBuilder(...), ListAgentFactories(), and ai.ErrAgentNotFound. Use ListModelProviders() to inspect registered model provider names. The registry list helpers return map keys and do not define a stable sort order.

Factory implementations exchange ModelConfig, ModelInfo, AgentConfig, and AgentFactory. These are plain public structs/interfaces for provider packages; applications usually consume them only when registering their own model or agent implementation.

Tools

Tools expose JSON-schema-style metadata plus an invocation method:

type Tool interface {
Info() *ToolInfo
Invoke(ctx context.Context, arguments string) (string, error)
}

arguments is a JSON-encoded string from the model. Tool implementations should decode and validate it at the tool boundary, then return a string payload for the model.

Tool metadata uses ToolInfo, ParameterSchema, and PropertySchema. The schema shape is intentionally small: type, properties, required, description, enum, and items cover the JSON-schema subset providers need for tool calling.

Tool-related data shapes:

StructPublic fields
ToolCallID, Name, Arguments
ToolResultCallID, Content
ToolInfoName, Description, Parameters
ParameterSchemaType, Properties, Required
PropertySchemaType, Description, Enum, Items

For tools that stream output, implement StreamableTool:

type StreamableTool interface {
Tool
InvokeStream(ctx context.Context, arguments string) (StringStream, error)
}

Agents

An Agent is a higher-level runner that can reason with a model and tools:

type Agent interface {
Run(ctx context.Context, input string, opts ...Option) (*Message, error)
Stream(ctx context.Context, input string, opts ...Option) (MessageStream, error)
}

AgentBuilder lets a registered factory expose fluent setup:

builder, err := ai.NewAgentBuilder("tool-loop")
if err != nil {
return err
}

agent, err := builder.
WithModel(model).
WithTools(searchTool, calculatorTool).
WithSystemPrompt("Use tools when needed.").
WithMaxIterations(8).
Build(ctx)

AgentConfig exposes the same builder inputs as fields: Model, Tools, SystemPrompt, and MaxIterations.

Streams

The base package defines two stream contracts:

type MessageStream interface {
io.Closer
Recv() (*MessageChunk, error)
Collect() (*Message, error)
}

type StringStream interface {
io.Closer
Recv() (string, error)
Collect() (string, error)
}

Recv returns io.EOF when the stream is exhausted.

Stream data shapes:

StructPublic fields
MessageChunkContent, ToolCalls, Done
TokenUsagePromptTokens, CompletionTokens, TotalTokens

UI Message SSE Streams

The ai/stream package converts messages into Server-Sent Events compatible with the AI SDK UI Message Stream protocol. It sets the standard SSE headers, emits data: ... frames, and terminates with data: [DONE].

Common adapters:

HelperSource
stream.FromChannel(ch)a Go channel of stream.Message
stream.FromCallback(fn)a callback that writes through CallbackWriter
stream.FromAiMessageStream(s)an ai.MessageStream
stream.NewChannelSource(ch)raw MessageSource adapter for a channel
stream.NewCallbackSource(fn)raw callback MessageSource
stream.NewAiMessageStreamSource(s)raw ai.MessageStream adapter

Built-in adapter behavior:

AdapterBehavior
NewChannelSource(ch)Recv() returns channel messages until the channel closes, then returns io.EOF; Close() marks the source closed and later Recv() calls return io.EOF without closing the caller-owned channel
NewCallbackSource(fn)runs fn in a goroutine with a buffered message channel; queued messages are delivered before a callback error is returned; Close() waits for the callback goroutine to finish
NewAiMessageStreamSource(s)converts each ai.MessageChunk into an assistant stream.Message, forwarding Content and any tool calls; Close() delegates to the wrapped ai.MessageStream

Example Fiber handler:

import aistream "github.com/coldsmirk/vef-framework-go/ai/stream"

func Chat(ctx fiber.Ctx) error {
return aistream.FromCallback(func(w aistream.CallbackWriter) error {
w.WriteText("Hello")
w.WriteText(" from VEF")
w.WriteData("usage", map[string]int{"totalTokens": 12})

return nil
}).Stream(ctx)
}

The builder emits start/finish chunks by default, plus text, reasoning, tool input, tool output, and custom data-{type} chunks when the source supplies those message parts. Source and file chunks are exposed through the low-level constructors for custom writers; WithSources(bool) only toggles the option field and does not add sources to ordinary MessageSource output by itself. WithReasoning(bool) gates reasoning output, and reasoning chunks are emitted only when the incoming stream.Message.Reasoning is non-empty.

Default stream options:

OptionDefault
SendReasoningtrue
SendSourcestrue
SendStarttrue
SendFinishtrue
OnErrorreturns err.Error()
OnFinishnil
GenerateIDnil in DefaultOptions(); the builder falls back to prefix + "_" + id.GenerateUUID()

Builder controls:

MethodPurpose
WithSource(source)set a custom MessageSource
WithMessageID(id) / WithIDGenerator(fn)control generated chunk IDs
WithStart(bool) / WithFinish(bool)toggle start/finish chunks
WithReasoning(bool) / WithSources(bool)toggle reasoning output and the public source-output option
WithHeader(key, value)add an SSE response header
OnError(fn) / OnFinish(fn)customize error text or completion callback
Stream(ctx) / StreamToWriter(w)write to Fiber or a buffered writer

Execution-path contracts:

PathBehavior
Stream(ctx) without WithSource(...)returns stream.ErrSourceRequired before writing headers
Stream(ctx) with a sourcewrites SseHeaders, then any WithHeader overrides/additions, then streams through fiber.Ctx.SendStreamWriter
StreamToWriter(w)writes directly to the buffered writer and must be used only after configuring a source; unlike Stream(ctx), it has no missing-source guard
source lifecyclethe configured source is closed when the stream loop exits
normal io.EOF from the sourcecloses open text/reasoning chunks, writes finish-step and finish when SendFinish is true, writes data: [DONE], then calls OnFinish(fullContent) if configured
disabled start/finishWithStart(false) omits both start and start-step; WithFinish(false) omits both finish-step and finish, but still writes data: [DONE]
non-EOF source errorwrites an error chunk using OnError(err), writes data: [DONE], and does not call OnFinish; any open text/reasoning chunks are not explicitly ended first
OnFinish(fullContent)receives the concatenated Content text chunks only, not reasoning, tool payloads, or custom data
tool call argumentsJSON-unmarshaled into the input payload; invalid JSON is sent as the original string; an empty tool call ID is replaced with a generated call_* ID
tool result contentJSON-unmarshaled into the output payload; invalid JSON is sent as the original string
custom data messageseach Data map entry becomes a data-{type} chunk; map iteration order is not stable when one message contains multiple data keys
writer errorschunk/write errors inside the stream loop are ignored because StreamToWriter and the Fiber stream callback do not return an error from the loop

Default SSE headers:

HeaderValue
Content-Typetext/event-stream
Cache-Controlno-cache
Connectionkeep-alive
Transfer-Encodingchunked
X-Vercel-AI-UI-Message-Streamv1
X-Accel-Bufferingno

Low-level chunk constructors are public for tests and custom writers:

Chunk familyConstructors
lifecycleNewStartChunk, NewFinishChunk, NewStartStepChunk, NewFinishStepChunk, NewErrorChunk
textNewTextStartChunk, NewTextDeltaChunk, NewTextEndChunk
reasoningNewReasoningStartChunk, NewReasoningDeltaChunk, NewReasoningEndChunk
toolsNewToolInputStartChunk, NewToolInputDeltaChunk, NewToolInputAvailableChunk, NewToolOutputAvailableChunk
sources/files/dataNewSourceURLChunk, NewSourceDocumentChunk, NewFileChunk, NewDataChunk

Exact chunk wire shapes:

ConstructorWire typeFields
NewStartChunk(messageID)starttype, messageID
NewFinishChunk()finishtype
NewStartStepChunk()start-steptype
NewFinishStepChunk()finish-steptype
NewErrorChunk(errorText)errortype, errorText
NewTextStartChunk(id)text-starttype, id
NewTextDeltaChunk(id, delta)text-deltatype, id, delta
NewTextEndChunk(id)text-endtype, id
NewReasoningStartChunk(id)reasoning-starttype, id
NewReasoningDeltaChunk(id, delta)reasoning-deltatype, id, delta
NewReasoningEndChunk(id)reasoning-endtype, id
NewToolInputStartChunk(toolCallID, toolName)tool-input-starttype, toolCallID, toolName
NewToolInputDeltaChunk(toolCallID, delta)tool-input-deltatype, toolCallID, inputTextDelta
NewToolInputAvailableChunk(toolCallID, toolName, input)tool-input-availabletype, toolCallID, toolName, input
NewToolOutputAvailableChunk(toolCallID, output)tool-output-availabletype, toolCallID, output
NewSourceURLChunk(sourceID, url, title)source-urltype, sourceID, url; title is included only when non-empty
NewSourceDocumentChunk(sourceID, mediaType, title)source-documenttype, sourceID, mediaType; title is included only when non-empty
NewFileChunk(fileID, mediaType, url)filetype, fileID, mediaType, url
NewDataChunk(dataType, data)data-{dataType}type, data; data-{dataType} is dynamic and is not a ChunkType constant

CallbackWriter writes these source messages:

MethodMessage effect
WriteText(content)assistant message with Content
WriteToolCall(id, name, arguments)assistant message with one ToolCall
WriteToolResult(toolCallID, content)tool message with ToolCallID and Content
WriteReasoning(reasoning)assistant message with Reasoning
WriteData(dataType, data)assistant message with Data[dataType]
WriteMessage(msg)forwards the provided stream.Message unchanged

Stream-package data shapes:

Struct or interfacePublic fields or methods
stream.MessageRole, Content, ToolCalls, ToolCallID, Reasoning, Data
stream.ToolCallID, Name, Arguments
stream.SourceType, ID, URL, Title, MediaType
stream.OptionsSendReasoning, SendSources, SendStart, SendFinish, OnError, OnFinish, GenerateID
MessageSourceRecv(), Close(); Recv() returns io.EOF when complete
StreamWriterWriteChunk(chunk), Flush()
ResponseWriterio.Writer compatibility for Fiber stream writers

stream.SseHeaders contains the default AI SDK UI Message Stream headers. stream.ErrSourceRequired is returned when a builder has no source. The stream.ErrSourceClosed sentinel is public for custom MessageSource implementations; the built-in channel and callback adapters report normal completion as io.EOF.

Supporting stream APIs:

API groupPublic surface
rolesstream.RoleUser, stream.RoleAssistant, stream.RoleTool, stream.RoleSystem
chunk type constantsChunkType, ChunkTypeStart, ChunkTypeFinish, ChunkTypeStartStep, ChunkTypeFinishStep, ChunkTypeError, ChunkTypeTextStart, ChunkTypeTextDelta, ChunkTypeTextEnd, ChunkTypeReasoningStart, ChunkTypeReasoningDelta, ChunkTypeReasoningEnd, ChunkTypeToolInputStart, ChunkTypeToolInputDelta, ChunkTypeToolInputAvailable, ChunkTypeToolOutputAvailable, ChunkTypeSourceURL, ChunkTypeSourceDocument, ChunkTypeFile
stream data structsstream.Message, stream.ToolCall, stream.Source, stream.Chunk, stream.Options
writer/source interfacesCallbackWriter, MessageSource, ResponseWriter, StreamWriter
defaultsstream.New(), DefaultOptions(), SseHeaders

Error Sentinels

ErrorMeaning
ai.ErrProviderNotFoundno registered model provider matched ModelConfig.Provider
ai.ErrModelNotSupportedprovider does not support the requested model
ai.ErrAgentNotFoundno registered agent factory matched the requested type
ai.ErrToolNotFoundan agent/model requested an unavailable tool
ai.ErrInvalidArgumentstool arguments failed validation
ai.ErrMaxIterationsReachedagent exceeded its iteration limit
ai.ErrNoContentmodel returned no usable content
ai.ErrStreamClosedstream read after close
stream.ErrSourceRequiredai/stream builder has no message source
stream.ErrSourceClosedpublic sentinel for custom MessageSource implementations

NewToolError(...) and NewModelError(...) create structured ToolError and ModelError values for tool and provider failures. ToolError.Unwrap() exposes the underlying tool error.

Structured error shapes:

TypePublic fields and behavior
ToolErrorToolName, Err; Error() returns ai: tool {ToolName}: {Err} and Unwrap() returns Err
ModelErrorProvider, StatusCode, Message; Error() returns ai: {Provider}: {Message}