package handlers import ( "encoding/json" "log" "net/http" "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" "git.gobha.me/xcaliber/chat-switchboard/store" ) // ModelHandler provides the unified models endpoint. type ModelHandler struct { stores store.Stores } func NewModelHandler(s store.Stores) *ModelHandler { return &ModelHandler{stores: s} } // 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 } c.JSON(http.StatusOK, gin.H{"models": userModels}) } // ResolveModelCaps is the canonical capability resolver for any model. // Priority chain: // 1. model_catalog DB — exact match (model_id + provider_config_id) // 2. model_catalog DB — any provider (same model, different config) // 3. Known model table (static, curated) // 4. Heuristic inference (name-based fallback) 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. Known model table (static, curated) caps, found := capspkg.LookupKnownModel(modelID) if found { caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps) return caps } // 4. 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(` SELECT capabilities FROM model_catalog WHERE model_id = $1 AND provider_config_id = $2 `, modelID, configID).Scan(&capsJSON) } else { err = database.DB.QueryRow(` SELECT capabilities FROM model_catalog WHERE model_id = $1 ORDER BY last_synced_at DESC NULLS LAST 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) return resolved, true } // liveQueryModelCaps queries a provider API to get capabilities for a specific model. func liveQueryModelCaps(c *gin.Context, configID, modelID string) (models.ModelCapabilities, bool) { if database.DB == nil { return models.ModelCapabilities{}, false } var providerID, endpoint string var apiKey *string var headersJSON []byte err := database.DB.QueryRow(` SELECT provider, endpoint, api_key_enc, headers FROM provider_configs WHERE id = $1 AND is_active = true `, configID).Scan(&providerID, &endpoint, &apiKey, &headersJSON) if err != nil { return models.ModelCapabilities{}, false } provider, err := providers.Get(providerID) if err != nil { return models.ModelCapabilities{}, false } 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 { log.Printf("[caps] live query for %s via config %s failed: %v", modelID, configID, err) return models.ModelCapabilities{}, false } for _, m := range modelList { if m.ID == modelID { resolved := capspkg.ResolveIntrinsic(modelID, &m.Capabilities) return resolved, true } } return models.ModelCapabilities{}, false }