143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
capspkg "chat-switchboard/capabilities"
|
|
"chat-switchboard/auth"
|
|
"chat-switchboard/health"
|
|
"chat-switchboard/models"
|
|
"chat-switchboard/store"
|
|
)
|
|
|
|
// ModelHandler provides the unified models endpoint.
|
|
type ModelHandler struct {
|
|
stores store.Stores
|
|
healthStore HealthStatusQuerier // provider health for status enrichment (v0.22.3, nil = disabled)
|
|
}
|
|
|
|
func NewModelHandler(s store.Stores) *ModelHandler {
|
|
return &ModelHandler{stores: s}
|
|
}
|
|
|
|
// SetHealthStore attaches the health store for model status enrichment (v0.22.3).
|
|
func (h *ModelHandler) SetHealthStore(hs HealthStatusQuerier) {
|
|
h.healthStore = hs
|
|
}
|
|
|
|
// ListEnabledModels returns all models the user can access (catalog + personas),
|
|
// with user preferences (hidden, sort order) applied.
|
|
func (h *ModelHandler) ListEnabledModels(c *gin.Context) {
|
|
userID := getUserID(c)
|
|
|
|
userModels, err := capspkg.ModelsForUser(c.Request.Context(), h.stores, userID)
|
|
if err != nil {
|
|
log.Printf("error: ModelsForUser(%s): %v", userID, err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve models"})
|
|
return
|
|
}
|
|
|
|
// Enrich with provider health status (v0.22.3)
|
|
if h.healthStore != nil {
|
|
healthMap := h.buildHealthMap(c.Request.Context())
|
|
for i := range userModels {
|
|
if s, ok := healthMap[userModels[i].ProviderConfigID]; ok {
|
|
userModels[i].ProviderStatus = s
|
|
}
|
|
}
|
|
}
|
|
|
|
// Include admin default model so frontend can use it in resolution chain
|
|
defaultModel, _ := h.stores.Policies.Get(c.Request.Context(), "default_model")
|
|
|
|
// Model allowlist filtering (v0.24.2) — non-admin users with group-level
|
|
// model restrictions only see permitted models. Persona entries whose
|
|
// underlying model is disallowed are also filtered.
|
|
role, _ := c.Get("role")
|
|
if role != "admin" {
|
|
allowlist, err := auth.ResolveModelAllowlist(c.Request.Context(), h.stores, userID)
|
|
if err == nil && allowlist != nil {
|
|
filtered := make([]models.UserModel, 0, len(userModels))
|
|
for _, m := range userModels {
|
|
if allowlist[m.ModelID] {
|
|
filtered = append(filtered, m)
|
|
}
|
|
}
|
|
userModels = filtered
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": userModels, "default_model": defaultModel})
|
|
}
|
|
|
|
// buildHealthMap returns a map of configID → ProviderStatus from current health windows.
|
|
func (h *ModelHandler) buildHealthMap(ctx context.Context) map[string]models.ProviderStatus {
|
|
m := make(map[string]models.ProviderStatus)
|
|
windows, err := h.healthStore.ListAllCurrent(ctx)
|
|
if err != nil {
|
|
return m
|
|
}
|
|
for _, w := range windows {
|
|
m[w.ProviderConfigID] = health.DeriveStatus(w.ErrorRate(), w.RequestCount)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// ResolveModelCaps is the canonical capability resolver for any model.
|
|
// v0.29.0: accepts CatalogStore instead of using database.DB directly.
|
|
func ResolveModelCaps(catalog store.CatalogStore, modelID, configID string) models.ModelCapabilities {
|
|
// 1. Exact match: model_id + provider_config_id
|
|
if configID != "" {
|
|
caps, ok := capsFromCatalog(catalog, modelID, configID)
|
|
if ok {
|
|
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
|
return caps
|
|
}
|
|
}
|
|
|
|
// 2. Any provider: same model_id, any config
|
|
caps, ok := capsFromCatalog(catalog, modelID, "")
|
|
if ok {
|
|
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
|
return caps
|
|
}
|
|
|
|
// 3. Heuristic inference
|
|
caps = capspkg.InferCapabilities(modelID)
|
|
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
|
return caps
|
|
}
|
|
|
|
// capsFromCatalog looks up capabilities from the model_catalog table.
|
|
func capsFromCatalog(catalog store.CatalogStore, modelID, configID string) (models.ModelCapabilities, bool) {
|
|
if catalog == nil {
|
|
return models.ModelCapabilities{}, false
|
|
}
|
|
|
|
var capsJSON []byte
|
|
var err error
|
|
if configID != "" {
|
|
capsJSON, err = catalog.GetCapabilities(context.Background(), modelID, configID)
|
|
} else {
|
|
capsJSON, err = catalog.GetCapabilitiesAny(context.Background(), modelID)
|
|
}
|
|
if err != nil || len(capsJSON) == 0 {
|
|
return models.ModelCapabilities{}, false
|
|
}
|
|
|
|
var caps models.ModelCapabilities
|
|
if json.Unmarshal(capsJSON, &caps) != nil || !caps.HasProviderData() {
|
|
return models.ModelCapabilities{}, false
|
|
}
|
|
|
|
// Merge with known data to fill gaps
|
|
resolved := capspkg.ResolveIntrinsic(modelID, &caps, nil)
|
|
return resolved, true
|
|
}
|
|
|