package llm import ( "fmt" "log/slog" "net/http" "time" ) // Config holds the configuration for an LLM client. type Config struct { // Key is the API key for authentication. Key string `yaml:"key"` // SystemMessage is prepended to all conversations. SystemMessage string `yaml:"system_message"` // Timeout is the maximum duration for a query (e.g., "5m"). // Defaults to 5 minutes if empty. Timeout string `yaml:"timeout"` // URL is the base URL of the OpenAI-compatible API endpoint. URL string `yaml:"url"` } // Validate checks that required configuration values are present and valid. func (cfg Config) Validate() error { if cfg.Timeout != "" { if _, err := time.ParseDuration(cfg.Timeout); err != nil { return fmt.Errorf("invalid timeout: %w", err) } } if cfg.URL == "" { return fmt.Errorf("missing URL") } return nil } // Client wraps an OpenAI-compatible client with tool execution support. type Client struct { baseURL string httpc *http.Client key string log *slog.Logger registry *Registry systemMessage string timeout time.Duration tools []APITool } // NewClient creates a new LLM client with the provided configuration. // The registry is optional; if nil, tool calling is disabled. func NewClient( cfg Config, registry *Registry, log *slog.Logger, ) (*Client, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } timeout := 5 * time.Minute if cfg.Timeout != "" { d, err := time.ParseDuration(cfg.Timeout) if err != nil { return nil, fmt.Errorf("parse timeout: %v", err) } timeout = d } llm := &Client{ baseURL: cfg.URL, key: cfg.Key, log: log, systemMessage: cfg.SystemMessage, registry: registry, httpc: &http.Client{}, timeout: timeout, } // Convert tools from registry. llm.tools = registry.ToAPI() return llm, nil }