Changeset 0.20.0 (#85)
This commit is contained in:
237
server/handlers/channel_models.go
Normal file
237
server/handlers/channel_models.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/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, 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)
|
||||
c.JSON(http.StatusCreated, gin.H{"models": 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)
|
||||
c.JSON(http.StatusOK, gin.H{"models": 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)
|
||||
c.JSON(http.StatusOK, gin.H{"models": 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})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user