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/capabilities.go
2026-03-13 00:31:05 +00:00

149 lines
4.5 KiB
Go

package handlers
import (
"context"
"encoding/json"
"log"
"net/http"
"github.com/gin-gonic/gin"
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
"git.gobha.me/xcaliber/chat-switchboard/auth"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/health"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/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.
func ResolveModelCaps(c *gin.Context, modelID, configID string) models.ModelCapabilities {
// 1. Exact match: model_id + provider_config_id
if configID != "" {
caps, ok := capsFromCatalog(modelID, configID)
if ok {
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
return caps
}
}
// 2. Any provider: same model_id, any config
caps, ok := capsFromCatalog(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(modelID, configID string) (models.ModelCapabilities, bool) {
if database.DB == nil {
return models.ModelCapabilities{}, false
}
var capsJSON []byte
var err error
if configID != "" {
err = database.DB.QueryRow(database.Q(`
SELECT capabilities FROM model_catalog
WHERE model_id = $1 AND provider_config_id = $2
`), modelID, configID).Scan(&capsJSON)
} else {
err = database.DB.QueryRow(database.Q(`
SELECT capabilities FROM model_catalog
WHERE model_id = $1 ORDER BY last_synced_at DESC LIMIT 1
`), modelID).Scan(&capsJSON)
}
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
}