Files
go-llm/v2/llm.go
Steve Dudenhoeffer a4cb4baab5 Add go-llm v2: redesigned API for simpler LLM abstraction
v2 is a new Go module (v2/) with a dramatically simpler API:
- Unified Message type (no more Input marker interface)
- Define[T] for ergonomic tool creation with standard context.Context
- Chat session with automatic tool-call loop (agent loop)
- Streaming via pull-based StreamReader
- MCP one-call connect (MCPStdioServer, MCPHTTPServer, MCPSSEServer)
- Middleware support (logging, retry, timeout, usage tracking)
- Decoupled JSON Schema (map[string]any, no provider coupling)
- Sample tools: WebSearch, Browser, Exec, ReadFile, WriteFile, HTTP
- Providers: OpenAI, Anthropic, Google (all with streaming)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 20:00:08 -05:00

200 lines
4.8 KiB
Go

package llm
import (
"context"
"gitea.stevedudenhoeffer.com/steve/go-llm/v2/provider"
)
// Client represents an LLM provider. Create with OpenAI(), Anthropic(), Google().
type Client struct {
p provider.Provider
middleware []Middleware
}
// newClient creates a Client backed by the given provider.
func newClient(p provider.Provider) *Client {
return &Client{p: p}
}
// Model returns a Model for the specified model version.
func (c *Client) Model(modelVersion string) *Model {
return &Model{
provider: c.p,
model: modelVersion,
middleware: c.middleware,
}
}
// WithMiddleware returns a new Client with additional middleware applied to all models.
func (c *Client) WithMiddleware(mw ...Middleware) *Client {
c2 := &Client{
p: c.p,
middleware: append(append([]Middleware{}, c.middleware...), mw...),
}
return c2
}
// Model represents a specific model from a provider, ready for completions.
type Model struct {
provider provider.Provider
model string
middleware []Middleware
}
// Complete sends a non-streaming completion request.
func (m *Model) Complete(ctx context.Context, messages []Message, opts ...RequestOption) (Response, error) {
cfg := &requestConfig{}
for _, opt := range opts {
opt(cfg)
}
chain := m.buildChain()
return chain(ctx, m.model, messages, cfg)
}
// Stream sends a streaming completion request, returning a StreamReader.
func (m *Model) Stream(ctx context.Context, messages []Message, opts ...RequestOption) (*StreamReader, error) {
cfg := &requestConfig{}
for _, opt := range opts {
opt(cfg)
}
req := buildProviderRequest(m.model, messages, cfg)
return newStreamReader(ctx, m.provider, req)
}
// WithMiddleware returns a new Model with additional middleware applied.
func (m *Model) WithMiddleware(mw ...Middleware) *Model {
return &Model{
provider: m.provider,
model: m.model,
middleware: append(append([]Middleware{}, m.middleware...), mw...),
}
}
func (m *Model) buildChain() CompletionFunc {
// Base handler that calls the provider
base := func(ctx context.Context, model string, messages []Message, cfg *requestConfig) (Response, error) {
req := buildProviderRequest(model, messages, cfg)
resp, err := m.provider.Complete(ctx, req)
if err != nil {
return Response{}, err
}
return convertProviderResponse(resp), nil
}
// Apply middleware in reverse order (first middleware wraps outermost)
chain := base
for i := len(m.middleware) - 1; i >= 0; i-- {
chain = m.middleware[i](chain)
}
return chain
}
func buildProviderRequest(model string, messages []Message, cfg *requestConfig) provider.Request {
req := provider.Request{
Model: model,
Messages: convertMessages(messages),
}
if cfg.temperature != nil {
req.Temperature = cfg.temperature
}
if cfg.maxTokens != nil {
req.MaxTokens = cfg.maxTokens
}
if cfg.topP != nil {
req.TopP = cfg.topP
}
if len(cfg.stop) > 0 {
req.Stop = cfg.stop
}
if cfg.tools != nil {
for _, tool := range cfg.tools.AllTools() {
req.Tools = append(req.Tools, provider.ToolDef{
Name: tool.Name,
Description: tool.Description,
Schema: tool.Schema,
})
}
}
return req
}
func convertMessages(msgs []Message) []provider.Message {
out := make([]provider.Message, len(msgs))
for i, m := range msgs {
pm := provider.Message{
Role: string(m.Role),
Content: m.Content.Text,
ToolCallID: m.ToolCallID,
}
for _, img := range m.Content.Images {
pm.Images = append(pm.Images, provider.Image{
URL: img.URL,
Base64: img.Base64,
ContentType: img.ContentType,
})
}
for _, tc := range m.ToolCalls {
pm.ToolCalls = append(pm.ToolCalls, provider.ToolCall{
ID: tc.ID,
Name: tc.Name,
Arguments: tc.Arguments,
})
}
out[i] = pm
}
return out
}
func convertProviderResponse(resp provider.Response) Response {
r := Response{
Text: resp.Text,
}
for _, tc := range resp.ToolCalls {
r.ToolCalls = append(r.ToolCalls, ToolCall{
ID: tc.ID,
Name: tc.Name,
Arguments: tc.Arguments,
})
}
if resp.Usage != nil {
r.Usage = &Usage{
InputTokens: resp.Usage.InputTokens,
OutputTokens: resp.Usage.OutputTokens,
TotalTokens: resp.Usage.TotalTokens,
}
}
// Build the assistant message for conversation history
r.message = Message{
Role: RoleAssistant,
Content: Content{Text: resp.Text},
ToolCalls: r.ToolCalls,
}
return r
}
// --- Provider constructors ---
// These are defined here and delegate to provider-specific packages.
// They are set up via init() in the provider packages, or defined directly.
// ClientOption configures a client.
type ClientOption func(*clientConfig)
type clientConfig struct {
baseURL string
}
// WithBaseURL overrides the API base URL.
func WithBaseURL(url string) ClientOption {
return func(c *clientConfig) { c.baseURL = url }
}