Files
Trackeep/backend/services/ai_service.go
T
Tomas Dvorak d27cf14110 first test
2026-02-08 14:14:55 +01:00

533 lines
13 KiB
Go

package services
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
// AIProvider represents different AI providers
type AIProvider string
const (
ProviderMistral AIProvider = "mistral"
ProviderLongCat AIProvider = "longcat"
ProviderGrok AIProvider = "grok"
ProviderDeepSeek AIProvider = "deepseek"
ProviderOllama AIProvider = "ollama"
ProviderOpenRouter AIProvider = "openrouter"
)
// AIRequest represents a generic AI request
type AIRequest struct {
Messages []Message `json:"messages"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
ModelType string `json:"model_type,omitempty"` // "standard", "thinking", "upgraded_thinking"
}
// Message represents a chat message
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
}
// AIResponse represents a generic AI response
type AIResponse 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"`
}
// Choice represents a choice in AI response
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
}
// Usage represents token usage
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// AIService handles multiple AI providers
type AIService struct {
provider AIProvider
}
// NewAIService creates a new AI service with the specified provider
func NewAIService(provider AIProvider) *AIService {
return &AIService{provider: provider}
}
// GetAvailableProviders returns available AI providers
func GetAvailableProviders() []AIProvider {
// Return all known providers so the frontend can show them in settings
// regardless of current environment configuration. Environment flags
// and API keys still control whether requests actually succeed.
return []AIProvider{
ProviderMistral,
ProviderLongCat,
ProviderGrok,
ProviderDeepSeek,
ProviderOllama,
ProviderOpenRouter,
}
}
// ChatCompletion sends a chat completion request to the configured provider
func (s *AIService) ChatCompletion(req AIRequest) (*AIResponse, error) {
switch s.provider {
case ProviderMistral:
return s.callMistral(req)
case ProviderLongCat:
return s.callLongCat(req)
case ProviderGrok:
return s.callGrok(req)
case ProviderDeepSeek:
return s.callDeepSeek(req)
case ProviderOllama:
return s.callOllama(req)
case ProviderOpenRouter:
return s.callOpenRouter(req)
default:
return nil, fmt.Errorf("unsupported AI provider: %s", s.provider)
}
}
// ChatCompletionWithThinking sends a chat completion request with thinking model
func (s *AIService) ChatCompletionWithThinking(req AIRequest) (*AIResponse, error) {
// Override model with thinking model
thinkingModel := s.getThinkingModel()
if thinkingModel != "" {
req.Model = thinkingModel
}
return s.ChatCompletion(req)
}
// ChatCompletionWithUpgradedThinking sends a chat completion request with upgraded thinking model (LongCat only)
func (s *AIService) ChatCompletionWithUpgradedThinking(req AIRequest) (*AIResponse, error) {
if s.provider != ProviderLongCat {
return nil, fmt.Errorf("upgraded thinking model only available for LongCat provider")
}
// Override model with upgraded thinking model
upgradedModel := os.Getenv("LONGCAT_MODEL_THINKING_UPGRADED")
if upgradedModel != "" {
req.Model = upgradedModel
}
return s.ChatCompletion(req)
}
// ParseThinkingResponse extracts the actual content from thinking model responses
func ParseThinkingResponse(resp *AIResponse, provider AIProvider, modelType string) string {
if provider == ProviderLongCat {
// Handle LongCat thinking models
if resp.Choices[0].Message.Content != "" {
content := resp.Choices[0].Message.Content
// For LongCat-Flash-Thinking, remove thinking tags
if strings.Contains(content, "<longcat_think>") {
// Extract content after thinking tags
parts := strings.Split(content, "</longcat_think>")
if len(parts) > 1 {
return strings.TrimSpace(parts[1])
}
// If no closing tag, try to extract after the thinking content
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.Contains(line, "</longcat_think>") {
return strings.TrimSpace(strings.Join(lines[i+1:], "\n"))
}
}
}
return content
} else if resp.Choices[0].Message.ReasoningContent != "" {
// For LongCat-Flash-Thinking-2601, check if there's actual content
// This model puts reasoning in reasoning_content and final answer in content
// If content is null, we might need to extract from reasoning or return the reasoning itself
return resp.Choices[0].Message.ReasoningContent
}
}
// For Grok, DeepSeek, Mistral and other providers, or if no special handling needed
return resp.Choices[0].Message.Content
}
// getThinkingModel returns the appropriate thinking model for the provider
func (s *AIService) getThinkingModel() string {
switch s.provider {
case ProviderMistral:
return os.Getenv("MISTRAL_MODEL_THINKING")
case ProviderLongCat:
return os.Getenv("LONGCAT_MODEL_THINKING")
case ProviderGrok:
return os.Getenv("GROK_MODEL_THINKING")
case ProviderDeepSeek:
return os.Getenv("DEEPSEEK_MODEL_THINKING")
case ProviderOllama:
return os.Getenv("OLLAMA_MODEL_THINKING")
case ProviderOpenRouter:
return os.Getenv("OPENROUTER_MODEL_THINKING")
default:
return ""
}
}
// callOpenRouter calls the OpenRouter API (OpenAI-compatible)
func (s *AIService) callOpenRouter(req AIRequest) (*AIResponse, error) {
apiKey := os.Getenv("OPENROUTER_API_KEY")
baseURL := os.Getenv("OPENROUTER_BASE_URL")
if baseURL == "" {
baseURL = "https://openrouter.ai/api"
}
model := os.Getenv("OPENROUTER_MODEL")
if model == "" {
model = "openrouter/auto"
}
if req.Model == "" {
req.Model = model
}
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", baseURL+"/v1/chat/completions", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
if apiKey != "" {
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OpenRouter API returned status %d", resp.StatusCode)
}
var orResp AIResponse
if err := json.NewDecoder(resp.Body).Decode(&orResp); err != nil {
return nil, err
}
return &orResp, nil
}
// callMistral calls Mistral AI API
func (s *AIService) callMistral(req AIRequest) (*AIResponse, error) {
apiKey := os.Getenv("MISTRAL_API_KEY")
baseURL := "https://api.mistral.ai/v1"
model := os.Getenv("MISTRAL_MODEL")
if model == "" {
model = "mistral-small-latest"
}
if req.Model == "" {
req.Model = model
}
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", baseURL+"/chat/completions", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Mistral API returned status %d", resp.StatusCode)
}
var mistralResp AIResponse
if err := json.NewDecoder(resp.Body).Decode(&mistralResp); err != nil {
return nil, err
}
return &mistralResp, nil
}
// callLongCat calls LongCat AI API
func (s *AIService) callLongCat(req AIRequest) (*AIResponse, error) {
apiKey := os.Getenv("LONGCAT_API_KEY")
// Determine format and endpoint
format := os.Getenv("LONGCAT_FORMAT")
if format == "" {
format = "openai" // Default to OpenAI format
}
var baseURL string
switch format {
case "openai":
baseURL = "https://api.longcat.chat/openai"
case "anthropic":
baseURL = "https://api.longcat.chat/anthropic"
default:
baseURL = "https://api.longcat.chat/openai"
}
model := os.Getenv("LONGCAT_MODEL")
if model == "" {
model = "LongCat-Flash-Chat"
}
if req.Model == "" {
req.Model = model
}
var jsonBody []byte
var httpReq *http.Request
var err error
if format == "anthropic" {
// Convert to Anthropic format
anthropicReq := map[string]interface{}{
"model": req.Model,
"max_tokens": req.MaxTokens,
"messages": req.Messages,
}
if req.Temperature > 0 {
anthropicReq["temperature"] = req.Temperature
}
jsonBody, err = json.Marshal(anthropicReq)
if err != nil {
return nil, err
}
httpReq, err = http.NewRequest("POST", baseURL+"/v1/messages", strings.NewReader(string(jsonBody)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
} else {
// OpenAI format
jsonBody, err = json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err = http.NewRequest("POST", baseURL+"/v1/chat/completions", strings.NewReader(string(jsonBody)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("LongCat API returned status %d", resp.StatusCode)
}
var longcatResp AIResponse
if err := json.NewDecoder(resp.Body).Decode(&longcatResp); err != nil {
return nil, err
}
return &longcatResp, nil
}
// callGrok calls Grok AI API
func (s *AIService) callGrok(req AIRequest) (*AIResponse, error) {
apiKey := os.Getenv("GROK_API_KEY")
baseURL := os.Getenv("GROK_BASE_URL")
if baseURL == "" {
baseURL = "https://api.x.ai/v1"
}
model := os.Getenv("GROK_MODEL")
if model == "" {
model = "grok-beta"
}
if req.Model == "" {
req.Model = model
}
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", baseURL+"/chat/completions", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Grok API returned status %d", resp.StatusCode)
}
var grokResp AIResponse
if err := json.NewDecoder(resp.Body).Decode(&grokResp); err != nil {
return nil, err
}
return &grokResp, nil
}
// callDeepSeek calls DeepSeek API
func (s *AIService) callDeepSeek(req AIRequest) (*AIResponse, error) {
apiKey := os.Getenv("DEEPSEEK_API_KEY")
baseURL := os.Getenv("DEEPSEEK_BASE_URL")
if baseURL == "" {
baseURL = "https://api.deepseek.com"
}
model := os.Getenv("DEEPSEEK_MODEL")
if model == "" {
model = "deepseek-chat"
}
if req.Model == "" {
req.Model = model
}
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", baseURL+"/chat/completions", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("DeepSeek API returned status %d", resp.StatusCode)
}
var deepseekResp AIResponse
if err := json.NewDecoder(resp.Body).Decode(&deepseekResp); err != nil {
return nil, err
}
return &deepseekResp, nil
}
// callOllama calls Ollama API
func (s *AIService) callOllama(req AIRequest) (*AIResponse, error) {
baseURL := os.Getenv("OLLAMA_BASE_URL")
if baseURL == "" {
baseURL = "http://localhost:11434"
}
model := os.Getenv("OLLAMA_MODEL")
if model == "" {
model = "llama3.1"
}
if req.Model == "" {
req.Model = model
}
jsonData, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", baseURL+"/api/chat", strings.NewReader(string(jsonData)))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 60 * time.Second} // Ollama can be slower
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Ollama API returned status %d", resp.StatusCode)
}
var ollamaResp AIResponse
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
return nil, err
}
return &ollamaResp, nil
}
// SetProvider changes the AI provider
func (s *AIService) SetProvider(provider AIProvider) {
s.provider = provider
}
// GetProvider returns the current AI provider
func (s *AIService) GetProvider() AIProvider {
return s.provider
}