Changeset 0.4.3 (#32)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
||||
)
|
||||
|
||||
// ── Types ───────────────────────────────────
|
||||
@@ -384,3 +385,283 @@ func (h *AdminHandler) GetStats(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// ── Global API Configs ──────────────────────
|
||||
|
||||
type adminGlobalConfigResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
ModelDefault *string `json:"model_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
HasKey bool `json:"has_key"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListGlobalConfigs(c *gin.Context) {
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT id, name, provider, endpoint, model_default, is_active,
|
||||
(api_key_encrypted IS NOT NULL AND api_key_encrypted != '') as has_key,
|
||||
created_at
|
||||
FROM api_configs
|
||||
WHERE user_id IS NULL
|
||||
ORDER BY created_at ASC
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
configs := make([]adminGlobalConfigResponse, 0)
|
||||
for rows.Next() {
|
||||
var cfg adminGlobalConfigResponse
|
||||
if err := rows.Scan(
|
||||
&cfg.ID, &cfg.Name, &cfg.Provider, &cfg.Endpoint,
|
||||
&cfg.ModelDefault, &cfg.IsActive, &cfg.HasKey, &cfg.CreatedAt,
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"configs": configs})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CreateGlobalConfig(c *gin.Context) {
|
||||
var req createAPIConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := database.DB.QueryRow(`
|
||||
INSERT INTO api_configs (name, provider, endpoint, api_key_encrypted, model_default, user_id)
|
||||
VALUES ($1, $2, $3, $4, $5, NULL)
|
||||
RETURNING id
|
||||
`, req.Name, req.Provider, req.Endpoint, req.APIKey, req.ModelDefault).Scan(&id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create config"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"id": id, "message": "global config created"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DeleteGlobalConfig(c *gin.Context) {
|
||||
configID := c.Param("id")
|
||||
|
||||
result, err := database.DB.Exec(
|
||||
`DELETE FROM api_configs WHERE id = $1 AND user_id IS NULL`,
|
||||
configID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete config"})
|
||||
return
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "global config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "global config deleted"})
|
||||
}
|
||||
|
||||
// ── Model Configs ───────────────────────────
|
||||
|
||||
type modelConfigResponse struct {
|
||||
ID string `json:"id"`
|
||||
APIConfigID string `json:"api_config_id"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
Capabilities map[string]interface{} `json:"capabilities"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type updateModelConfigRequest struct {
|
||||
IsEnabled *bool `json:"is_enabled"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Capabilities map[string]interface{} `json:"capabilities"`
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT mc.id, mc.api_config_id, ac.name, mc.model_id, mc.display_name,
|
||||
mc.is_enabled, mc.capabilities, mc.created_at, mc.updated_at
|
||||
FROM model_configs mc
|
||||
JOIN api_configs ac ON mc.api_config_id = ac.id
|
||||
WHERE ac.user_id IS NULL
|
||||
ORDER BY ac.name, mc.model_id
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list models"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
models := make([]modelConfigResponse, 0)
|
||||
for rows.Next() {
|
||||
var m modelConfigResponse
|
||||
var capsJSON []byte
|
||||
if err := rows.Scan(
|
||||
&m.ID, &m.APIConfigID, &m.ProviderName, &m.ModelID, &m.DisplayName,
|
||||
&m.IsEnabled, &capsJSON, &m.CreatedAt, &m.UpdatedAt,
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
_ = json.Unmarshal(capsJSON, &m.Capabilities)
|
||||
models = append(models, m)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"models": models})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
// Load all global api_configs
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT id, provider, endpoint, api_key_encrypted
|
||||
FROM api_configs
|
||||
WHERE user_id IS NULL AND is_active = true
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type fetchResult struct {
|
||||
ConfigID string `json:"config_id"`
|
||||
Provider string `json:"provider"`
|
||||
Added int `json:"added"`
|
||||
Skipped int `json:"skipped"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
results := make([]fetchResult, 0)
|
||||
totalAdded := 0
|
||||
|
||||
for rows.Next() {
|
||||
var cfgID, providerID, endpoint string
|
||||
var apiKey *string
|
||||
if err := rows.Scan(&cfgID, &providerID, &endpoint, &apiKey); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
prov, err := providers.Get(providerID)
|
||||
if err != nil {
|
||||
results = append(results, fetchResult{ConfigID: cfgID, Provider: providerID, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
|
||||
key := ""
|
||||
if apiKey != nil {
|
||||
key = *apiKey
|
||||
}
|
||||
|
||||
models, err := prov.ListModels(c.Request.Context(), providers.ProviderConfig{
|
||||
Endpoint: endpoint,
|
||||
APIKey: key,
|
||||
})
|
||||
if err != nil {
|
||||
results = append(results, fetchResult{ConfigID: cfgID, Provider: providerID, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
|
||||
fr := fetchResult{ConfigID: cfgID, Provider: providerID}
|
||||
for _, m := range models {
|
||||
result, err := database.DB.Exec(`
|
||||
INSERT INTO model_configs (api_config_id, model_id, display_name)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (api_config_id, model_id) DO NOTHING
|
||||
`, cfgID, m.ID, m.Name)
|
||||
if err != nil {
|
||||
fr.Skipped++
|
||||
continue
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected > 0 {
|
||||
fr.Added++
|
||||
totalAdded++
|
||||
} else {
|
||||
fr.Skipped++
|
||||
}
|
||||
}
|
||||
results = append(results, fr)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"total_added": totalAdded,
|
||||
"results": results,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
|
||||
modelID := c.Param("id")
|
||||
|
||||
var req updateModelConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Build dynamic update
|
||||
if req.IsEnabled != nil {
|
||||
_, err := database.DB.Exec(
|
||||
`UPDATE model_configs SET is_enabled = $1, updated_at = NOW() WHERE id = $2`,
|
||||
*req.IsEnabled, modelID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update enabled"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.DisplayName != nil {
|
||||
_, err := database.DB.Exec(
|
||||
`UPDATE model_configs SET display_name = $1, updated_at = NOW() WHERE id = $2`,
|
||||
*req.DisplayName, modelID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update name"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Capabilities != nil {
|
||||
capsJSON, err := json.Marshal(req.Capabilities)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid capabilities"})
|
||||
return
|
||||
}
|
||||
_, err = database.DB.Exec(
|
||||
`UPDATE model_configs SET capabilities = $1, updated_at = NOW() WHERE id = $2`,
|
||||
string(capsJSON), modelID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update capabilities"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "model updated"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DeleteModelConfig(c *gin.Context) {
|
||||
modelID := c.Param("id")
|
||||
result, err := database.DB.Exec(`DELETE FROM model_configs WHERE id = $1`, modelID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete model"})
|
||||
return
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "model not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "model deleted"})
|
||||
}
|
||||
|
||||
@@ -411,6 +411,47 @@ func (h *APIConfigHandler) ListAllModels(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"models": allModels})
|
||||
}
|
||||
|
||||
// ── List Enabled Models (from model_configs) ─
|
||||
|
||||
func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
||||
type enabledModel struct {
|
||||
ID string `json:"id"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ConfigID string `json:"config_id"`
|
||||
Capabilities map[string]interface{} `json:"capabilities"`
|
||||
}
|
||||
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT mc.id, mc.model_id, mc.display_name, ac.provider, ac.name, mc.api_config_id, mc.capabilities
|
||||
FROM model_configs mc
|
||||
JOIN api_configs ac ON mc.api_config_id = ac.id
|
||||
WHERE mc.is_enabled = true AND ac.is_active = true AND ac.user_id IS NULL
|
||||
ORDER BY ac.name, mc.model_id
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"models": []interface{}{}})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
models := make([]enabledModel, 0)
|
||||
for rows.Next() {
|
||||
var m enabledModel
|
||||
var capsJSON []byte
|
||||
if err := rows.Scan(&m.ID, &m.ModelID, &m.DisplayName, &m.Provider, &m.ProviderName, &m.ConfigID, &capsJSON); err != nil {
|
||||
continue
|
||||
}
|
||||
m.Capabilities = make(map[string]interface{})
|
||||
_ = json.Unmarshal(capsJSON, &m.Capabilities)
|
||||
models = append(models, m)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"models": models})
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────
|
||||
|
||||
type scannable interface {
|
||||
|
||||
Reference in New Issue
Block a user