209 lines
6.6 KiB
Go
209 lines
6.6 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
)
|
|
|
|
// GetDefaultConfigDir returns the OS-specific default configuration directory for Bifrost.
|
|
// This follows standard conventions:
|
|
// - Linux/macOS: ~/.config/bifrost
|
|
// - Windows: %APPDATA%\bifrost
|
|
// - If appDir is provided (non-empty), it returns that instead
|
|
func GetDefaultConfigDir(appDir string) string {
|
|
// If appDir is provided, use it directly
|
|
if appDir != "" {
|
|
return appDir
|
|
}
|
|
|
|
// Get OS-specific config directory
|
|
var configDir string
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
// Windows: %APPDATA%\bifrost
|
|
if appData := os.Getenv("APPDATA"); appData != "" {
|
|
configDir = filepath.Join(appData, "bifrost")
|
|
} else {
|
|
// Fallback to user home directory
|
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
configDir = filepath.Join(homeDir, "AppData", "Roaming", "bifrost")
|
|
}
|
|
}
|
|
default:
|
|
// Linux, macOS and other Unix-like systems: ~/.config/bifrost
|
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
configDir = filepath.Join(homeDir, ".config", "bifrost")
|
|
}
|
|
}
|
|
|
|
// If we couldn't determine the config directory, fall back to current directory
|
|
if configDir == "" {
|
|
configDir = "./bifrost-data"
|
|
}
|
|
|
|
return configDir
|
|
}
|
|
|
|
// registerPluginWithStatus instantiates, registers, and updates status for a plugin (used by builtin plugins)
|
|
func (s *BifrostHTTPServer) registerPluginWithStatus(ctx context.Context, name string, path *string, config any, failOnError bool) error {
|
|
plugin, err := InstantiatePlugin(ctx, name, path, config, s.Config)
|
|
if err != nil {
|
|
logger.Error("failed to initialize %s plugin: %v", name, err)
|
|
// Use name since plugin may be nil when InstantiatePlugin returns an error
|
|
s.Config.UpdatePluginOverallStatus(name, name, schemas.PluginStatusError,
|
|
[]string{fmt.Sprintf("error initializing %s plugin: %v", name, err)}, []schemas.PluginType{})
|
|
if failOnError {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ensure plugin is not nil before using it (defensive check)
|
|
if plugin == nil {
|
|
logger.Error("plugin %s instantiated but returned nil", name)
|
|
s.Config.UpdatePluginOverallStatus(name, name, schemas.PluginStatusError,
|
|
[]string{fmt.Sprintf("plugin %s instantiated but returned nil", name)}, []schemas.PluginType{})
|
|
if failOnError {
|
|
return fmt.Errorf("plugin %s instantiated but returned nil", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
s.Config.ReloadPlugin(plugin)
|
|
s.Config.UpdatePluginOverallStatus(name, name, schemas.PluginStatusActive,
|
|
[]string{fmt.Sprintf("%s plugin initialized successfully", name)}, InferPluginTypes(plugin))
|
|
return nil
|
|
}
|
|
|
|
// CollectObservabilityPlugins gathers all loaded plugins that implement ObservabilityPlugin interface
|
|
func (s *BifrostHTTPServer) CollectObservabilityPlugins() []schemas.ObservabilityPlugin {
|
|
var observabilityPlugins []schemas.ObservabilityPlugin
|
|
|
|
// Check LLM plugins
|
|
for _, plugin := range s.Config.GetLoadedLLMPlugins() {
|
|
if observabilityPlugin, ok := plugin.(schemas.ObservabilityPlugin); ok {
|
|
observabilityPlugins = append(observabilityPlugins, observabilityPlugin)
|
|
}
|
|
}
|
|
|
|
// Check MCP plugins
|
|
for _, plugin := range s.Config.GetLoadedMCPPlugins() {
|
|
if observabilityPlugin, ok := plugin.(schemas.ObservabilityPlugin); ok {
|
|
observabilityPlugins = append(observabilityPlugins, observabilityPlugin)
|
|
}
|
|
}
|
|
|
|
return observabilityPlugins
|
|
}
|
|
|
|
// MarshalPluginConfig marshals the plugin configuration
|
|
func MarshalPluginConfig[T any](source any) (*T, error) {
|
|
// If its a *T, then we will confirm
|
|
if config, ok := source.(*T); ok {
|
|
return config, nil
|
|
}
|
|
// Initialize a new instance for unmarshaling
|
|
config := new(T)
|
|
// If its a map[string]any, then we will JSON parse and confirm
|
|
if configMap, ok := source.(map[string]any); ok {
|
|
configString, err := sonic.Marshal(configMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := sonic.Unmarshal([]byte(configString), config); err != nil {
|
|
return nil, err
|
|
}
|
|
return config, nil
|
|
}
|
|
// If its a string, then we will JSON parse and confirm
|
|
if configStr, ok := source.(string); ok {
|
|
if err := sonic.Unmarshal([]byte(configStr), config); err != nil {
|
|
return nil, err
|
|
}
|
|
return config, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid config type")
|
|
}
|
|
|
|
// updateKeyStatus updates the model discovery status for keys or providers based on key statuses.
|
|
// For keyed providers: updates individual key status
|
|
// For keyless providers: updates provider-level status
|
|
func (s *BifrostHTTPServer) updateKeyStatus(
|
|
ctx context.Context,
|
|
keyStatuses []schemas.KeyStatus,
|
|
) {
|
|
if s.Config == nil || s.Config.ConfigStore == nil || len(keyStatuses) == 0 {
|
|
return
|
|
}
|
|
|
|
// Update each key/provider status individually
|
|
for _, ks := range keyStatuses {
|
|
errorMsg := ""
|
|
if ks.Error != nil && ks.Error.Error != nil {
|
|
errorMsg = ks.Error.Error.Message
|
|
}
|
|
|
|
if err := s.Config.ConfigStore.UpdateStatus(ctx, ks.Provider, ks.KeyID, string(ks.Status), errorMsg); err != nil {
|
|
target := ks.KeyID
|
|
if target == "" {
|
|
target = string(ks.Provider)
|
|
}
|
|
logger.Error("failed to update model discovery status for %s: %v", target, err)
|
|
continue // Skip in-memory update if DB update failed
|
|
}
|
|
|
|
s.Config.Mu.Lock()
|
|
|
|
providerConfig, exists := s.Config.Providers[ks.Provider]
|
|
if !exists {
|
|
s.Config.Mu.Unlock()
|
|
logger.Warn("provider %s not found in memory during status update", ks.Provider)
|
|
continue
|
|
}
|
|
|
|
isKeylessProvider := providerConfig.CustomProviderConfig != nil && providerConfig.CustomProviderConfig.IsKeyLess
|
|
|
|
if ks.KeyID == "" {
|
|
if !isKeylessProvider {
|
|
logger.Warn("received provider-level status update for keyed provider %s; skipping in-memory update", ks.Provider)
|
|
s.Config.Mu.Unlock()
|
|
continue
|
|
}
|
|
providerConfig.Status = string(ks.Status)
|
|
providerConfig.Description = errorMsg
|
|
s.Config.Providers[ks.Provider] = providerConfig
|
|
logger.Debug("updated in-memory status for keyless provider %s", ks.Provider)
|
|
s.Config.Mu.Unlock()
|
|
continue
|
|
}
|
|
|
|
// Find and update the specific key in the Keys slice
|
|
updated := false
|
|
for i := range providerConfig.Keys {
|
|
if providerConfig.Keys[i].ID == ks.KeyID {
|
|
// Update Status and Description fields
|
|
providerConfig.Keys[i].Status = ks.Status
|
|
providerConfig.Keys[i].Description = errorMsg
|
|
updated = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
// Write the modified config back to the map
|
|
s.Config.Providers[ks.Provider] = providerConfig
|
|
logger.Debug("updated in-memory status for key %s of provider %s", ks.KeyID, ks.Provider)
|
|
} else {
|
|
logger.Warn("key %s not found in provider %s during in-memory update", ks.KeyID, ks.Provider)
|
|
}
|
|
|
|
s.Config.Mu.Unlock()
|
|
}
|
|
}
|