Changeset 0.9.0 (#50)
This commit is contained in:
276
server/capabilities/resolver.go
Normal file
276
server/capabilities/resolver.go
Normal file
@@ -0,0 +1,276 @@
|
||||
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
|
||||
}
|
||||
|
||||
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 ────
|
||||
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],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Personal BYOK enabled models → owner ────
|
||||
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],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user