package llm import ( "fmt" "log/slog" "net/http" "time" ) const ( // chatCompletionsPath is the endpoint for chat completion requests. chatCompletionsPath = "/chat/completions" // chatTemplateEnableThinking is the chat-template argument that selects // whether a reasoning model emits reasoning tokens. chatTemplateEnableThinking = "enable_thinking" // defaultTimeout limits requests when Config.Timeout is empty. defaultTimeout = 5 * time.Minute // sseDone is the sentinel that terminates a chat completion stream. sseDone = "[DONE]" ) // Config configures a Client. type Config struct { // Key is sent as a bearer token. An empty key disables authentication. Key string `yaml:"key"` // Provider is returned unchanged by Client.Provider. It is not sent to the // inference endpoint. Provider string `yaml:"provider"` // Timeout limits each request, including reading a streaming response. It // uses Go duration syntax and defaults to five minutes when empty. Timeout string `yaml:"timeout"` // URL is the endpoint root. Client appends /chat/completions or /models. URL string `yaml:"url"` } // Validate reports whether Timeout is a Go duration, when set, and URL is // non-empty. It does not contact the endpoint. 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 is an OpenAI-compatible inference client. type Client struct { baseURL string httpc *http.Client key string log *slog.Logger provider string timeout time.Duration } // NewClient creates a client from cfg. If cfg.Timeout is empty, requests have a // five-minute timeout. log must be non-nil; pass // slog.New(slog.DiscardHandler) to run without logs. func NewClient(cfg Config, log *slog.Logger) (*Client, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } if log == nil { return nil, fmt.Errorf("missing logger") } timeout := defaultTimeout if cfg.Timeout != "" { d, err := time.ParseDuration(cfg.Timeout) if err != nil { return nil, fmt.Errorf("parse timeout: %v", err) } timeout = d } return &Client{ baseURL: cfg.URL, key: cfg.Key, log: log, provider: cfg.Provider, httpc: &http.Client{}, timeout: timeout, }, nil } // Provider returns the application-defined provider name supplied in Config. func (c *Client) Provider() string { return c.provider }