package handlers import ( "encoding/json" "log" "net/http" "strconv" "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. func (h *TeamHandler) ListTeamProviders(c *gin.Context) { teamID := getTeamID(c) rows, err := database.DB.Query(` SELECT id, name, provider, endpoint, api_key_enc, model_default, config::text, is_active, is_private, created_at, updated_at FROM provider_configs WHERE scope = 'team' AND owner_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. func (h *TeamHandler) CreateTeamProvider(c *gin.Context) { teamID := getTeamID(c) if !isTeamProvidersAllowed(teamID) { c.JSON(http.StatusForbidden, gin.H{"error": "team providers are not enabled for this team"}) return } 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 } 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 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) 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. func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) { teamID := getTeamID(c) providerID := c.Param("id") 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 } // Verify provider belongs to this team var count int 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 provider_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_enc = $" + 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 scope = 'team' AND owner_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. func (h *TeamHandler) DeleteTeamProvider(c *gin.Context) { teamID := getTeamID(c) providerID := c.Param("id") result, err := database.DB.Exec(` 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"}) 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). 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_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"}) 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 models.ModelCapabilities `json:"capabilities"` } out := make([]modelInfo, 0, len(modelList)) for _, m := range modelList { 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": out, "provider": name}) } // 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 { if database.DB == nil { return false } 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 } var settingsJSON []byte err = database.DB.QueryRow(`SELECT settings FROM teams WHERE id = $1`, teamID).Scan(&settingsJSON) if err != nil { return true } 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 }