ultisuite-backend/internal/llm/client.go
R3D347HR4Y 556d5f416d Enhance API and configuration for contact discovery and public sharing
- Introduced new endpoints for contact discovery, including scanning, listing, and managing discovered contacts.
- Implemented retry logic for handling missing DAV credentials during contact operations.
- Added public share functionality for drive API, allowing users to manage public shares, including upload, delete, and rename operations.
- Updated Nextcloud configuration to support public share links and improved error handling for public share permissions.
- Enhanced logging and validation across contact and drive APIs for better error tracking and user feedback.
- Added tests for new contact matching and ranking functionalities to ensure accuracy and reliability.
2026-06-06 20:27:02 +02:00

209 lines
5.2 KiB
Go

package llm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type Provider struct {
ID string `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key,omitempty"`
DefaultModel string `json:"default_model"`
}
type Settings struct {
DefaultProviderID string `json:"default_provider_id"`
Providers []Provider `json:"providers"`
ContactDiscoveryModel string `json:"contact_discovery_model,omitempty"`
ContactDiscoveryProvider string `json:"contact_discovery_provider_id,omitempty"`
}
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
Temperature float64 `json:"temperature"`
}
type chatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
type modelsResponse struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
type Client struct {
http *http.Client
}
func NewClient() *Client {
return &Client{http: &http.Client{Timeout: 90 * time.Second}}
}
func (c *Client) Complete(ctx context.Context, provider Provider, model, systemPrompt, userPrompt string) (string, error) {
baseURL := strings.TrimRight(strings.TrimSpace(provider.BaseURL), "/")
if baseURL == "" {
return "", fmt.Errorf("llm provider base_url is required")
}
model = strings.TrimSpace(model)
if model == "" {
model = strings.TrimSpace(provider.DefaultModel)
}
if model == "" {
return "", fmt.Errorf("llm model is required")
}
reqBody := chatRequest{
Model: model,
Messages: []ChatMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userPrompt},
},
Temperature: 0.2,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
url := baseURL + "/chat/completions"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
if strings.TrimSpace(provider.APIKey) != "" {
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(provider.APIKey))
}
resp, err := c.http.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", err
}
if resp.StatusCode >= 400 {
return "", fmt.Errorf("llm request failed (%d): %s", resp.StatusCode, string(body))
}
var parsed chatResponse
if err := json.Unmarshal(body, &parsed); err != nil {
return "", err
}
if parsed.Error != nil && parsed.Error.Message != "" {
return "", fmt.Errorf("llm error: %s", parsed.Error.Message)
}
if len(parsed.Choices) == 0 {
return "", fmt.Errorf("llm returned no choices")
}
return strings.TrimSpace(parsed.Choices[0].Message.Content), nil
}
func (c *Client) ListModels(ctx context.Context, provider Provider) ([]string, error) {
baseURL := strings.TrimRight(strings.TrimSpace(provider.BaseURL), "/")
if baseURL == "" {
return nil, fmt.Errorf("llm provider base_url is required")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/models", nil)
if err != nil {
return nil, err
}
if strings.TrimSpace(provider.APIKey) != "" {
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(provider.APIKey))
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("llm models request failed (%d): %s", resp.StatusCode, string(body))
}
var parsed modelsResponse
if err := json.Unmarshal(body, &parsed); err != nil {
return nil, err
}
if parsed.Error != nil && parsed.Error.Message != "" {
return nil, fmt.Errorf("llm error: %s", parsed.Error.Message)
}
models := make([]string, 0, len(parsed.Data))
seen := make(map[string]struct{}, len(parsed.Data))
for _, item := range parsed.Data {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
models = append(models, id)
}
return models, nil
}
func ResolveProvider(settings Settings, providerID string) (Provider, string, error) {
if providerID == "" {
providerID = strings.TrimSpace(settings.ContactDiscoveryProvider)
}
if providerID == "" {
providerID = strings.TrimSpace(settings.DefaultProviderID)
}
for _, p := range settings.Providers {
if p.ID == providerID {
model := strings.TrimSpace(settings.ContactDiscoveryModel)
if model == "" {
model = strings.TrimSpace(p.DefaultModel)
}
return p, model, nil
}
}
if len(settings.Providers) > 0 {
p := settings.Providers[0]
model := strings.TrimSpace(settings.ContactDiscoveryModel)
if model == "" {
model = strings.TrimSpace(p.DefaultModel)
}
return p, model, nil
}
return Provider{}, "", fmt.Errorf("no llm provider configured")
}