package controllers import ( "bufio" "bytes" "encoding/json" "fmt" "io" "log" "net/http" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) type DataIa struct { Prompt string `json:"prompt"` Model string `json:"model"` Stream bool `json:"stream"` } type respuestaOllama struct { Response string `json:"response"` Done bool `json:"done"` Error string `json:"error"` } type openaiChatMsg struct { Role string `json:"role"` Content string `json:"content"` } type openaiChatReq struct { Model string `json:"model"` Messages []openaiChatMsg `json:"messages"` Stream bool `json:"stream"` } type openaiChatChoice struct { Delta struct { Content string `json:"content"` } `json:"delta"` FinishReason *string `json:"finish_reason"` } type openaiChatResp struct { Choices []openaiChatChoice `json:"choices"` } type geminiPart struct { Text string `json:"text"` } type geminiContent struct { Parts []geminiPart `json:"parts"` } type geminiReq struct { Contents []geminiContent `json:"contents"` } type geminiCandidate struct { Content geminiContent `json:"content"` FinishReason *string `json:"finishReason"` } type geminiResp struct { Candidates []geminiCandidate `json:"candidates"` } func GeneraTextoStream(c *fiber.Ctx) error { var data DataIa if err := c.BodyParser(&data); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Datos inválidos"}) } data.Stream = true config, err := models.GetAiConfigForService("ia") if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No hay configuración de IA activa. Configura una en /app/ai-config", }) } if data.Model == "" { data.Model = config.ModelName } if data.Model == "" { data.Model = "gemma3:1b" } provider := strings.ToLower(config.Provider) baseURL := strings.TrimRight(config.BaseURL, "/") hasV1 := strings.Contains(baseURL, "/v1") var endpoint string var bodyReader io.Reader useOpenAI := provider != "ollama" || hasV1 useGemini := provider == "gemini" if useGemini { model := data.Model endpoint = fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:streamGenerateContent?alt=sse&key=%s", model, config.ApiKey) gReq := geminiReq{ Contents: []geminiContent{ {Parts: []geminiPart{{Text: data.Prompt}}}, }, } jsonData, _ := json.Marshal(gReq) bodyReader = bytes.NewBuffer(jsonData) } else if !useOpenAI { // Native Ollama API endpoint = baseURL + "/api/generate" jsonData, _ := json.Marshal(data) bodyReader = bytes.NewBuffer(jsonData) } else { // OpenAI-compatible (/v1/chat/completions) baseURL = strings.TrimSuffix(baseURL, "/v1") endpoint = baseURL + "/v1/chat/completions" chatReq := openaiChatReq{ Model: data.Model, Messages: []openaiChatMsg{ {Role: "user", Content: data.Prompt}, }, Stream: true, } jsonData, _ := json.Marshal(chatReq) bodyReader = bytes.NewBuffer(jsonData) } log.Printf("[IA] endpoint=%s model=%s provider=%s", endpoint, data.Model, provider) req, err := http.NewRequest("POST", endpoint, bodyReader) if err != nil { return err } req.Header.Set("Content-Type", "application/json") // Auth if useGemini { // API key ya va en la URL } else if provider == "ollama" && config.ApiKey != "" && config.ApiKey != "ollama" { req.SetBasicAuth("ollama", config.ApiKey) } else if config.ApiKey != "" { req.Header.Set("Authorization", "Bearer "+config.ApiKey) } client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("[IA] Error conectando: %v", err) return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{ "error": fmt.Sprintf("Error conectando a %s: %v", provider, err), }) } if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) resp.Body.Close() log.Printf("[IA] %s status %d: %s", provider, resp.StatusCode, string(bodyBytes)) return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{ "error": fmt.Sprintf("%s respondió con status %d", provider, resp.StatusCode), "details": string(bodyBytes), }) } c.Set("Content-Type", "text/plain; charset=utf-8") c.Set("Cache-Control", "no-cache") c.Set("X-Accel-Buffering", "no") c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { line := scanner.Text() if line == "" { continue } if useGemini { if !strings.HasPrefix(line, "data: ") { continue } var msg geminiResp if err := json.Unmarshal([]byte(line[6:]), &msg); err != nil { continue } if len(msg.Candidates) > 0 && len(msg.Candidates[0].Content.Parts) > 0 { text := msg.Candidates[0].Content.Parts[0].Text if text != "" { fmt.Fprint(w, text) w.Flush() } if msg.Candidates[0].FinishReason != nil { break } } } else if useOpenAI { var msg openaiChatResp if err := json.Unmarshal([]byte(line), &msg); err != nil { if err != io.EOF { log.Printf("[IA] Error decodificando respuesta OpenAI: %v", err) } continue } if len(msg.Choices) == 0 { continue } content := msg.Choices[0].Delta.Content if content != "" { fmt.Fprint(w, content) w.Flush() } if msg.Choices[0].FinishReason != nil { break } } else { var msg respuestaOllama if err := json.Unmarshal([]byte(line), &msg); err != nil { continue } if msg.Error != "" { log.Printf("[IA] Ollama error: %s", msg.Error) fmt.Fprintf(w, "[Error: %s]", msg.Error) w.Flush() break } if msg.Response != "" { fmt.Fprint(w, msg.Response) w.Flush() } if msg.Done { break } } } }) return nil }