package llm import ( "bytes" "encoding/json" "errors" "io" "log/slog" "net/http" "strconv" "time" config "git.estatecloud.org/radumaco/souvenir/config" "git.estatecloud.org/radumaco/souvenir/model" ) const COMPLETIONS_API = "/v1/chat/completions" const MODELS_API = "/v1/models" type ChatClient struct { Cfg config.Config logger slog.Logger client *http.Client } // Request Types type ChatRequest struct { Model string `json:"model"` Messages []model.Message `json:"messages"` Tools []config.Tool `json:"tools,omitempty"` } // Response types // type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []Choice `json:"choices"` Usage Usage `json:"usage"` } type RenameResponse struct { Title string `json:"title"` Summary string `json:"summary"` } type Choice struct { Index int `json:"index"` Message model.Message `json:"message"` FinishReason string `json:"finish_reason"` } type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } func NewLLMClient(cfg config.Config) *ChatClient { return &ChatClient{ Cfg: cfg, logger: *slog.Default().With("Component", "ChatClient"), client: &http.Client{Timeout: time.Duration(cfg.Api.Timeout) * time.Millisecond}, } } func (cl ChatClient) Models() ([]string, error) { req, err := http.NewRequest("GET", cl.Cfg.Api.Url+MODELS_API, nil) if err != nil { cl.logger.Error("There was an error creating the request", "error", err.Error()) return nil, err } req.Header.Set("Content-Type", "application/json") if cl.Cfg.Api.Key != "" { req.Header.Set("Authorization", "Bearer "+cl.Cfg.Api.Key) } resp, err := cl.client.Do(req) if err != nil { cl.logger.Error("There was an error during the network request", "error", err.Error()) return nil, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { cl.logger.Error("There was an error reading the response body", "error", err.Error()) return nil, err } if resp.StatusCode != 200 && resp.StatusCode != 202 { cl.logger.Error("Call returned non 200 status", "statusCode", resp.StatusCode, "status", resp.Status) return nil, errors.New("Call returned invalid status " + strconv.Itoa(resp.StatusCode) + resp.Status) } cl.logger.Debug("Received data", "data", data) var response struct { Data []struct { Id string } } if err := json.Unmarshal(data, &response); err != nil { cl.logger.Error("There was an error parsing the response", "error", err.Error()) return nil, err } cl.logger.Debug("Unmarshal response", "data", response) var ret []string for _, m := range response.Data { ret = append(ret, m.Id) } return ret, nil } func (cl ChatClient) Rename(messages []model.Message) (RenameResponse, error) { llm := cl.Cfg.Llm.TitleModel if llm == "" { llm = cl.Cfg.Llm.Model } request := ChatRequest{ Model: llm, Messages: append([]model.Message{{ Role: "system", Content: "Reply with ONLY JSON with the form: {\"title\":