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/channel_models.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

241 lines
7.3 KiB
Go

package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
"switchboard-core/models"
"switchboard-core/store"
)
// ── Channel Models Handler ──────────────────
// Manages multi-model assignments per channel (v0.20.0 Phase 2).
// Routes:
// GET /channels/:id/models — list
// POST /channels/:id/models — add
// PATCH /channels/:id/models/:modelId — update (display_name, is_default, system_prompt)
// DELETE /channels/:id/models/:modelId — remove
const maxModelsPerChannel = 5
type ChannelModelHandler struct {
stores store.Stores
}
func NewChannelModelHandler(stores store.Stores) *ChannelModelHandler {
return &ChannelModelHandler{stores: stores}
}
// ── List ─────────────────────────────────────
func (h *ChannelModelHandler) List(c *gin.Context) {
channelID := c.Param("id")
userID := getUserID(c)
if !userOwnsChannel(c, channelID, userID) {
return
}
roster, err := h.stores.Channels.GetModels(c.Request.Context(), channelID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list models"})
return
}
if roster == nil {
roster = []models.ChannelModel{}
}
c.JSON(http.StatusOK, gin.H{"data": roster})
}
// ── Add ──────────────────────────────────────
type addChannelModelRequest struct {
ModelID string `json:"model_id" binding:"required"`
ProviderConfigID string `json:"provider_config_id"`
DisplayName string `json:"display_name" binding:"required"`
SystemPrompt string `json:"system_prompt"`
IsDefault bool `json:"is_default"`
}
func (h *ChannelModelHandler) Add(c *gin.Context) {
channelID := c.Param("id")
userID := getUserID(c)
if !userOwnsChannel(c, channelID, userID) {
return
}
var req addChannelModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check max models limit
existing, err := h.stores.Channels.GetModels(c.Request.Context(), channelID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check existing models"})
return
}
if len(existing) >= maxModelsPerChannel {
c.JSON(http.StatusConflict, gin.H{"error": "maximum models per channel reached"})
return
}
// If this is the first model or marked as default, ensure only one default
isDefault := req.IsDefault || len(existing) == 0
cm := &models.ChannelModel{
ChannelID: channelID,
ModelID: req.ModelID,
ProviderConfigID: req.ProviderConfigID,
DisplayName: req.DisplayName,
SystemPrompt: req.SystemPrompt,
IsDefault: isDefault,
}
// SetModel handles INSERT ON CONFLICT (upsert by channel_id + model_id)
if err := h.stores.Channels.SetModel(c.Request.Context(), cm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add model"})
return
}
// If this is the new default, clear default on others
if isDefault {
h.clearOtherDefaults(c, channelID, req.ModelID)
}
// Return the updated roster
roster, _ := h.stores.Channels.GetModels(c.Request.Context(), channelID)
if roster == nil { roster = []models.ChannelModel{} }
c.JSON(http.StatusCreated, gin.H{"data": roster})
}
// ── Update ───────────────────────────────────
type updateChannelModelRequest struct {
DisplayName *string `json:"display_name"`
SystemPrompt *string `json:"system_prompt"`
IsDefault *bool `json:"is_default"`
}
func (h *ChannelModelHandler) Update(c *gin.Context) {
channelID := c.Param("id")
modelRecordID := c.Param("modelId")
userID := getUserID(c)
if !userOwnsChannel(c, channelID, userID) {
return
}
var req updateChannelModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify model belongs to this channel
cm, err := h.stores.Channels.GetModelByID(c.Request.Context(), modelRecordID)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "model not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up model"})
return
}
if cm.ChannelID != channelID {
c.JSON(http.StatusForbidden, gin.H{"error": "model does not belong to this channel"})
return
}
fields := make(map[string]interface{})
if req.DisplayName != nil {
fields["display_name"] = *req.DisplayName
}
if req.SystemPrompt != nil {
fields["system_prompt"] = *req.SystemPrompt
}
if req.IsDefault != nil {
fields["is_default"] = *req.IsDefault
if *req.IsDefault {
h.clearOtherDefaults(c, channelID, cm.ModelID)
}
}
if len(fields) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
if err := h.stores.Channels.UpdateModel(c.Request.Context(), modelRecordID, fields); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update model"})
return
}
roster, _ := h.stores.Channels.GetModels(c.Request.Context(), channelID)
if roster == nil { roster = []models.ChannelModel{} }
c.JSON(http.StatusOK, gin.H{"data": roster})
}
// ── Delete ───────────────────────────────────
func (h *ChannelModelHandler) Delete(c *gin.Context) {
channelID := c.Param("id")
modelRecordID := c.Param("modelId")
userID := getUserID(c)
if !userOwnsChannel(c, channelID, userID) {
return
}
// Verify model belongs to this channel
cm, err := h.stores.Channels.GetModelByID(c.Request.Context(), modelRecordID)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "model not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up model"})
return
}
if cm.ChannelID != channelID {
c.JSON(http.StatusForbidden, gin.H{"error": "model does not belong to this channel"})
return
}
if err := h.stores.Channels.DeleteModel(c.Request.Context(), modelRecordID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete model"})
return
}
// If deleted model was default, promote the first remaining model
if cm.IsDefault {
remaining, _ := h.stores.Channels.GetModels(c.Request.Context(), channelID)
if len(remaining) > 0 {
h.stores.Channels.UpdateModel(c.Request.Context(), remaining[0].ID,
map[string]interface{}{"is_default": true})
}
}
roster, _ := h.stores.Channels.GetModels(c.Request.Context(), channelID)
if roster == nil { roster = []models.ChannelModel{} }
c.JSON(http.StatusOK, gin.H{"data": roster})
}
// ── Helpers ──────────────────────────────────
// clearOtherDefaults sets is_default=false on all models in the channel
// except the one with the given modelID.
func (h *ChannelModelHandler) clearOtherDefaults(c *gin.Context, channelID, keepModelID string) {
roster, _ := h.stores.Channels.GetModels(c.Request.Context(), channelID)
for _, m := range roster {
if m.ModelID != keepModelID && m.IsDefault {
h.stores.Channels.UpdateModel(c.Request.Context(), m.ID,
map[string]interface{}{"is_default": false})
}
}
}