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>
200 lines
4.8 KiB
Go
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 }
|
|
}
|