diff --git a/pkg/models/ai_config.go b/pkg/models/ai_config.go index 502d8e6..d3e42b3 100644 --- a/pkg/models/ai_config.go +++ b/pkg/models/ai_config.go @@ -57,6 +57,10 @@ func DeleteAiConfig(id uint) error { return app.Http.Database.DB.Delete(&AiConfig{}, id).Error } +func GetAiConfigByID(id uint, out *AiConfig) error { + return app.Http.Database.DB.First(out, id).Error +} + // GetActiveAiConfig retorna la primera configuración activa del provider indicado. // Si provider está vacío, retorna cualquier config activa. func GetActiveAiConfig(provider string) (*AiConfig, error) { diff --git a/resources/views/ai_config.html b/resources/views/ai_config.html index 0498c98..b558ec0 100644 --- a/resources/views/ai_config.html +++ b/resources/views/ai_config.html @@ -101,6 +101,8 @@
+
+ +
+
+
+ + +

+
+

+            
+ +
+
+
+
@@ -260,7 +278,7 @@ function aiConfigApp() { loading: false, saving: false, items: [], total: 0, totalPages: 1, page: 1, search: '', - showModal: false, editItem: null, deleteId: null, + showModal: false, editItem: null, deleteId: null, testResult: null, errorMsg: '', successMsg: '', formError: '', form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '', modulos: [] }, @@ -341,6 +359,13 @@ function aiConfigApp() { await this.load() }, + async testConfig(id) { + this.loading = true + const res = await fetch(`/app/ai-config/${id}/test`) + this.loading = false + this.testResult = await res.json() + }, + showSuccess(msg) { this.successMsg = msg setTimeout(() => { this.successMsg = '' }, 3000) diff --git a/rest/controllers/ai_config_controller.go b/rest/controllers/ai_config_controller.go index 36e0cc0..19f4f7a 100644 --- a/rest/controllers/ai_config_controller.go +++ b/rest/controllers/ai_config_controller.go @@ -1,9 +1,12 @@ package controllers import ( + "encoding/json" "math" + "net/http" "strconv" "strings" + "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" @@ -164,6 +167,81 @@ func GetAiConfigSelect(c *fiber.Ctx) error { return c.JSON(fiber.Map{"registros": items}) } +// TestAiConfigHandler verifica conectividad con el provider de la config. +func TestAiConfigHandler(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) + } + var item models.AiConfig + if err := models.GetAiConfigByID(uint(id), &item); err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "config no encontrada"}) + } + + client := &http.Client{Timeout: 10 * time.Second} + + var testURL string + var req *http.Request + + switch item.Provider { + case "ollama": + base := strings.TrimSuffix(strings.TrimRight(item.BaseURL, "/"), "/v1") + testURL = base + "/api/tags" + req, _ = http.NewRequest("GET", testURL, nil) + if item.ApiKey != "" && item.ApiKey != "ollama" { + req.SetBasicAuth("ollama", item.ApiKey) + } + case "anthropic": + // Anthropic no tiene /models; usamos /v1/models igual (devuelve 200 con lista) + base := item.BaseURL + if base == "" { + base = "https://api.anthropic.com/v1" + } + testURL = base + "/models" + req, _ = http.NewRequest("GET", testURL, nil) + req.Header.Set("x-api-key", item.ApiKey) + req.Header.Set("anthropic-version", "2023-06-01") + default: + base := item.BaseURL + if base == "" { + switch item.Provider { + case "openai": + base = "https://api.openai.com/v1" + case "qwen": + base = "https://dashscope.aliyuncs.com/compatible-mode/v1" + case "groq": + base = "https://api.groq.com/openai/v1" + default: + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "BaseURL requerida para este provider"}) + } + } + testURL = base + "/models" + req, _ = http.NewRequest("GET", testURL, nil) + req.Header.Set("Authorization", "Bearer "+item.ApiKey) + } + + resp, err := client.Do(req) + if err != nil { + return c.JSON(fiber.Map{"ok": false, "error": err.Error()}) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + return c.JSON(fiber.Map{"ok": false, "status": resp.StatusCode, "error": body}) + } + + // Para Ollama devolvemos la lista de modelos disponibles + if item.Provider == "ollama" { + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + return c.JSON(fiber.Map{"ok": true, "status": resp.StatusCode, "data": body}) + } + + return c.JSON(fiber.Map{"ok": true, "status": resp.StatusCode}) +} + // DeleteAiConfigHandler elimina una configuración de IA. func DeleteAiConfigHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) diff --git a/rest/routes/user.go b/rest/routes/user.go index 61ce3ab..f655e4a 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -290,6 +290,7 @@ func UserRoutes(app fiber.Router) { protected.Post("/ai-config", controllers.CreateAiConfigHandler) protected.Put("/ai-config/:id", controllers.UpdateAiConfigHandler) protected.Delete("/ai-config/:id", controllers.DeleteAiConfigHandler) + protected.Get("/ai-config/:id/test", controllers.TestAiConfigHandler) // ─── OSS API (Alibaba Cloud + S3/MinIO) ──────────────────────────────────── protected.Get("/oss-api", middlewares.MenuMiddleware, controllers.OssApiIndex)