This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/team_providers.go
2026-02-22 16:52:19 +00:00

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
}