- Replaced 'Voice' with 'Audio' in the system configuration and related tests to better reflect functionality. - Introduced new methods for role management in the llmprovider, allowing for dynamic retrieval of roles based on user and team context. - Updated the OpenAPI settings to support new role management endpoints and capabilities. - Enhanced the handling of API keys in provider management, allowing for optional plain-text retrieval.
353 lines
7.8 KiB
Go
353 lines
7.8 KiB
Go
package llmprovider
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/yaoapp/gou/connector"
|
|
"github.com/yaoapp/gou/store"
|
|
)
|
|
|
|
// Global is the singleton LLM Provider Registry.
|
|
var Global *Registry
|
|
|
|
// Registry manages LLM providers with CRUD, persistence, cache and runtime sync.
|
|
type Registry struct {
|
|
store store.Store
|
|
cache store.Store
|
|
encKey string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// Init initializes the global Registry.
|
|
// Must be called after store.Load (so __yao.store and __yao.cache are available).
|
|
func Init() error {
|
|
s, err := store.Get("__yao.store")
|
|
if err != nil {
|
|
return fmt.Errorf("llmprovider.Init: %w", err)
|
|
}
|
|
c, _ := store.Get("__yao.cache")
|
|
|
|
r := &Registry{store: s, cache: c}
|
|
Global = r
|
|
|
|
if err := importFromConnectors(r); err != nil {
|
|
return fmt.Errorf("llmprovider.Init importFromConnectors: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetEncryptionKey sets the key used for API key encryption at rest.
|
|
// Should be called right after Init if encryption is desired.
|
|
func (r *Registry) SetEncryptionKey(key string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.encKey = key
|
|
}
|
|
|
|
// shouldExposeKey returns true when the caller explicitly requests plain-text APIKey.
|
|
func shouldExposeKey(withKey []bool) bool {
|
|
return len(withKey) > 0 && withKey[0]
|
|
}
|
|
|
|
// Get retrieves a provider by key. Lazily ensures its connector is registered.
|
|
// By default the APIKey is masked; pass withKey=true to get the plain-text key
|
|
// (only for internal LLM-request paths).
|
|
func (r *Registry) Get(key string, withKey ...bool) (*Provider, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
p, err := storeGet(r.store, r.cache, key, r.encKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_ = ensureConnector(p)
|
|
|
|
if !shouldExposeKey(withKey) {
|
|
cp := *p
|
|
cp.APIKey = maskAPIKey(cp.APIKey)
|
|
return &cp, nil
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// GetByConnectorID finds a provider by its ConnectorID field (linear scan).
|
|
// Use when the caller has a ConnectorID but not the store Key.
|
|
// By default the APIKey is masked; pass withKey=true to get the plain-text key.
|
|
func (r *Registry) GetByConnectorID(cid string, withKey ...bool) (*Provider, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
keys, err := indexGet(r.store, r.cache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, key := range keys {
|
|
p, err := storeGet(r.store, r.cache, key, r.encKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if p.ConnectorID == cid {
|
|
_ = ensureConnector(p)
|
|
if !shouldExposeKey(withKey) {
|
|
cp := *p
|
|
cp.APIKey = maskAPIKey(cp.APIKey)
|
|
return &cp, nil
|
|
}
|
|
return p, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("provider with connector_id %q not found", cid)
|
|
}
|
|
|
|
// Deprecated: GetMasked is equivalent to Get(key) since Get now masks by default.
|
|
func (r *Registry) GetMasked(key string) (*Provider, error) {
|
|
return r.Get(key)
|
|
}
|
|
|
|
// Create adds a new provider. Persists, caches, registers connector, and updates index.
|
|
func (r *Registry) Create(p *Provider) (*Provider, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if p.Key == "" {
|
|
return nil, fmt.Errorf("provider key is required")
|
|
}
|
|
if r.store.Has(storeKey(p.Key)) {
|
|
return nil, fmt.Errorf("provider %s already exists", p.Key)
|
|
}
|
|
|
|
if p.Source == "" {
|
|
p.Source = ProviderSourceDynamic
|
|
}
|
|
if p.ConnectorID == "" {
|
|
p.ConnectorID = connectorID(p)
|
|
}
|
|
if p.Status == "" {
|
|
p.Status = "unconfigured"
|
|
}
|
|
|
|
if err := storeSet(r.store, r.cache, p, r.encKey); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := indexAdd(r.store, r.cache, p.Key); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.Enabled {
|
|
_ = ensureConnector(p)
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// Update modifies an existing provider. Hot-replaces the connector if needed.
|
|
func (r *Registry) Update(key string, p *Provider) (*Provider, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
old, err := storeGet(r.store, r.cache, key, r.encKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p.Key = key
|
|
if p.Source == "" {
|
|
p.Source = old.Source
|
|
}
|
|
if p.ConnectorID == "" {
|
|
p.ConnectorID = old.ConnectorID
|
|
}
|
|
if p.Owner == (ProviderOwner{}) {
|
|
p.Owner = old.Owner
|
|
}
|
|
|
|
_ = unregisterConnector(old)
|
|
|
|
if err := storeSet(r.store, r.cache, p, r.encKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.Enabled {
|
|
_ = ensureConnector(p)
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// Delete removes a provider by key. Unregisters connector, deletes store/cache/index.
|
|
func (r *Registry) Delete(key string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
p, err := storeGet(r.store, r.cache, key, r.encKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = unregisterConnector(p)
|
|
|
|
if err := storeDel(r.store, r.cache, key); err != nil {
|
|
return err
|
|
}
|
|
return indexRemove(r.store, r.cache, key)
|
|
}
|
|
|
|
// List returns providers matching the filter.
|
|
// By default the APIKey is masked; pass withKey=true to get plain-text keys.
|
|
func (r *Registry) List(filter *ProviderFilter, withKey ...bool) ([]Provider, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
keys, err := indexGet(r.store, r.cache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expose := shouldExposeKey(withKey)
|
|
var result []Provider
|
|
for _, key := range keys {
|
|
p, err := storeGet(r.store, r.cache, key, r.encKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if filter != nil && !matchFilter(p, filter) {
|
|
continue
|
|
}
|
|
cp := *p
|
|
if !expose {
|
|
cp.APIKey = maskAPIKey(cp.APIKey)
|
|
}
|
|
result = append(result, cp)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Reload re-reads all providers from persistent store and rebuilds cache + connectors.
|
|
func (r *Registry) Reload() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
keys, err := indexGet(r.store, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, key := range keys {
|
|
p, err := storeGet(r.store, nil, key, r.encKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
m, err := providerToMap(p, r.encKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if r.cache != nil {
|
|
r.cache.Set(storeKey(key), m, 0)
|
|
}
|
|
if p.Source == ProviderSourceDynamic && p.Enabled {
|
|
_ = ensureConnector(p)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetConnector returns the runtime connector for a given provider key.
|
|
func (r *Registry) GetConnector(key string) (connector.Connector, error) {
|
|
p, err := r.Get(key, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cid := p.ConnectorID
|
|
if cid == "" {
|
|
cid = connectorID(p)
|
|
}
|
|
return connector.Select(cid)
|
|
}
|
|
|
|
// GetSetting returns the runtime connector setting map for a given provider key.
|
|
func (r *Registry) GetSetting(key string) (map[string]interface{}, error) {
|
|
conn, err := r.GetConnector(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return conn.Setting(), nil
|
|
}
|
|
|
|
// matchFilter checks if a provider matches the given filter.
|
|
func matchFilter(p *Provider, f *ProviderFilter) bool {
|
|
src := f.Source
|
|
if src == "" {
|
|
src = ProviderSourceDynamic
|
|
}
|
|
if src != ProviderSourceAll && p.Source != src {
|
|
return false
|
|
}
|
|
|
|
if f.Owner != nil {
|
|
if f.Owner.Type != "" && p.Owner.Type != f.Owner.Type {
|
|
return false
|
|
}
|
|
if f.Owner.UserID != "" && p.Owner.UserID != f.Owner.UserID {
|
|
return false
|
|
}
|
|
if f.Owner.TeamID != "" && p.Owner.TeamID != f.Owner.TeamID {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if f.Enabled != nil && p.Enabled != *f.Enabled {
|
|
return false
|
|
}
|
|
|
|
if f.Type != nil && p.Type != *f.Type {
|
|
return false
|
|
}
|
|
|
|
if f.PresetKey != nil && p.PresetKey != *f.PresetKey {
|
|
return false
|
|
}
|
|
|
|
if len(f.Capabilities) > 0 && !matchCapabilities(p, f.Capabilities) {
|
|
return false
|
|
}
|
|
|
|
if f.Keyword != "" {
|
|
kw := strings.ToLower(f.Keyword)
|
|
if !strings.Contains(strings.ToLower(p.Name), kw) &&
|
|
!strings.Contains(strings.ToLower(p.Key), kw) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// matchCapabilities returns true if at least one model in the provider
|
|
// satisfies ALL of the required capabilities (AND logic).
|
|
func matchCapabilities(p *Provider, required []string) bool {
|
|
for _, m := range p.Models {
|
|
if !m.Enabled {
|
|
continue
|
|
}
|
|
capSet := make(map[string]bool, len(m.Capabilities))
|
|
for _, c := range m.Capabilities {
|
|
capSet[c] = true
|
|
}
|
|
allMatch := true
|
|
for _, req := range required {
|
|
if !capSet[req] {
|
|
allMatch = false
|
|
break
|
|
}
|
|
}
|
|
if allMatch {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|