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 }