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-02-22 16:52:19 +00:00

138 lines
4.3 KiB
Go

package handlers
import (
"encoding/json"
"log"
"github.com/gin-gonic/gin"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/providers"
)
// 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)
// 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 ──
if configID != "" {
caps, ok := capsFromModelConfigs(modelID, configID)
if ok {
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
return caps
}
}
// ── 2. Any provider: same model_id, any config ──
caps, ok := capsFromModelConfigs(modelID, "")
if ok {
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
return caps
}
// ── 3. Known model table (static, curated) ──
caps, found := providers.LookupKnownModel(modelID)
if found {
caps.MaxOutputTokens = providers.ResolveMaxOutput(modelID, caps)
return caps
}
// ── 4. Heuristic inference ──
caps = providers.InferCapabilities(modelID)
caps.MaxOutputTokens = providers.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) {
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
`, 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
`, modelID).Scan(&capsJSON)
}
if err != nil || len(capsJSON) == 0 {
return providers.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
}
}
// Fall through to canonical resolver
return ResolveModelCaps(c, modelID, configID)
}
// 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) {
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
`, configID).Scan(&providerID, &endpoint, &apiKey, &headersJSON)
if err != nil {
return providers.ModelCapabilities{}, false
}
provider, err := providers.Get(providerID)
if err != nil {
return providers.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 providers.ModelCapabilities{}, false
}
for _, m := range modelList {
if m.ID == modelID {
caps := providers.MergeCapabilities(m.Capabilities, modelID)
return caps, true
}
}
return providers.ModelCapabilities{}, false
}