299 lines
12 KiB
Go
299 lines
12 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/plugins/compat"
|
|
"github.com/maximhq/bifrost/plugins/governance"
|
|
"github.com/maximhq/bifrost/plugins/logging"
|
|
"github.com/maximhq/bifrost/plugins/maxim"
|
|
"github.com/maximhq/bifrost/plugins/otel"
|
|
"github.com/maximhq/bifrost/plugins/prompts"
|
|
"github.com/maximhq/bifrost/plugins/semanticcache"
|
|
"github.com/maximhq/bifrost/plugins/telemetry"
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/handlers"
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
|
)
|
|
|
|
// InferPluginTypes determines which interface types a plugin implements
|
|
func InferPluginTypes(plugin schemas.BasePlugin) []schemas.PluginType {
|
|
var types []schemas.PluginType
|
|
if _, ok := plugin.(schemas.LLMPlugin); ok {
|
|
types = append(types, schemas.PluginTypeLLM)
|
|
}
|
|
if _, ok := plugin.(schemas.MCPPlugin); ok {
|
|
types = append(types, schemas.PluginTypeMCP)
|
|
}
|
|
if _, ok := plugin.(schemas.HTTPTransportPlugin); ok {
|
|
types = append(types, schemas.PluginTypeHTTP)
|
|
}
|
|
return types
|
|
}
|
|
|
|
// Single-plugin methods used plugin create/update
|
|
|
|
// InstantiatePlugin creates a plugin instance but does NOT register it
|
|
// Registration is done separately via Config.RegisterPlugin()
|
|
func InstantiatePlugin(ctx context.Context, name string, path *string, pluginConfig any, bifrostConfig *lib.Config) (schemas.BasePlugin, error) {
|
|
// Custom plugin (has path)
|
|
if path != nil {
|
|
return loadCustomPlugin(ctx, path, pluginConfig, bifrostConfig)
|
|
}
|
|
|
|
// Built-in plugin (by name)
|
|
return loadBuiltinPlugin(ctx, name, pluginConfig, bifrostConfig)
|
|
}
|
|
|
|
// loadBuiltinPlugin instantiates a built-in plugin by name
|
|
func loadBuiltinPlugin(ctx context.Context, name string, pluginConfig any, bifrostConfig *lib.Config) (schemas.BasePlugin, error) {
|
|
switch name {
|
|
case telemetry.PluginName:
|
|
telConfig := &telemetry.Config{
|
|
CustomLabels: bifrostConfig.ClientConfig.PrometheusLabels,
|
|
}
|
|
// Merge push gateway config if provided (e.g., from config file or UI update)
|
|
if pluginConfig != nil {
|
|
extraConfig, err := MarshalPluginConfig[telemetry.Config](pluginConfig)
|
|
if err == nil && extraConfig != nil && extraConfig.PushGateway != nil {
|
|
telConfig.PushGateway = extraConfig.PushGateway
|
|
}
|
|
}
|
|
return telemetry.Init(telConfig, bifrostConfig.ModelCatalog, logger)
|
|
|
|
case prompts.PluginName:
|
|
return prompts.Init(ctx, bifrostConfig.ConfigStore, logger)
|
|
|
|
case logging.PluginName:
|
|
loggingConfig, err := MarshalPluginConfig[logging.Config](pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal logging plugin config: %w", err)
|
|
}
|
|
return logging.Init(ctx, loggingConfig, logger, bifrostConfig.LogsStore,
|
|
bifrostConfig.ModelCatalog, bifrostConfig.MCPCatalog)
|
|
|
|
case governance.PluginName:
|
|
governanceConfig, err := MarshalPluginConfig[governance.Config](pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal governance plugin config: %w", err)
|
|
}
|
|
inMemoryStore := &GovernanceInMemoryStore{Config: bifrostConfig}
|
|
return governance.Init(ctx, governanceConfig, logger, bifrostConfig.ConfigStore,
|
|
bifrostConfig.GovernanceConfig, bifrostConfig.ModelCatalog,
|
|
bifrostConfig.MCPCatalog, inMemoryStore)
|
|
|
|
case maxim.PluginName:
|
|
maximConfig, err := MarshalPluginConfig[maxim.Config](pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal maxim plugin config: %w", err)
|
|
}
|
|
return maxim.Init(maximConfig, logger)
|
|
|
|
case semanticcache.PluginName:
|
|
semanticConfig, err := MarshalPluginConfig[semanticcache.Config](pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal semantic cache plugin config: %w", err)
|
|
}
|
|
return semanticcache.Init(ctx, semanticConfig, logger, bifrostConfig.VectorStore)
|
|
|
|
case otel.PluginName:
|
|
otelConfig, err := MarshalPluginConfig[otel.Config](pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal otel plugin config: %w", err)
|
|
}
|
|
return otel.Init(ctx, otelConfig, logger, bifrostConfig.ModelCatalog, handlers.GetVersion())
|
|
|
|
case compat.PluginName:
|
|
compatConfig, err := MarshalPluginConfig[compat.Config](pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal compat plugin config: %w", err)
|
|
}
|
|
return compat.Init(*compatConfig, logger, bifrostConfig.ModelCatalog)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown built-in plugin: %s", name)
|
|
}
|
|
}
|
|
|
|
// loadCustomPlugin loads a plugin from a shared object file
|
|
func loadCustomPlugin(ctx context.Context, path *string, pluginConfig any, bifrostConfig *lib.Config) (schemas.BasePlugin, error) {
|
|
logger.Info("loading custom plugin from path %s", *path)
|
|
|
|
plugin, err := bifrostConfig.PluginLoader.LoadPlugin(*path, pluginConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load custom plugin: %w", err)
|
|
}
|
|
return plugin, nil
|
|
}
|
|
|
|
// LoadPlugins loads the plugins for the server.
|
|
func (s *BifrostHTTPServer) LoadPlugins(ctx context.Context) error {
|
|
// Load built-in plugins first (order matters)
|
|
if err := s.loadBuiltinPlugins(ctx); err != nil {
|
|
return err
|
|
}
|
|
// Load custom plugins from config
|
|
if err := s.loadCustomPlugins(ctx); err != nil {
|
|
return err
|
|
}
|
|
// Sort all plugins by placement group and order
|
|
s.Config.SortAndRebuildPlugins()
|
|
return nil
|
|
}
|
|
|
|
// getPluginConfig retrieves a plugin's config from PluginConfigs by name
|
|
func (s *BifrostHTTPServer) getPluginConfig(name string) *schemas.PluginConfig {
|
|
for _, cfg := range s.Config.PluginConfigs {
|
|
if cfg.Name == name {
|
|
return cfg
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadBuiltinPlugins loads required built-in plugins in specific order
|
|
func (s *BifrostHTTPServer) loadBuiltinPlugins(ctx context.Context) error {
|
|
builtinPlacement := schemas.Ptr(schemas.PluginPlacementBuiltin)
|
|
|
|
// 1. Telemetry (always first - tracks everything)
|
|
if err := s.registerPluginWithStatus(ctx, telemetry.PluginName, nil, nil, true); err != nil {
|
|
return err
|
|
}
|
|
s.Config.SetPluginOrderInfo(telemetry.PluginName, builtinPlacement, schemas.Ptr(1))
|
|
|
|
// 2. Prompts (requires config store for prompt repository; disabled in enterprise)
|
|
if s.Config.ConfigStore != nil && ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
|
|
s.registerPluginWithStatus(ctx, prompts.PluginName, nil, nil, false)
|
|
} else {
|
|
s.markPluginDisabled(prompts.PluginName)
|
|
}
|
|
s.Config.SetPluginOrderInfo(prompts.PluginName, builtinPlacement, schemas.Ptr(2))
|
|
|
|
// 3. Logging (if enabled)
|
|
if (s.Config.ClientConfig.EnableLogging == nil || *s.Config.ClientConfig.EnableLogging) && s.Config.LogsStore != nil {
|
|
config := &logging.Config{
|
|
DisableContentLogging: &s.Config.ClientConfig.DisableContentLogging,
|
|
LoggingHeaders: &s.Config.ClientConfig.LoggingHeaders,
|
|
}
|
|
s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false)
|
|
} else {
|
|
s.markPluginDisabled(logging.PluginName)
|
|
}
|
|
s.Config.SetPluginOrderInfo(logging.PluginName, builtinPlacement, schemas.Ptr(3))
|
|
|
|
// 4. Governance (if enabled and not enterprise)
|
|
if ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
|
|
config := &governance.Config{
|
|
IsVkMandatory: &s.Config.ClientConfig.EnforceAuthOnInference,
|
|
RequiredHeaders: &s.Config.ClientConfig.RequiredHeaders,
|
|
DisableAutoToolInject: &s.Config.ClientConfig.MCPDisableAutoToolInject,
|
|
RoutingChainMaxDepth: &s.Config.ClientConfig.RoutingChainMaxDepth,
|
|
}
|
|
s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false)
|
|
} else {
|
|
s.markPluginDisabled(governance.PluginName)
|
|
}
|
|
s.Config.SetPluginOrderInfo(governance.PluginName, builtinPlacement, schemas.Ptr(4))
|
|
|
|
// 5. OTEL (if configured in PluginConfigs)
|
|
otelConfig := s.getPluginConfig(otel.PluginName)
|
|
if otelConfig != nil && otelConfig.Enabled {
|
|
s.registerPluginWithStatus(ctx, otel.PluginName, nil, otelConfig.Config, false)
|
|
} else {
|
|
s.markPluginDisabled(otel.PluginName)
|
|
}
|
|
s.Config.SetPluginOrderInfo(otel.PluginName, builtinPlacement, schemas.Ptr(5))
|
|
|
|
// 6. Semantic Cache (if configured in PluginConfigs)
|
|
semanticCacheConfig := s.getPluginConfig(semanticcache.PluginName)
|
|
if semanticCacheConfig != nil && semanticCacheConfig.Enabled {
|
|
s.registerPluginWithStatus(ctx, semanticcache.PluginName, nil, semanticCacheConfig.Config, false)
|
|
} else {
|
|
s.markPluginDisabled(semanticcache.PluginName)
|
|
}
|
|
s.Config.SetPluginOrderInfo(semanticcache.PluginName, builtinPlacement, schemas.Ptr(6))
|
|
|
|
// 7. Compat (if any compat feature is enabled in ClientConfig)
|
|
cc := s.Config.ClientConfig.Compat
|
|
compatCfg := &compat.Config{
|
|
ConvertTextToChat: cc.ConvertTextToChat,
|
|
ConvertChatToResponses: cc.ConvertChatToResponses,
|
|
ShouldDropParams: cc.ShouldDropParams,
|
|
ShouldConvertParams: cc.ShouldConvertParams,
|
|
}
|
|
s.registerPluginWithStatus(ctx, compat.PluginName, nil, compatCfg, false)
|
|
s.Config.SetPluginOrderInfo(compat.PluginName, builtinPlacement, schemas.Ptr(7))
|
|
|
|
// 8. Maxim (if configured in PluginConfigs)
|
|
maximConfig := s.getPluginConfig(maxim.PluginName)
|
|
if maximConfig != nil && maximConfig.Enabled {
|
|
s.registerPluginWithStatus(ctx, maxim.PluginName, nil, maximConfig.Config, false)
|
|
} else {
|
|
s.markPluginDisabled(maxim.PluginName)
|
|
}
|
|
s.Config.SetPluginOrderInfo(maxim.PluginName, builtinPlacement, schemas.Ptr(8))
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadCustomPlugins loads plugins from PluginConfigs
|
|
func (s *BifrostHTTPServer) loadCustomPlugins(ctx context.Context) error {
|
|
for _, cfg := range s.Config.PluginConfigs {
|
|
// Skip built-ins (already loaded)
|
|
if lib.IsBuiltinPlugin(cfg.Name) {
|
|
continue
|
|
}
|
|
// Handle disabled plugins
|
|
if !cfg.Enabled {
|
|
// For custom plugins with a path, verify to get the real plugin name
|
|
if cfg.Path != nil {
|
|
pluginName, err := s.Config.PluginLoader.VerifyBasePlugin(*cfg.Path)
|
|
if err != nil {
|
|
logger.Error("failed to verify disabled plugin %s: %v", cfg.Name, err)
|
|
continue
|
|
}
|
|
// Store plugin status without instantiating (no Init() call, no resource usage)
|
|
// Note: We can't determine types without instantiating, so pass empty slice
|
|
s.Config.UpdatePluginOverallStatus(pluginName, cfg.Name, schemas.PluginStatusDisabled,
|
|
[]string{fmt.Sprintf("plugin %s is disabled", cfg.Name)}, []schemas.PluginType{})
|
|
} else {
|
|
// Built-in plugin - use cfg.Name directly
|
|
s.Config.UpdatePluginOverallStatus(cfg.Name, cfg.Name, schemas.PluginStatusDisabled,
|
|
[]string{fmt.Sprintf("plugin %s is disabled", cfg.Name)}, []schemas.PluginType{})
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Plugin is enabled - instantiate it
|
|
plugin, err := InstantiatePlugin(ctx, cfg.Name, cfg.Path, cfg.Config, s.Config)
|
|
if err != nil {
|
|
// Skip enterprise plugins silently
|
|
if slices.Contains(enterprisePlugins, cfg.Name) {
|
|
continue
|
|
}
|
|
logger.Error("failed to load plugin %s: %v", cfg.Name, err)
|
|
// Use cfg.Name since plugin may be nil when InstantiatePlugin returns an error
|
|
s.Config.UpdatePluginOverallStatus(cfg.Name, cfg.Name, schemas.PluginStatusError,
|
|
[]string{fmt.Sprintf("error loading plugin %s: %v", cfg.Name, err)}, []schemas.PluginType{})
|
|
continue
|
|
}
|
|
|
|
// Ensure plugin is not nil before using it (defensive check)
|
|
if plugin == nil {
|
|
logger.Error("plugin %s instantiated but returned nil", cfg.Name)
|
|
s.Config.UpdatePluginOverallStatus(cfg.Name, cfg.Name, schemas.PluginStatusError,
|
|
[]string{fmt.Sprintf("plugin %s instantiated but returned nil", cfg.Name)}, []schemas.PluginType{})
|
|
continue
|
|
}
|
|
|
|
// Register enabled plugin and mark as active
|
|
s.Config.ReloadPlugin(plugin)
|
|
s.Config.SetPluginOrderInfo(plugin.GetName(), cfg.Placement, cfg.Order)
|
|
s.Config.UpdatePluginOverallStatus(plugin.GetName(), cfg.Name, schemas.PluginStatusActive,
|
|
[]string{fmt.Sprintf("plugin %s initialized successfully", cfg.Name)}, InferPluginTypes(plugin))
|
|
}
|
|
return nil
|
|
}
|