305 lines
8.7 KiB
Go
305 lines
8.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/database"
|
|
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
|
)
|
|
|
|
// ── Team Provider Handlers ──────────────────
|
|
|
|
// ListTeamProviders returns API configs scoped to a team.
|
|
// GET /api/v1/teams/:teamId/providers
|
|
func (h *TeamHandler) ListTeamProviders(c *gin.Context) {
|
|
teamID := getTeamID(c)
|
|
|
|
rows, err := database.DB.Query(`
|
|
SELECT id, name, provider, endpoint, api_key_encrypted,
|
|
model_default, config::text, is_active, is_private, created_at, updated_at
|
|
FROM api_configs
|
|
WHERE team_id = $1
|
|
ORDER BY name ASC
|
|
`, teamID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list team providers"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type teamProvider struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
Endpoint string `json:"endpoint"`
|
|
HasKey bool `json:"has_key"`
|
|
ModelDefault *string `json:"model_default"`
|
|
Config map[string]interface{} `json:"config"`
|
|
IsActive bool `json:"is_active"`
|
|
IsPrivate bool `json:"is_private"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
configs := make([]teamProvider, 0)
|
|
for rows.Next() {
|
|
var p teamProvider
|
|
var apiKeyEnc *string
|
|
var configRaw string
|
|
if err := rows.Scan(&p.ID, &p.Name, &p.Provider, &p.Endpoint, &apiKeyEnc,
|
|
&p.ModelDefault, &configRaw, &p.IsActive, &p.IsPrivate, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
|
continue
|
|
}
|
|
p.HasKey = apiKeyEnc != nil && *apiKeyEnc != ""
|
|
p.Config = parseJSONBConfig(configRaw)
|
|
configs = append(configs, p)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"providers": configs,
|
|
"allow_team_providers": isTeamProvidersAllowed(teamID),
|
|
})
|
|
}
|
|
|
|
// CreateTeamProvider creates an API config scoped to a team.
|
|
// POST /api/v1/teams/:teamId/providers
|
|
func (h *TeamHandler) CreateTeamProvider(c *gin.Context) {
|
|
teamID := getTeamID(c)
|
|
|
|
// Check allow_team_providers setting
|
|
if !isTeamProvidersAllowed(teamID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "team providers are not enabled for this team"})
|
|
return
|
|
}
|
|
|
|
var req createAPIConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if _, err := providers.Get(req.Provider); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "unsupported provider: " + req.Provider,
|
|
"supported_providers": providers.List(),
|
|
})
|
|
return
|
|
}
|
|
|
|
configJSON := "{}"
|
|
if req.Config != nil {
|
|
b, _ := json.Marshal(req.Config)
|
|
configJSON = string(b)
|
|
}
|
|
|
|
var id string
|
|
err := database.DB.QueryRow(`
|
|
INSERT INTO api_configs (team_id, name, provider, endpoint, api_key_encrypted, model_default, config, is_private)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
|
|
RETURNING id
|
|
`, teamID, req.Name, req.Provider, req.Endpoint, req.APIKey, req.ModelDefault, configJSON, req.IsPrivate,
|
|
).Scan(&id)
|
|
if err != nil {
|
|
log.Printf("[WARN] Failed to create team provider: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create provider"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"id": id})
|
|
}
|
|
|
|
// UpdateTeamProvider updates a team-scoped API config.
|
|
// PUT /api/v1/teams/:teamId/providers/:id
|
|
func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) {
|
|
teamID := getTeamID(c)
|
|
providerID := c.Param("id")
|
|
|
|
var req updateAPIConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Verify provider belongs to this team
|
|
var count int
|
|
database.DB.QueryRow(`SELECT COUNT(*) FROM api_configs WHERE id = $1 AND team_id = $2`, providerID, teamID).Scan(&count)
|
|
if count == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found in this team"})
|
|
return
|
|
}
|
|
|
|
// Build dynamic update
|
|
query := "UPDATE api_configs SET updated_at = NOW()"
|
|
args := []interface{}{}
|
|
argN := 1
|
|
|
|
if req.Name != nil {
|
|
query += ", name = $" + strconv.Itoa(argN)
|
|
args = append(args, *req.Name)
|
|
argN++
|
|
}
|
|
if req.Endpoint != nil {
|
|
query += ", endpoint = $" + strconv.Itoa(argN)
|
|
args = append(args, *req.Endpoint)
|
|
argN++
|
|
}
|
|
if req.APIKey != nil {
|
|
query += ", api_key_encrypted = $" + strconv.Itoa(argN)
|
|
args = append(args, *req.APIKey)
|
|
argN++
|
|
}
|
|
if req.ModelDefault != nil {
|
|
query += ", model_default = $" + strconv.Itoa(argN)
|
|
args = append(args, *req.ModelDefault)
|
|
argN++
|
|
}
|
|
if req.IsActive != nil {
|
|
query += ", is_active = $" + strconv.Itoa(argN)
|
|
args = append(args, *req.IsActive)
|
|
argN++
|
|
}
|
|
if req.IsPrivate != nil {
|
|
query += ", is_private = $" + strconv.Itoa(argN)
|
|
args = append(args, *req.IsPrivate)
|
|
argN++
|
|
}
|
|
if req.Config != nil {
|
|
b, _ := json.Marshal(req.Config)
|
|
query += ", config = $" + strconv.Itoa(argN) + "::jsonb"
|
|
args = append(args, string(b))
|
|
argN++
|
|
}
|
|
|
|
query += " WHERE id = $" + strconv.Itoa(argN) + " AND team_id = $" + strconv.Itoa(argN+1)
|
|
args = append(args, providerID, teamID)
|
|
|
|
_, err := database.DB.Exec(query, args...)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update provider"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"id": providerID, "updated": true})
|
|
}
|
|
|
|
// DeleteTeamProvider removes a team-scoped API config.
|
|
// DELETE /api/v1/teams/:teamId/providers/:id
|
|
func (h *TeamHandler) DeleteTeamProvider(c *gin.Context) {
|
|
teamID := getTeamID(c)
|
|
providerID := c.Param("id")
|
|
|
|
result, err := database.DB.Exec(`
|
|
DELETE FROM api_configs WHERE id = $1 AND team_id = $2
|
|
`, providerID, teamID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete provider"})
|
|
return
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found in this team"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"deleted": true})
|
|
}
|
|
|
|
// ListTeamProviderModels lists models available from a team provider (live query).
|
|
// GET /api/v1/teams/:teamId/providers/:id/models
|
|
func (h *TeamHandler) ListTeamProviderModels(c *gin.Context) {
|
|
teamID := getTeamID(c)
|
|
providerID := c.Param("id")
|
|
|
|
var name, providerType, endpoint string
|
|
var apiKey *string
|
|
var headersJSON []byte
|
|
err := database.DB.QueryRow(`
|
|
SELECT name, provider, endpoint, api_key_encrypted, custom_headers
|
|
FROM api_configs
|
|
WHERE id = $1 AND team_id = $2 AND is_active = true
|
|
`, providerID, teamID).Scan(&name, &providerType, &endpoint, &apiKey, &headersJSON)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
|
return
|
|
}
|
|
|
|
provider, err := providers.Get(providerType)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported provider"})
|
|
return
|
|
}
|
|
|
|
key := ""
|
|
if apiKey != nil {
|
|
key = *apiKey
|
|
}
|
|
|
|
var customHeaders map[string]string
|
|
_ = json.Unmarshal(headersJSON, &customHeaders)
|
|
|
|
modelList, err := provider.ListModels(c.Request.Context(), providers.ProviderConfig{
|
|
Endpoint: endpoint,
|
|
APIKey: key,
|
|
CustomHeaders: customHeaders,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to fetch models: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
type modelInfo struct {
|
|
ID string `json:"id"`
|
|
Capabilities providers.ModelCapabilities `json:"capabilities"`
|
|
}
|
|
|
|
models := make([]modelInfo, 0, len(modelList))
|
|
for _, m := range modelList {
|
|
caps := providers.MergeCapabilities(m.Capabilities, m.ID)
|
|
caps.MaxOutputTokens = providers.ResolveMaxOutput(m.ID, caps)
|
|
models = append(models, modelInfo{ID: m.ID, Capabilities: caps})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"models": models, "provider": name})
|
|
}
|
|
|
|
// isTeamProvidersAllowed checks if team providers are enabled for a team.
|
|
// First checks the global allow_team_providers setting, then team.settings JSONB.
|
|
func isTeamProvidersAllowed(teamID string) bool {
|
|
// Check global setting
|
|
var globalVal string
|
|
err := database.DB.QueryRow(`
|
|
SELECT value FROM global_settings WHERE key = 'allow_team_providers'
|
|
`).Scan(&globalVal)
|
|
if err == nil && globalVal == "false" {
|
|
return false
|
|
}
|
|
// Default to true if not set
|
|
|
|
// Check team-level override
|
|
var settingsJSON []byte
|
|
err = database.DB.QueryRow(`
|
|
SELECT settings FROM teams WHERE id = $1
|
|
`, teamID).Scan(&settingsJSON)
|
|
if err != nil {
|
|
return true // default allow
|
|
}
|
|
|
|
var settings map[string]interface{}
|
|
if err := json.Unmarshal(settingsJSON, &settings); err != nil {
|
|
return true
|
|
}
|
|
if v, ok := settings["allow_team_providers"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
return b
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|