Changeset 0.9.0 (#50)
This commit is contained in:
@@ -3,109 +3,130 @@ 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.
|
||||
// It walks a priority chain and returns the best capabilities available:
|
||||
//
|
||||
// 1. model_configs DB — exact match (model_id + api_config_id)
|
||||
// 2. model_configs DB — any provider (same model, different config)
|
||||
// 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)
|
||||
//
|
||||
// configID is optional — pass "" to skip the exact-match step.
|
||||
func ResolveModelCaps(c *gin.Context, modelID, configID string) providers.ModelCapabilities {
|
||||
// ── 1. Exact match: model_id + api_config_id ──
|
||||
func ResolveModelCaps(c *gin.Context, modelID, configID string) models.ModelCapabilities {
|
||||
// 1. Exact match: model_id + provider_config_id
|
||||
if configID != "" {
|
||||
caps, ok := capsFromModelConfigs(modelID, configID)
|
||||
caps, ok := capsFromCatalog(modelID, configID)
|
||||
if ok {
|
||||
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
|
||||
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
||||
return caps
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Any provider: same model_id, any config ──
|
||||
caps, ok := capsFromModelConfigs(modelID, "")
|
||||
// 2. Any provider: same model_id, any config
|
||||
caps, ok := capsFromCatalog(modelID, "")
|
||||
if ok {
|
||||
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
|
||||
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
||||
return caps
|
||||
}
|
||||
|
||||
// ── 3. Known model table (static, curated) ──
|
||||
caps, found := providers.LookupKnownModel(modelID)
|
||||
// 3. Known model table (static, curated)
|
||||
caps, found := capspkg.LookupKnownModel(modelID)
|
||||
if found {
|
||||
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
|
||||
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
||||
return caps
|
||||
}
|
||||
|
||||
// ── 4. Heuristic inference ──
|
||||
caps = providers.InferCapabilities(modelID)
|
||||
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
|
||||
// 4. Heuristic inference
|
||||
caps = capspkg.InferCapabilities(modelID)
|
||||
caps.MaxOutputTokens = capspkg.ResolveMaxOutput(modelID, caps)
|
||||
return caps
|
||||
}
|
||||
|
||||
// capsFromModelConfigs looks up capabilities from the model_configs table.
|
||||
// If configID is non-empty, it matches exactly; otherwise it finds any entry
|
||||
// for the model_id (capabilities for the same model are provider-agnostic).
|
||||
func capsFromModelConfigs(modelID, configID string) (providers.ModelCapabilities, bool) {
|
||||
// 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_configs
|
||||
WHERE model_id = $1 AND api_config_id = $2
|
||||
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_configs
|
||||
WHERE model_id = $1 ORDER BY updated_at DESC LIMIT 1
|
||||
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 providers.ModelCapabilities{}, false
|
||||
return models.ModelCapabilities{}, false
|
||||
}
|
||||
var caps providers.ModelCapabilities
|
||||
if json.Unmarshal(capsJSON, &caps) != nil || !caps.HasProviderData() {
|
||||
return providers.ModelCapabilities{}, false
|
||||
}
|
||||
return providers.MergeCapabilities(caps, modelID), true
|
||||
}
|
||||
|
||||
// ResolveModelCapsFromLoaded checks an existing slice of models first (avoids
|
||||
// redundant DB/network calls when we already have models in memory).
|
||||
func ResolveModelCapsFromLoaded(c *gin.Context, modelID, configID string, loaded []enabledModel) providers.ModelCapabilities {
|
||||
// Check already-loaded models first
|
||||
for _, m := range loaded {
|
||||
if m.ModelID == modelID {
|
||||
return m.Capabilities
|
||||
}
|
||||
var caps models.ModelCapabilities
|
||||
if json.Unmarshal(capsJSON, &caps) != nil || !caps.HasProviderData() {
|
||||
return models.ModelCapabilities{}, false
|
||||
}
|
||||
// Fall through to canonical resolver
|
||||
return ResolveModelCaps(c, modelID, configID)
|
||||
|
||||
// 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.
|
||||
// Used for team provider presets whose base model isn't in model_configs.
|
||||
func liveQueryModelCaps(c *gin.Context, configID, modelID string) (providers.ModelCapabilities, bool) {
|
||||
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_encrypted, custom_headers
|
||||
FROM api_configs WHERE id = $1 AND is_active = true
|
||||
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 providers.ModelCapabilities{}, false
|
||||
return models.ModelCapabilities{}, false
|
||||
}
|
||||
|
||||
provider, err := providers.Get(providerID)
|
||||
if err != nil {
|
||||
return providers.ModelCapabilities{}, false
|
||||
return models.ModelCapabilities{}, false
|
||||
}
|
||||
|
||||
key := ""
|
||||
@@ -123,15 +144,15 @@ func liveQueryModelCaps(c *gin.Context, configID, modelID string) (providers.Mod
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[caps] live query for %s via config %s failed: %v", modelID, configID, err)
|
||||
return providers.ModelCapabilities{}, false
|
||||
return models.ModelCapabilities{}, false
|
||||
}
|
||||
|
||||
for _, m := range modelList {
|
||||
if m.ID == modelID {
|
||||
caps := providers.MergeCapabilities(m.Capabilities, modelID)
|
||||
return caps, true
|
||||
resolved := capspkg.ResolveIntrinsic(modelID, &m.Capabilities)
|
||||
return resolved, true
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ModelCapabilities{}, false
|
||||
return models.ModelCapabilities{}, false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user