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)
|