Changeset 0.9.0 (#50)
This commit is contained in:
@@ -8,22 +8,23 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"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,
|
||||
SELECT id, name, provider, endpoint, api_key_enc,
|
||||
model_default, config::text, is_active, is_private, created_at, updated_at
|
||||
FROM api_configs
|
||||
WHERE team_id = $1
|
||||
FROM provider_configs
|
||||
WHERE scope = 'team' AND owner_id = $1
|
||||
ORDER BY name ASC
|
||||
`, teamID)
|
||||
if err != nil {
|
||||
@@ -67,17 +68,23 @@ func (h *TeamHandler) ListTeamProviders(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Provider string `json:"provider" binding:"required"`
|
||||
Endpoint string `json:"endpoint" binding:"required"`
|
||||
APIKey string `json:"api_key"`
|
||||
ModelDefault string `json:"model_default,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
IsPrivate bool `json:"is_private,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -99,8 +106,8 @@ func (h *TeamHandler) CreateTeamProvider(c *gin.Context) {
|
||||
|
||||
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)
|
||||
INSERT INTO provider_configs (scope, owner_id, name, provider, endpoint, api_key_enc, model_default, config, is_private)
|
||||
VALUES ('team', $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)
|
||||
@@ -114,12 +121,19 @@ func (h *TeamHandler) CreateTeamProvider(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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
|
||||
var req struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Endpoint *string `json:"endpoint,omitempty"`
|
||||
APIKey *string `json:"api_key,omitempty"`
|
||||
ModelDefault *string `json:"model_default,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
IsPrivate *bool `json:"is_private,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -127,14 +141,14 @@ func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) {
|
||||
|
||||
// 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)
|
||||
database.DB.QueryRow(`SELECT COUNT(*) FROM provider_configs WHERE id = $1 AND scope = 'team' AND owner_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()"
|
||||
query := "UPDATE provider_configs SET updated_at = NOW()"
|
||||
args := []interface{}{}
|
||||
argN := 1
|
||||
|
||||
@@ -149,7 +163,7 @@ func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) {
|
||||
argN++
|
||||
}
|
||||
if req.APIKey != nil {
|
||||
query += ", api_key_encrypted = $" + strconv.Itoa(argN)
|
||||
query += ", api_key_enc = $" + strconv.Itoa(argN)
|
||||
args = append(args, *req.APIKey)
|
||||
argN++
|
||||
}
|
||||
@@ -175,7 +189,7 @@ func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) {
|
||||
argN++
|
||||
}
|
||||
|
||||
query += " WHERE id = $" + strconv.Itoa(argN) + " AND team_id = $" + strconv.Itoa(argN+1)
|
||||
query += " WHERE id = $" + strconv.Itoa(argN) + " AND scope = 'team' AND owner_id = $" + strconv.Itoa(argN+1)
|
||||
args = append(args, providerID, teamID)
|
||||
|
||||
_, err := database.DB.Exec(query, args...)
|
||||
@@ -188,13 +202,12 @@ func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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
|
||||
DELETE FROM provider_configs WHERE id = $1 AND scope = 'team' AND owner_id = $2
|
||||
`, providerID, teamID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete provider"})
|
||||
@@ -211,7 +224,6 @@ func (h *TeamHandler) DeleteTeamProvider(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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")
|
||||
@@ -220,9 +232,9 @@ func (h *TeamHandler) ListTeamProviderModels(c *gin.Context) {
|
||||
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
|
||||
SELECT name, provider, endpoint, api_key_enc, headers
|
||||
FROM provider_configs
|
||||
WHERE id = $1 AND scope = 'team' AND owner_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"})
|
||||
@@ -254,24 +266,38 @@ func (h *TeamHandler) ListTeamProviderModels(c *gin.Context) {
|
||||
}
|
||||
|
||||
type modelInfo struct {
|
||||
ID string `json:"id"`
|
||||
Capabilities providers.ModelCapabilities `json:"capabilities"`
|
||||
ID string `json:"id"`
|
||||
Capabilities models.ModelCapabilities `json:"capabilities"`
|
||||
}
|
||||
|
||||
models := make([]modelInfo, 0, len(modelList))
|
||||
out := 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})
|
||||
caps := capspkg.ResolveIntrinsic(m.ID, &m.Capabilities)
|
||||
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(m.ID, caps)
|
||||
out = append(out, modelInfo{ID: m.ID, Capabilities: caps})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"models": models, "provider": name})
|
||||
c.JSON(http.StatusOK, gin.H{"models": out, "provider": name})
|
||||
}
|
||||
|
||||
// isTeamProvidersAllowed checks if team providers are enabled for a team.
|
||||
// First checks the global allow_team_providers setting, then team.settings JSONB.
|
||||
// parseJSONBConfig parses a JSONB text string into a map.
|
||||
func parseJSONBConfig(raw string) map[string]interface{} {
|
||||
if raw == "" || raw == "{}" || raw == "null" {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// isTeamProvidersAllowed checks if team providers are enabled.
|
||||
func isTeamProvidersAllowed(teamID string) bool {
|
||||
// Check global setting
|
||||
if database.DB == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var globalVal string
|
||||
err := database.DB.QueryRow(`
|
||||
SELECT value FROM global_settings WHERE key = 'allow_team_providers'
|
||||
@@ -279,15 +305,11 @@ func isTeamProvidersAllowed(teamID string) bool {
|
||||
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)
|
||||
err = database.DB.QueryRow(`SELECT settings FROM teams WHERE id = $1`, teamID).Scan(&settingsJSON)
|
||||
if err != nil {
|
||||
return true // default allow
|
||||
return true
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
|
||||
Reference in New Issue
Block a user