Changeset 0.9.0 (#50)

This commit is contained in:
2026-02-23 01:57:28 +00:00
parent 15be26c516
commit 8264aa6016
94 changed files with 9812 additions and 8574 deletions

View File

@@ -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{}