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. // Use this to integrate custom provider implementations or for testing. 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 } }