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/capabilities/resolver.go
2026-02-23 23:55:22 +00:00

286 lines
8.9 KiB
Go

package capabilities
import (
"context"
"log"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/store"
)
// ModelsForUser returns all models and Personas visible to a user.
//
// Visibility is controlled by the three-state visibility field on each
// catalog entry (enabled / team / disabled), applied per provider scope:
//
// Tier | enabled | team | disabled
// ----------+----------------+----------------------+---------
// Global | All users | Team admin → presets | Hidden
// Team | Team members | Team admin → presets | Hidden
// Personal | Owner direct | N/A | Hidden
//
// Sources aggregated:
// 1. Global catalog: enabled models → all authenticated users
// 2. Team catalog: enabled models → team members
// 3. Personal BYOK: enabled models → owner (if allow_user_byok)
// 4. Personas: global + team-scoped + personal + shared
// 5. User hidden preferences applied last
func ModelsForUser(ctx context.Context, stores store.Stores, userID string) ([]models.UserModel, error) {
result := make([]models.UserModel, 0) // never nil — serializes as [] not null
// Load policies once
policies, err := stores.Policies.GetAll(ctx)
if err != nil {
return nil, err
}
allowBYOK := policies["allow_user_byok"] == "true"
// Load user's hidden preferences
hiddenMap, err := stores.UserSettings.GetHiddenModelIDs(ctx, userID)
if err != nil {
log.Printf("warn: failed to load user model settings: %v", err)
hiddenMap = make(map[string]bool)
}
// Get user's team IDs
teamIDs, err := stores.Teams.GetUserTeamIDs(ctx, userID)
if err != nil {
log.Printf("warn: failed to load user teams: %v", err)
}
// ── 1. Global enabled catalog models → all users ────
globalModels, err := stores.Catalog.ListVisible(ctx)
if err != nil {
return nil, err
}
// Build provider name lookup for global providers
globalProviders, _ := stores.Providers.ListGlobal(ctx)
providerMap := make(map[string]models.ProviderConfig)
for _, p := range globalProviders {
providerMap[p.ID] = p
}
countGlobal := len(globalModels)
for _, entry := range globalModels {
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities)
prov := providerMap[entry.ProviderConfigID]
result = append(result, models.UserModel{
ID: entry.ModelID,
DisplayName: displayName(entry.DisplayName, entry.ModelID),
ModelID: entry.ModelID,
Source: "catalog",
ProviderConfigID: entry.ProviderConfigID,
ConfigID: entry.ProviderConfigID,
ProviderName: prov.Name,
ProviderType: prov.Provider,
Capabilities: caps,
Pricing: entry.Pricing,
Scope: models.ScopeGlobal,
Hidden: hiddenMap[entry.ModelID],
})
}
// ── 2. Team enabled catalog models → team members ────
countTeam := 0
for _, teamID := range teamIDs {
teamProviders, err := stores.Providers.ListForTeam(ctx, teamID)
if err != nil {
log.Printf("warn: failed to load team %s providers: %v", teamID, err)
continue
}
for _, prov := range teamProviders {
if !prov.IsActive {
continue
}
entries, err := stores.Catalog.ListEnabledForProvider(ctx, prov.ID)
if err != nil {
continue
}
for _, entry := range entries {
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities)
result = append(result, models.UserModel{
ID: entry.ModelID,
DisplayName: displayName(entry.DisplayName, entry.ModelID),
ModelID: entry.ModelID,
Source: "catalog",
ProviderConfigID: prov.ID,
ConfigID: prov.ID,
ProviderName: prov.Name,
ProviderType: prov.Provider,
Capabilities: caps,
Pricing: entry.Pricing,
Scope: models.ScopeTeam,
OwnerID: prov.OwnerID,
Hidden: hiddenMap[entry.ModelID],
})
countTeam++
}
}
}
// ── 3. Personal BYOK enabled models → owner ────
countPersonal := 0
if allowBYOK {
personalProviders, err := stores.Providers.ListForUser(ctx, userID)
if err != nil {
log.Printf("warn: failed to load personal providers: %v", err)
} else {
for _, prov := range personalProviders {
if !prov.IsActive {
continue
}
entries, err := stores.Catalog.ListEnabledForProvider(ctx, prov.ID)
if err != nil {
continue
}
for _, entry := range entries {
caps := ResolveIntrinsic(entry.ModelID, &entry.Capabilities)
result = append(result, models.UserModel{
ID: entry.ModelID,
DisplayName: displayName(entry.DisplayName, entry.ModelID),
ModelID: entry.ModelID,
Source: "catalog",
ProviderConfigID: prov.ID,
ConfigID: prov.ID,
ProviderName: prov.Name,
ProviderType: prov.Provider,
Capabilities: caps,
Pricing: entry.Pricing,
Scope: models.ScopePersonal,
OwnerID: &userID,
Hidden: hiddenMap[entry.ModelID],
})
countPersonal++
}
}
}
}
// ── 4. Personas (always resolved) ────────────────
personas, err := stores.Personas.ListForUser(ctx, userID)
if err != nil {
log.Printf("warn: failed to load personas: %v", err)
} else {
for _, p := range personas {
// Resolve base model capabilities
var catalogCaps *models.ModelCapabilities
if p.ProviderConfigID != nil {
if entry, err := stores.Catalog.GetByModelID(ctx, *p.ProviderConfigID, p.BaseModelID); err == nil {
catalogCaps = &entry.Capabilities
}
}
// Fallback: look up any provider's catalog entry for this model
// (covers auto-resolve presets where provider_config_id is NULL)
if catalogCaps == nil {
if entry, err := stores.Catalog.GetByModelIDAny(ctx, p.BaseModelID); err == nil {
catalogCaps = &entry.Capabilities
}
}
caps := ResolveIntrinsic(p.BaseModelID, catalogCaps)
// Load tool grants
toolGrants, _ := stores.Personas.GetToolGrants(ctx, p.ID)
if len(toolGrants) > 0 {
caps.ToolCalling = true
}
// Look up provider info
var provName, provType string
if p.ProviderConfigID != nil {
if prov, err := stores.Providers.GetByID(ctx, *p.ProviderConfigID); err == nil {
provName = prov.Name
provType = prov.Provider
}
}
result = append(result, models.UserModel{
ID: p.ID,
DisplayName: p.Name,
ModelID: p.BaseModelID,
Source: "persona",
ProviderConfigID: deref(p.ProviderConfigID),
ConfigID: deref(p.ProviderConfigID),
ProviderName: provName,
ProviderType: provType,
Capabilities: caps,
IsPreset: true,
PresetID: p.ID,
PresetScope: p.Scope,
PresetAvatar: p.Avatar,
PresetTeamName: teamName(p, stores, ctx),
PersonaID: p.ID,
Description: p.Description,
Icon: p.Icon,
Avatar: p.Avatar,
SystemPrompt: p.SystemPrompt,
Temperature: p.Temperature,
MaxTokens: p.MaxTokens,
ToolGrants: toolGrants,
Scope: p.Scope,
OwnerID: p.OwnerID,
Hidden: hiddenMap[p.ID],
})
}
}
countPersonas := len(result) - countGlobal - countTeam - countPersonal
log.Printf("📋 ModelsForUser(%s): %d global, %d team, %d personal, %d personas → %d total",
userID, countGlobal, countTeam, countPersonal, countPersonas, len(result))
return result, nil
}
// ResolveForPersona returns effective capabilities for a specific Persona.
// Used at completion time to determine what tools/features are available.
func ResolveForPersona(ctx context.Context, stores store.Stores, persona *models.Persona) (models.ModelCapabilities, []string, error) {
var catalogCaps *models.ModelCapabilities
if persona.ProviderConfigID != nil {
if entry, err := stores.Catalog.GetByModelID(ctx, *persona.ProviderConfigID, persona.BaseModelID); err == nil {
catalogCaps = &entry.Capabilities
}
}
// Fallback: any provider's catalog entry (auto-resolve presets)
if catalogCaps == nil {
if entry, err := stores.Catalog.GetByModelIDAny(ctx, persona.BaseModelID); err == nil {
catalogCaps = &entry.Capabilities
}
}
caps := ResolveIntrinsic(persona.BaseModelID, catalogCaps)
toolGrants, err := stores.Personas.GetToolGrants(ctx, persona.ID)
if err != nil {
return caps, nil, err
}
return caps, toolGrants, nil
}
func displayName(name, modelID string) string {
if name != "" {
return name
}
return modelID
}
func deref(s *string) string {
if s == nil {
return ""
}
return *s
}
// teamName resolves the team_name for a persona's owner (if team-scoped).
func teamName(p models.Persona, stores store.Stores, ctx context.Context) string {
if p.Scope != models.ScopeTeam || p.OwnerID == nil {
return ""
}
team, err := stores.Teams.GetByID(ctx, *p.OwnerID)
if err != nil {
return ""
}
return team.Name
}