Files
bifrost/transports/bifrost-http/server/server.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

1545 lines
62 KiB
Go

// Package server provides the HTTP server for Bifrost.
package server
import (
"context"
"embed"
"errors"
"fmt"
"net"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/fasthttp/router"
"github.com/google/uuid"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/configstore"
"github.com/maximhq/bifrost/framework/configstore/tables"
"github.com/maximhq/bifrost/framework/logstore"
dynamicPlugins "github.com/maximhq/bifrost/framework/plugins"
"github.com/maximhq/bifrost/framework/tracing"
"github.com/maximhq/bifrost/plugins/governance"
"github.com/maximhq/bifrost/plugins/logging"
"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/integrations"
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
bfws "github.com/maximhq/bifrost/transports/bifrost-http/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
)
// Constants
const (
DefaultHost = "localhost"
DefaultPort = "8080"
DefaultAppDir = "" // Empty string means use OS-specific config directory
DefaultLogLevel = string(schemas.LogLevelInfo)
DefaultLogOutputStyle = string(schemas.LoggerOutputTypeJSON)
)
var enterprisePlugins = []string{
"datadog",
}
// ServerCallbacks is a interface that defines the callbacks for the server.
type ServerCallbacks interface {
// Plugins callbacks
ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any, placement *schemas.PluginPlacement, order *int) error
RemovePlugin(ctx context.Context, name string) error
GetPluginStatus(ctx context.Context) map[string]schemas.PluginStatus
// Auth related callbacks
UpdateAuthConfig(ctx context.Context, authConfig *configstore.AuthConfig) error
ReloadClientConfigFromConfigStore(ctx context.Context) error
// Pricing related callbacks
UpdateSyncConfig(ctx context.Context) error
ForceReloadPricing(ctx context.Context) error
UpsertPricingOverride(ctx context.Context, override *tables.TablePricingOverride) error
DeletePricingOverride(ctx context.Context, id string) error
// Proxy related callbacks
ReloadProxyConfig(ctx context.Context, config *tables.GlobalProxyConfig) error
// Client config related callbacks
ReloadHeaderFilterConfig(ctx context.Context, config *tables.GlobalHeaderFilterConfig) error
UpdateDropExcessRequests(ctx context.Context, value bool)
// Governance related callbacks
GetGovernanceData(ctx context.Context) *governance.GovernanceData
ReloadTeam(ctx context.Context, id string) (*tables.TableTeam, error)
RemoveTeam(ctx context.Context, id string) error
ReloadCustomer(ctx context.Context, id string) (*tables.TableCustomer, error)
RemoveCustomer(ctx context.Context, id string) error
// Virtual key related callbacks
ReloadVirtualKey(ctx context.Context, id string) (*tables.TableVirtualKey, error)
RemoveVirtualKey(ctx context.Context, id string) error
// Provider related callbacks
GetModelsForProvider(provider schemas.ModelProvider) []string
GetUnfilteredModelsForProvider(provider schemas.ModelProvider) []string
ReloadModelConfig(ctx context.Context, id string) (*tables.TableModelConfig, error)
RemoveModelConfig(ctx context.Context, id string) error
ReloadProvider(ctx context.Context, provider schemas.ModelProvider) (*tables.TableProvider, error)
RemoveProvider(ctx context.Context, provider schemas.ModelProvider) error
ReloadRoutingRule(ctx context.Context, id string) error
RemoveRoutingRule(ctx context.Context, id string) error
// MCP related callbacks
AddMCPClient(ctx context.Context, clientConfig *schemas.MCPClientConfig) error
RemoveMCPClient(ctx context.Context, id string) error
UpdateMCPClient(ctx context.Context, id string, updatedConfig *schemas.MCPClientConfig) error
UpdateMCPToolManagerConfig(ctx context.Context, maxAgentDepth int, toolExecutionTimeoutInSeconds int, codeModeBindingLevel string, disableAutoToolInject bool) error
// VerifyPerUserOAuthConnection verifies an MCP server using a temporary token and discovers tools.
VerifyPerUserOAuthConnection(ctx context.Context, config *schemas.MCPClientConfig, accessToken string) (map[string]schemas.ChatTool, map[string]string, error)
// SetClientTools updates the tool map for an existing client.
SetClientTools(clientID string, tools map[string]schemas.ChatTool, toolNameMapping map[string]string)
ReconnectMCPClient(ctx context.Context, id string) error
}
// BifrostHTTPServer represents a HTTP server instance.
type BifrostHTTPServer struct {
Ctx *schemas.BifrostContext
cancel context.CancelFunc
Version string
UIContent embed.FS
Port string
Host string
AppDir string
LogLevel string
LogOutputStyle string
LogsCleaner *logstore.LogsCleaner
AsyncJobCleaner *logstore.AsyncJobCleaner
Client *bifrost.Bifrost
Config *lib.Config
Server *fasthttp.Server
Router *router.Router
WebSocketHandler *handlers.WebSocketHandler
MCPServerHandler *handlers.MCPServerHandler
devPprofHandler *handlers.DevPprofHandler
IntegrationHandler *handlers.IntegrationHandler
AuthMiddleware *handlers.AuthMiddleware
TracingMiddleware *handlers.TracingMiddleware
WSTicketStore *handlers.WSTicketStore
wsPool *bfws.Pool
}
var logger schemas.Logger
// SetLogger sets the logger for the server.
func SetLogger(l schemas.Logger) {
logger = l
}
// NewBifrostHTTPServer creates a new instance of BifrostHTTPServer.
func NewBifrostHTTPServer(version string, uiContent embed.FS) *BifrostHTTPServer {
return &BifrostHTTPServer{
Version: version,
UIContent: uiContent,
Port: DefaultPort,
Host: DefaultHost,
AppDir: DefaultAppDir,
LogLevel: DefaultLogLevel,
LogOutputStyle: DefaultLogOutputStyle,
}
}
type GovernanceInMemoryStore struct {
Config *lib.Config
}
func (s *GovernanceInMemoryStore) GetConfiguredProviders() map[schemas.ModelProvider]configstore.ProviderConfig {
// Use read lock for thread-safe access - no need to copy on hot path
s.Config.Mu.RLock()
defer s.Config.Mu.RUnlock()
return s.Config.Providers
}
func (s *GovernanceInMemoryStore) GetMCPClientsAllowingAllVirtualKeys() map[string]string {
return s.Config.GetAllowOnAllVirtualKeysClients()
}
// AddMCPClient adds a new MCP client to the in-memory store
func (s *BifrostHTTPServer) AddMCPClient(ctx context.Context, clientConfig *schemas.MCPClientConfig) error {
if err := s.Config.AddMCPClient(ctx, clientConfig); err != nil {
return err
}
if err := s.MCPServerHandler.SyncAllMCPServers(ctx); err != nil {
logger.Warn("failed to sync MCP servers after adding client: %v", err)
}
return nil
}
// ReconnectMCPClient reconnects an MCP client to the in-memory store
func (s *BifrostHTTPServer) ReconnectMCPClient(ctx context.Context, id string) error {
// Check if client is registered in Bifrost (can be not registered if client initialization failed)
if clients, err := s.Client.GetMCPClients(); err == nil && len(clients) > 0 {
for _, client := range clients {
if client.Config.ID == id {
if err := s.Client.ReconnectMCPClient(id); err != nil {
return err
}
return nil
}
}
}
// Config exists in store, but not in Bifrost (can happen if client initialization failed)
clientConfig, err := s.Config.GetMCPClient(id)
if err != nil {
return err
}
if err := s.Client.AddMCPClient(clientConfig); err != nil {
return err
}
if err := s.MCPServerHandler.SyncAllMCPServers(ctx); err != nil {
logger.Warn("failed to sync MCP servers after adding client: %v", err)
}
return nil
}
// UpdateMCPClient updates an MCP client in the in-memory store
func (s *BifrostHTTPServer) UpdateMCPClient(ctx context.Context, id string, updatedConfig *schemas.MCPClientConfig) error {
if err := s.Config.UpdateMCPClient(ctx, id, updatedConfig); err != nil {
return err
}
if err := s.MCPServerHandler.SyncAllMCPServers(ctx); err != nil {
logger.Warn("failed to sync MCP servers after editing client: %v", err)
}
return nil
}
// RemoveMCPClient removes an MCP client from the in-memory store
func (s *BifrostHTTPServer) RemoveMCPClient(ctx context.Context, id string) error {
if err := s.Config.RemoveMCPClient(ctx, id); err != nil {
return err
}
if err := s.MCPServerHandler.SyncAllMCPServers(ctx); err != nil {
logger.Warn("failed to sync MCP servers after removing client: %v", err)
}
return nil
}
// VerifyPerUserOAuthConnection delegates to the Bifrost client to verify an MCP
// server using a temporary access token and discover available tools.
func (s *BifrostHTTPServer) VerifyPerUserOAuthConnection(ctx context.Context, config *schemas.MCPClientConfig, accessToken string) (map[string]schemas.ChatTool, map[string]string, error) {
return s.Client.VerifyPerUserOAuthConnection(ctx, config, accessToken)
}
// SetClientTools delegates to the Bifrost client to update tool map for an existing MCP client,
// then re-syncs the MCP server so the new tools are immediately visible via /mcp.
func (s *BifrostHTTPServer) SetClientTools(clientID string, tools map[string]schemas.ChatTool, toolNameMapping map[string]string) {
s.Client.SetClientTools(clientID, tools, toolNameMapping)
if err := s.MCPServerHandler.SyncAllMCPServers(context.Background()); err != nil {
logger.Warn("failed to sync MCP servers after setting client tools: %v", err)
}
}
// ExecuteChatMCPTool executes an MCP tool call and returns the result as a chat message.
func (s *BifrostHTTPServer) ExecuteChatMCPTool(ctx context.Context, toolCall *schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, *schemas.BifrostError) {
bifrostCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
return s.Client.ExecuteChatMCPTool(bifrostCtx, toolCall)
}
// ExecuteResponsesMCPTool executes an MCP tool call and returns the result as a responses message.
func (s *BifrostHTTPServer) ExecuteResponsesMCPTool(ctx context.Context, toolCall *schemas.ResponsesToolMessage) (*schemas.ResponsesMessage, *schemas.BifrostError) {
bifrostCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
return s.Client.ExecuteResponsesMCPTool(bifrostCtx, toolCall)
}
func (s *BifrostHTTPServer) GetAvailableMCPTools(ctx context.Context) []schemas.ChatTool {
bifrostCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
return s.Client.GetAvailableMCPTools(bifrostCtx)
}
// markPluginDisabled marks a plugin as disabled in the plugin status
func (s *BifrostHTTPServer) markPluginDisabled(name string) error {
return s.Config.UpdatePluginStatus(name, schemas.PluginStatusDisabled)
}
// getGovernancePluginName returns the governance plugin name from context or default
func (s *BifrostHTTPServer) getGovernancePluginName() string {
if name, ok := s.Ctx.Value(schemas.BifrostContextKeyGovernancePluginName).(string); ok && name != "" {
return name
}
return governance.PluginName
}
// getPromptsPluginName returns the prompts plugin name from context or default
func (s *BifrostHTTPServer) getPromptsPluginName() string {
if name, ok := s.Ctx.Value(schemas.BifrostContextKeyPromptsPluginName).(string); ok && name != "" {
return name
}
return prompts.PluginName
}
// getGovernancePlugin safely retrieves the governance plugin with proper locking.
// It acquires a read lock, finds the plugin, releases the lock, performs type assertion,
// and returns the BaseGovernancePlugin implementation or an error.
func (s *BifrostHTTPServer) getGovernancePlugin() (governance.BaseGovernancePlugin, error) {
// Use type-safe finder from Config
return lib.FindPluginAs[governance.BaseGovernancePlugin](s.Config, s.getGovernancePluginName())
}
// ReloadVirtualKey reloads a virtual key from the in-memory store
func (s *BifrostHTTPServer) ReloadVirtualKey(ctx context.Context, id string) (*tables.TableVirtualKey, error) {
// Load relationships for response
preloadedVk, err := s.Config.ConfigStore.RetryOnNotFound(ctx, func(ctx context.Context) (any, error) {
preloadedVk, err := s.Config.ConfigStore.GetVirtualKey(ctx, id)
if err != nil {
return nil, err
}
return preloadedVk, nil
}, lib.DBLookupMaxRetries, 2*lib.DBLookupDelay)
if err != nil {
logger.Error("failed to load virtual key: %v", err)
return nil, err
}
if preloadedVk == nil {
logger.Error("virtual key not found")
return nil, fmt.Errorf("virtual key not found")
}
// Type assertion (should never happen)
virtualKey, ok := preloadedVk.(*tables.TableVirtualKey)
if !ok {
logger.Error("virtual key type assertion failed")
return nil, fmt.Errorf("virtual key type assertion failed")
}
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return nil, err
}
governancePlugin.GetGovernanceStore().UpdateVirtualKeyInMemory(ctx, virtualKey, nil, nil, nil)
s.MCPServerHandler.SyncVKMCPServer(virtualKey)
return virtualKey, nil
}
// RemoveVirtualKey removes a virtual key from the in-memory store
func (s *BifrostHTTPServer) RemoveVirtualKey(ctx context.Context, id string) error {
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return err
}
preloadedVk, err := s.Config.ConfigStore.GetVirtualKey(ctx, id)
if err != nil {
if !errors.Is(err, configstore.ErrNotFound) {
return err
}
}
if preloadedVk == nil {
// This could be broadcast message from other server, so we will just clean up in-memory store
governancePlugin.GetGovernanceStore().DeleteVirtualKeyInMemory(ctx, id)
return nil
}
governancePlugin.GetGovernanceStore().DeleteVirtualKeyInMemory(ctx, id)
s.MCPServerHandler.DeleteVKMCPServer(preloadedVk.Value)
return nil
}
// ReloadTeam reloads a team from the in-memory store
func (s *BifrostHTTPServer) ReloadTeam(ctx context.Context, id string) (*tables.TableTeam, error) {
// Load relationships for response
preloadedTeam, err := s.Config.ConfigStore.GetTeam(ctx, id)
if err != nil {
logger.Error("failed to load relationships for created team: %v", err)
return nil, err
}
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return nil, err
}
// Add to in-memory store
governancePlugin.GetGovernanceStore().UpdateTeamInMemory(ctx, preloadedTeam, nil)
return preloadedTeam, nil
}
// RemoveTeam removes a team from the in-memory store
func (s *BifrostHTTPServer) RemoveTeam(ctx context.Context, id string) error {
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return err
}
preloadedTeam, err := s.Config.ConfigStore.GetTeam(ctx, id)
if err != nil {
if !errors.Is(err, configstore.ErrNotFound) {
return err
}
}
if preloadedTeam == nil {
// At-least deleting from in-memory store to avoid conflicts
governancePlugin.GetGovernanceStore().DeleteTeamInMemory(ctx, id)
return nil
}
governancePlugin.GetGovernanceStore().DeleteTeamInMemory(ctx, id)
return nil
}
// ReloadCustomer reloads a customer from the in-memory store
func (s *BifrostHTTPServer) ReloadCustomer(ctx context.Context, id string) (*tables.TableCustomer, error) {
preloadedCustomer, err := s.Config.ConfigStore.GetCustomer(ctx, id)
if err != nil {
return nil, err
}
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return nil, err
}
// Add to in-memory store
governancePlugin.GetGovernanceStore().UpdateCustomerInMemory(ctx, preloadedCustomer, nil)
return preloadedCustomer, nil
}
// RemoveCustomer removes a customer from the in-memory store
func (s *BifrostHTTPServer) RemoveCustomer(ctx context.Context, id string) error {
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return err
}
preloadedCustomer, err := s.Config.ConfigStore.GetCustomer(ctx, id)
if err != nil {
if !errors.Is(err, configstore.ErrNotFound) {
return err
}
}
if preloadedCustomer == nil {
// At-least deleting from in-memory store to avoid conflicts
governancePlugin.GetGovernanceStore().DeleteCustomerInMemory(ctx, id)
return nil
}
governancePlugin.GetGovernanceStore().DeleteCustomerInMemory(ctx, id)
return nil
}
// ReloadModelConfig reloads a model config from the database into in-memory store
// If usage was modified (e.g., reset due to config change), syncs it back to DB
func (s *BifrostHTTPServer) ReloadModelConfig(ctx context.Context, id string) (*tables.TableModelConfig, error) {
preloadedMC, err := s.Config.ConfigStore.GetModelConfigByID(ctx, id)
if err != nil {
logger.Error("failed to load model config: %v", err)
return nil, err
}
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return nil, err
}
// Update in memory and get back the potentially modified model config
updatedMC := governancePlugin.GetGovernanceStore().UpdateModelConfigInMemory(ctx, preloadedMC)
if updatedMC == nil {
return preloadedMC, nil
}
// Sync updated usage values back to database if they changed
if updatedMC.Budget != nil && preloadedMC.Budget != nil {
if updatedMC.Budget.CurrentUsage != preloadedMC.Budget.CurrentUsage {
if err := s.Config.ConfigStore.UpdateBudgetUsage(ctx, updatedMC.Budget.ID, updatedMC.Budget.CurrentUsage); err != nil {
logger.Error("failed to sync budget usage to database: %v", err)
}
}
}
if updatedMC.RateLimit != nil && preloadedMC.RateLimit != nil {
tokenUsageChanged := updatedMC.RateLimit.TokenCurrentUsage != preloadedMC.RateLimit.TokenCurrentUsage
requestUsageChanged := updatedMC.RateLimit.RequestCurrentUsage != preloadedMC.RateLimit.RequestCurrentUsage
if tokenUsageChanged || requestUsageChanged {
if err := s.Config.ConfigStore.UpdateRateLimitUsage(ctx, updatedMC.RateLimit.ID, updatedMC.RateLimit.TokenCurrentUsage, updatedMC.RateLimit.RequestCurrentUsage); err != nil {
logger.Error("failed to sync rate limit usage to database: %v", err)
}
}
}
return updatedMC, nil
}
// RemoveModelConfig removes a model config from the in-memory store
func (s *BifrostHTTPServer) RemoveModelConfig(ctx context.Context, id string) error {
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return err
}
governancePlugin.GetGovernanceStore().DeleteModelConfigInMemory(ctx, id)
return nil
}
func (s *BifrostHTTPServer) ReloadProvider(ctx context.Context, provider schemas.ModelProvider) (*tables.TableProvider, error) {
if s.Config == nil || s.Config.ConfigStore == nil {
return nil, fmt.Errorf("config store not found")
}
if s.Config.ModelCatalog == nil {
return nil, fmt.Errorf("pricing manager not found")
}
if s.Client == nil {
return nil, fmt.Errorf("bifrost client not found")
}
// Load provider from DB
providerInfo, err := s.Config.ConfigStore.GetProvider(ctx, provider)
if err != nil {
logger.Error("failed to load provider: %v", err)
return nil, err
}
// Initialize updatedProvider
updatedProvider := providerInfo
// Sync model level budgets in governance plugin (if governance is enabled)
if s.Config.IsPluginLoaded(s.getGovernancePluginName()) {
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
logger.Warn("governance plugin found but failed to get: %v", err)
} else {
// Update in memory and get back the potentially modified provider
govUpdated := governancePlugin.GetGovernanceStore().UpdateProviderInMemory(ctx, providerInfo)
if govUpdated != nil {
updatedProvider = govUpdated
}
// Sync updated usage values back to database if they changed
if updatedProvider.Budget != nil && providerInfo.Budget != nil {
if updatedProvider.Budget.CurrentUsage != providerInfo.Budget.CurrentUsage {
if err := s.Config.ConfigStore.UpdateBudgetUsage(ctx, updatedProvider.Budget.ID, updatedProvider.Budget.CurrentUsage); err != nil {
logger.Error("failed to sync budget usage to database: %v", err)
}
}
}
if updatedProvider.RateLimit != nil && providerInfo.RateLimit != nil {
tokenUsageChanged := updatedProvider.RateLimit.TokenCurrentUsage != providerInfo.RateLimit.TokenCurrentUsage
requestUsageChanged := updatedProvider.RateLimit.RequestCurrentUsage != providerInfo.RateLimit.RequestCurrentUsage
if tokenUsageChanged || requestUsageChanged {
if err := s.Config.ConfigStore.UpdateRateLimitUsage(ctx, updatedProvider.RateLimit.ID, updatedProvider.RateLimit.TokenCurrentUsage, updatedProvider.RateLimit.RequestCurrentUsage); err != nil {
logger.Error("failed to sync rate limit usage to database: %v", err)
}
}
}
}
}
// Read current key count from in-memory store (providerInfo.Keys is not preloaded from DB)
inMemoryKeys, _ := s.Config.GetProviderKeysRaw(provider)
isKeylessProvider := providerInfo.CustomProviderConfig != nil && providerInfo.CustomProviderConfig.IsKeyLess
hasNoKeys := len(inMemoryKeys) == 0 && !isKeylessProvider
// Getting allowed models from all provider keys (needed before model listing)
providerKeys, err := s.Config.ConfigStore.GetKeysByProvider(ctx, string(provider))
if err != nil {
return nil, fmt.Errorf("failed to update provider model catalog: failed to get keys by provider: %s", err)
}
bfCtx := schemas.NewBifrostContext(ctx, time.Now().Add(15*time.Second))
bfCtx.SetValue(schemas.BifrostContextKeySkipPluginPipeline, true)
bfCtx.SetValue(schemas.BifrostContextKeyValidateKeys, true) // Validate keys during provider add/update
defer bfCtx.Cancel()
// Run filtered and unfiltered model listing concurrently
var (
allModels *schemas.BifrostListModelsResponse
bifrostErr *schemas.BifrostError
unfilteredModels *schemas.BifrostListModelsResponse
listModelsErr *schemas.BifrostError
listWg sync.WaitGroup
)
listWg.Add(2)
go func() {
defer listWg.Done()
allModels, bifrostErr = s.Client.ListModelsRequest(bfCtx, &schemas.BifrostListModelsRequest{
Provider: provider,
})
}()
go func() {
defer listWg.Done()
unfilteredModels, listModelsErr = s.Client.ListModelsRequest(bfCtx, &schemas.BifrostListModelsRequest{
Provider: provider,
Unfiltered: true,
})
}()
listWg.Wait()
if allModels != nil && len(allModels.KeyStatuses) > 0 && s.Config.ConfigStore != nil {
s.updateKeyStatus(ctx, allModels.KeyStatuses)
}
if bifrostErr != nil {
if len(bifrostErr.ExtraFields.KeyStatuses) > 0 && s.Config.ConfigStore != nil {
s.updateKeyStatus(ctx, bifrostErr.ExtraFields.KeyStatuses)
}
if hasNoKeys {
logger.Warn("model discovery skipped for provider %s: no keys configured", provider)
} else {
logger.Warn("failed to update provider model catalog: failed to list all models: %s. We are falling back onto the static datasheet", bifrost.GetErrorMessage(bifrostErr))
}
// In case of error, we return an empty list of models, and fallback onto the static datasheet
allModels = &schemas.BifrostListModelsResponse{
Data: make([]schemas.Model, 0),
}
}
modelsInKeys := make([]schemas.Model, 0)
for _, key := range providerKeys {
if key.Models.IsUnrestricted() {
continue
}
for _, model := range key.Models {
modelsInKeys = append(modelsInKeys, schemas.Model{
ID: string(provider) + "/" + model,
})
}
}
s.Config.ModelCatalog.UpsertModelDataForProvider(provider, allModels, modelsInKeys)
if listModelsErr != nil {
if hasNoKeys {
logger.Warn("unfiltered model discovery skipped for provider %s: no keys configured", provider)
} else {
logger.Error("failed to list unfiltered models for provider %s: %v: falling back onto the static datasheet", provider, bifrost.GetErrorMessage(listModelsErr))
}
} else {
s.Config.ModelCatalog.UpsertUnfilteredModelDataForProvider(provider, unfilteredModels)
}
return updatedProvider, nil
}
// RemoveProvider removes a provider from the in-memory store
func (s *BifrostHTTPServer) RemoveProvider(ctx context.Context, provider schemas.ModelProvider) error {
err := s.Client.RemoveProvider(provider)
if err != nil && !strings.Contains(err.Error(), "not found") {
logger.Error("failed to remove provider from client: %v", err)
return err
}
err = s.Config.RemoveProvider(ctx, provider)
if err != nil && !errors.Is(err, lib.ErrNotFound) {
logger.Error("failed to remove provider from config: %v. Client and config may be out of sync, please restart bifrost", err)
return fmt.Errorf("failed to remove provider from config: %w. Client and config may be out of sync, please restart bifrost", err)
}
governancePlugin, err := s.getGovernancePlugin()
if err != nil {
return err
}
governancePlugin.GetGovernanceStore().DeleteProviderInMemory(ctx, string(provider))
if s.Config == nil || s.Config.ModelCatalog == nil {
return fmt.Errorf("pricing manager not found")
}
s.Config.ModelCatalog.DeleteModelDataForProvider(provider)
return nil
}
// GetGovernanceData returns the governance data
func (s *BifrostHTTPServer) GetGovernanceData(ctx context.Context) *governance.GovernanceData {
// Use type-safe finder from Config
governancePlugin, err := lib.FindPluginAs[governance.BaseGovernancePlugin](s.Config, s.getGovernancePluginName())
if err != nil {
return nil
}
return governancePlugin.GetGovernanceStore().GetGovernanceData(ctx)
}
// ReloadRoutingRule reloads a routing rule from the database into the governance store
func (s *BifrostHTTPServer) ReloadRoutingRule(ctx context.Context, id string) error {
governancePluginName := governance.PluginName
if name, ok := s.Ctx.Value(schemas.BifrostContextKeyGovernancePluginName).(string); ok && name != "" {
governancePluginName = name
}
governancePlugin, err := lib.FindPluginAs[governance.BaseGovernancePlugin](s.Config, governancePluginName)
if err != nil {
return fmt.Errorf("governance plugin not found: %w", err)
}
// Get the governance store from the plugin
store := governancePlugin.GetGovernanceStore()
rule, err := s.Config.ConfigStore.GetRoutingRule(ctx, id)
if err != nil {
return fmt.Errorf("failed to get routing rule from config store: %w", err)
}
// Update the rule in the store (this updates the in-memory cache)
if err := store.UpdateRoutingRuleInMemory(ctx, rule); err != nil {
return fmt.Errorf("failed to update routing rule in store: %w", err)
}
return nil
}
// RemoveRoutingRule removes a routing rule from the governance store
func (s *BifrostHTTPServer) RemoveRoutingRule(ctx context.Context, id string) error {
governancePluginName := governance.PluginName
if name, ok := s.Ctx.Value(schemas.BifrostContextKeyGovernancePluginName).(string); ok && name != "" {
governancePluginName = name
}
governancePlugin, err := lib.FindPluginAs[governance.BaseGovernancePlugin](s.Config, governancePluginName)
if err != nil {
return fmt.Errorf("governance plugin not found: %w", err)
}
// Get the governance store from the plugin
store := governancePlugin.GetGovernanceStore()
// Delete the rule from the store (this removes from in-memory cache)
if err := store.DeleteRoutingRuleInMemory(ctx, id); err != nil {
return fmt.Errorf("failed to delete routing rule from store: %w", err)
}
return nil
}
// ReloadClientConfigFromConfigStore reloads the client config from config store
func (s *BifrostHTTPServer) ReloadClientConfigFromConfigStore(ctx context.Context) error {
if s.Config == nil || s.Config.ConfigStore == nil {
return fmt.Errorf("config store not found")
}
config, err := s.Config.ConfigStore.GetClientConfig(context.Background())
if err != nil {
return fmt.Errorf("failed to get client config: %v", err)
}
if config == nil {
return fmt.Errorf("client config not found")
}
*s.Config.ClientConfig = *config
// Reloading whitelisted routes from the client config
if s.AuthMiddleware != nil {
s.AuthMiddleware.UpdateWhitelistedRoutes(config.WhitelistedRoutes)
}
// Reloading config in bifrost client
if s.Client != nil {
account := lib.NewBaseAccount(s.Config)
var mcpConfig *schemas.MCPConfig
if s.Config.MCPConfig != nil {
mcpConfig = s.Config.MCPConfig
}
s.Client.ReloadConfig(schemas.BifrostConfig{
Account: account,
InitialPoolSize: s.Config.ClientConfig.InitialPoolSize,
DropExcessRequests: s.Config.ClientConfig.DropExcessRequests,
LLMPlugins: s.Config.GetLoadedLLMPlugins(),
MCPPlugins: s.Config.GetLoadedMCPPlugins(),
MCPConfig: mcpConfig,
Logger: logger,
})
}
return nil
}
// UpdateAuthConfig updates auth config in the config store and updates the AuthMiddleware's in-memory config
func (s *BifrostHTTPServer) UpdateAuthConfig(ctx context.Context, authConfig *configstore.AuthConfig) error {
if authConfig == nil {
return fmt.Errorf("auth config is nil")
}
if s.Config == nil || s.Config.ConfigStore == nil {
return fmt.Errorf("config store not found")
}
// Allow disabling auth without credentials, but require them when enabling
if authConfig.IsEnabled && (authConfig.AdminUserName == nil || authConfig.AdminUserName.GetValue() == "" || authConfig.AdminPassword == nil || authConfig.AdminPassword.GetValue() == "") {
return fmt.Errorf("username and password are required when auth is enabled")
}
// Update the config store
if err := s.Config.ConfigStore.UpdateAuthConfig(ctx, authConfig); err != nil {
return err
}
// Update the AuthMiddleware's in-memory config
if s.AuthMiddleware != nil {
// Fetch the updated config from the store to ensure we have the latest
updatedAuthConfig, err := s.Config.ConfigStore.GetAuthConfig(ctx)
if err != nil {
logger.Warn("failed to get auth config from store after update: %v", err)
// Still update with what we have
s.AuthMiddleware.UpdateAuthConfig(authConfig)
} else {
s.AuthMiddleware.UpdateAuthConfig(updatedAuthConfig)
}
}
return nil
}
// UpdateDropExcessRequests updates excess requests config
func (s *BifrostHTTPServer) UpdateDropExcessRequests(ctx context.Context, value bool) {
if s.Config == nil {
return
}
s.Client.UpdateDropExcessRequests(value)
}
// UpdateMCPToolManagerConfig updates the MCP tool manager config.
// Always pass the current disableAutoToolInject value so it is never reset.
func (s *BifrostHTTPServer) UpdateMCPToolManagerConfig(ctx context.Context, maxAgentDepth int, toolExecutionTimeoutInSeconds int, codeModeBindingLevel string, disableAutoToolInject bool) error {
if s.Config == nil {
return fmt.Errorf("config not found")
}
return s.Client.UpdateToolManagerConfig(maxAgentDepth, toolExecutionTimeoutInSeconds, codeModeBindingLevel, disableAutoToolInject)
}
// reloadObservabilityPlugins reloads all observability plugins in the tracing middleware
func (s *BifrostHTTPServer) reloadObservabilityPlugins() {
observabilityPlugins := s.CollectObservabilityPlugins()
// Always update the tracing middleware, even with empty slice, to clear stale plugins
s.TracingMiddleware.SetObservabilityPlugins(observabilityPlugins)
}
// ReloadPricingManager reloads the pricing manager
func (s *BifrostHTTPServer) UpdateSyncConfig(ctx context.Context) error {
if s.Config == nil || s.Config.ModelCatalog == nil {
return fmt.Errorf("pricing manager not found")
}
if s.Config.FrameworkConfig == nil || s.Config.FrameworkConfig.Pricing == nil {
return fmt.Errorf("framework config not found")
}
return s.Config.ModelCatalog.UpdateSyncConfig(ctx, s.Config.FrameworkConfig.Pricing)
}
func (s *BifrostHTTPServer) populateModelPoolWithListModels(ctx context.Context) error {
// Fetching keys for all providers and allowed models first
// Based on allowed models we will set the data in the model catalog
var wg sync.WaitGroup
for provider, providerConfig := range s.Config.Providers {
wg.Add(1)
go func(provider schemas.ModelProvider, providerConfig configstore.ProviderConfig) {
defer wg.Done()
bfCtx := schemas.NewBifrostContext(ctx, time.Now().Add(15*time.Second))
bfCtx.SetValue(schemas.BifrostContextKeySkipPluginPipeline, true)
defer bfCtx.Cancel()
modelData, listModelsErr := s.Client.ListModelsRequest(bfCtx, &schemas.BifrostListModelsRequest{
Provider: provider,
})
if listModelsErr != nil {
logger.Error("failed to list models for provider %s: %v: falling back onto the static datasheet", provider, bifrost.GetErrorMessage(listModelsErr))
}
allowedModels := make([]schemas.Model, 0)
for _, key := range providerConfig.Keys {
if key.Models.IsUnrestricted() {
continue
}
for _, model := range key.Models {
allowedModels = append(allowedModels, schemas.Model{
ID: string(provider) + "/" + model,
})
}
}
s.Config.ModelCatalog.UpsertModelDataForProvider(provider, modelData, allowedModels)
unfilteredModelData, listModelsErr := s.Client.ListModelsRequest(bfCtx, &schemas.BifrostListModelsRequest{
Provider: provider,
Unfiltered: true,
})
if listModelsErr != nil {
logger.Error("failed to list unfiltered models for provider %s: %v: falling back onto the static datasheet", provider, bifrost.GetErrorMessage(listModelsErr))
} else {
s.Config.ModelCatalog.UpsertUnfilteredModelDataForProvider(provider, unfilteredModelData)
}
}(provider, providerConfig)
}
wg.Wait()
return nil
}
// ForceReloadPricing triggers an immediate pricing sync and resets the sync timer
func (s *BifrostHTTPServer) ForceReloadPricing(ctx context.Context) error {
if s.Config == nil {
return fmt.Errorf("server config not initialized")
}
if s.Config.ModelCatalog != nil {
if err := s.Config.ModelCatalog.ForceReloadPricing(ctx); err != nil {
return fmt.Errorf("failed to force reload pricing: %w", err)
}
return s.populateModelPoolWithListModels(ctx)
}
return nil
}
// ReloadPricingFromDBAndPopulateModelPool reloads the pricing from DB and populates the model pool
func (s *BifrostHTTPServer) ReloadPricingFromDBAndPopulateModelPool(ctx context.Context) error {
if s.Config == nil {
return fmt.Errorf("server config not initialized")
}
if s.Config.ModelCatalog != nil {
if err := s.Config.ModelCatalog.ReloadFromDB(ctx); err != nil {
return fmt.Errorf("failed to reload pricing from DB: %w", err)
}
return s.populateModelPoolWithListModels(ctx)
}
return nil
}
// UpsertPricingOverride inserts or updates a pricing override in the in-memory model catalog.
func (s *BifrostHTTPServer) UpsertPricingOverride(ctx context.Context, override *tables.TablePricingOverride) error {
if s.Config == nil || s.Config.ModelCatalog == nil {
return fmt.Errorf("pricing manager not found")
}
return s.Config.ModelCatalog.UpsertPricingOverrides(override)
}
// DeletePricingOverride removes a pricing override from the in-memory model catalog.
func (s *BifrostHTTPServer) DeletePricingOverride(ctx context.Context, id string) error {
if s.Config == nil || s.Config.ModelCatalog == nil {
return fmt.Errorf("pricing manager not found")
}
s.Config.ModelCatalog.DeletePricingOverride(id)
return nil
}
// ReloadProxyConfig reloads the proxy configuration
func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *tables.GlobalProxyConfig) error {
if s.Config == nil {
return fmt.Errorf("config not found")
}
// Store the proxy config in memory for use by components that need it
s.Config.ProxyConfig = config
logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
return nil
}
// ReloadHeaderFilterConfig reloads the header filter configuration
func (s *BifrostHTTPServer) ReloadHeaderFilterConfig(ctx context.Context, config *tables.GlobalHeaderFilterConfig) error {
if s.Config == nil {
return fmt.Errorf("config not found")
}
// Store the raw header filter config in ClientConfig
s.Config.ClientConfig.HeaderFilterConfig = config
// Compile into optimized matcher for O(1) per-request lookups
s.Config.SetHeaderMatcher(lib.NewHeaderMatcher(config))
allowlistLen := 0
denylistLen := 0
if config != nil {
allowlistLen = len(config.Allowlist)
denylistLen = len(config.Denylist)
}
logger.Info("header filter configuration reloaded: allowlist=%d, denylist=%d", allowlistLen, denylistLen)
return nil
}
// GetModelsForProvider returns all models for a specific provider from the model catalog
func (s *BifrostHTTPServer) GetModelsForProvider(provider schemas.ModelProvider) []string {
if s.Config == nil || s.Config.ModelCatalog == nil {
return []string{}
}
return s.Config.ModelCatalog.GetModelsForProvider(provider)
}
// GetUnfilteredModelsForProvider returns all unfiltered models for a specific provider from the model catalog
func (s *BifrostHTTPServer) GetUnfilteredModelsForProvider(provider schemas.ModelProvider) []string {
if s.Config == nil || s.Config.ModelCatalog == nil {
return []string{}
}
return s.Config.ModelCatalog.GetUnfilteredModelsForProvider(provider)
}
// GetPluginStatus returns the status of all plugins
// Delegates to Config for centralized plugin status management
func (s *BifrostHTTPServer) GetPluginStatus(ctx context.Context) map[string]schemas.PluginStatus {
return s.Config.GetPluginStatus()
}
// Helper to update error status
// Uses UpdatePluginOverallStatus to create the status entry if it doesn't exist,
// ensuring plugins that were never loaded can still have their error status tracked.
// Always returns the original error so the actual failure reason is surfaced to the user.
func (s *BifrostHTTPServer) updatePluginErrorStatus(name, step string, originalErr error) error {
logs := []string{fmt.Sprintf("error %s plugin %s: %v", step, name, originalErr)}
s.Config.UpdatePluginOverallStatus(name, name, schemas.PluginStatusError, logs, []schemas.PluginType{})
return originalErr
}
// SyncLoadedPlugin syncs a loaded plugin to the Bifrost client and updates the plugin status
func (s *BifrostHTTPServer) SyncLoadedPlugin(ctx context.Context, name string, plugin schemas.BasePlugin, placement *schemas.PluginPlacement, order *int) error {
// 2. Register (replaces old version atomically)
if err := s.Config.ReloadPlugin(plugin); err != nil {
return s.updatePluginErrorStatus(plugin.GetName(), "registering", err)
}
// 2b. Set order info and re-sort
s.Config.SetPluginOrderInfo(plugin.GetName(), placement, order)
s.Config.SortAndRebuildPlugins()
// 3. Update Bifrost client
if err := s.Client.ReloadPlugin(plugin, InferPluginTypes(plugin)); err != nil {
return s.updatePluginErrorStatus(plugin.GetName(), "reloading bifrost config for", err)
}
// 3b. Sync plugin execution order from config to core
s.Client.ReorderPlugins(s.Config.GetPluginOrder())
// 4. Special handling for observability plugins
if _, ok := plugin.(schemas.ObservabilityPlugin); ok {
s.reloadObservabilityPlugins()
}
// 5. Update plugin status
s.Config.UpdatePluginOverallStatus(plugin.GetName(), name, schemas.PluginStatusActive,
[]string{fmt.Sprintf("plugin %s reloaded successfully", name)}, InferPluginTypes(plugin))
return nil
}
// ReloadPlugin reloads a plugin with new instance and updates Bifrost core.
// The plugin is checked for LLM and MCP interfaces independently and registered
// to the appropriate arrays based on which interfaces it implements.
func (s *BifrostHTTPServer) ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any, placement *schemas.PluginPlacement, order *int) error {
logger.Debug("reloading plugin %s", name)
// 1. Instantiate new version
plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
if err != nil {
return s.updatePluginErrorStatus(name, "loading", err)
}
return s.SyncLoadedPlugin(ctx, name, plugin, placement, order)
}
// RemovePlugin removes a plugin from the server.
// The plugin is removed from both LLM and MCP arrays independently if it exists in them.
func (s *BifrostHTTPServer) RemovePlugin(ctx context.Context, displayName string) error {
// Get the actual plugin name from the display name
name, ok := s.Config.GetPluginNameByDisplayName(displayName)
if !ok {
return dynamicPlugins.ErrPluginNotFound
}
// Check if plugin implements ObservabilityPlugin before removal
var isObservability bool
var err error
var plugin schemas.BasePlugin
if plugin, err = s.Config.FindPluginByName(name); err == nil {
_, isObservability = plugin.(schemas.ObservabilityPlugin)
}
// 1. Unregister from config
if err := s.Config.UnregisterPlugin(name); err != nil {
return err
}
// 2. Update Bifrost client
if err := s.Client.RemovePlugin(name, InferPluginTypes(plugin)); err != nil {
logger.Warn("failed to reload bifrost config after plugin removal: %v", err)
}
// 3. Reload observability plugins if necessary
if isObservability {
s.reloadObservabilityPlugins()
}
// 4. Update status
if isDisabled, _ := ctx.Value(handlers.PluginDisabledKey).(bool); isDisabled {
s.markPluginDisabled(name)
} else {
s.Config.DeletePluginOverallStatus(name)
}
return nil
}
// RegisterInferenceRoutes initializes the routes for the inference handler
func (s *BifrostHTTPServer) RegisterInferenceRoutes(ctx context.Context, middlewares ...schemas.BifrostHTTPMiddleware) error {
// Initialize WebSocket pool and handler before integrations so it can be wired through
s.wsPool = bfws.NewPool(s.Config.WebSocketConfig.Pool)
wsResponsesHandler := handlers.NewWSResponsesHandler(s.Client, s.Config, s.wsPool)
wsRealtimeHandler := handlers.NewWSRealtimeHandler(s.Client, s.Config, s.wsPool)
webrtcRealtimeHandler := handlers.NewWebRTCRealtimeHandler(s.Client, s.Config)
realtimeClientSecretsHandler := handlers.NewRealtimeClientSecretsHandler(s.Client, s.Config)
inferenceHandler := handlers.NewInferenceHandler(s.Client, s.Config)
s.IntegrationHandler = handlers.NewIntegrationHandler(s.Client, s.Config, wsResponsesHandler, wsRealtimeHandler, webrtcRealtimeHandler, realtimeClientSecretsHandler)
mcpInferenceHandler := handlers.NewMCPInferenceHandler(s.Client, s.Config)
mcpServerHandler, err := handlers.NewMCPServerHandler(ctx, s.Config, s)
if err != nil {
return fmt.Errorf("failed to initialize mcp server handler: %v", err)
}
s.MCPServerHandler = mcpServerHandler
asyncHandler := handlers.NewAsyncHandler(s.Client, s.Config)
s.IntegrationHandler.RegisterRoutes(s.Router, middlewares...)
inferenceHandler.RegisterRoutes(s.Router, middlewares...)
asyncHandler.RegisterRoutes(s.Router, middlewares...)
mcpInferenceHandler.RegisterRoutes(s.Router, middlewares...)
s.MCPServerHandler.RegisterRoutes(s.Router, middlewares...)
return nil
}
// RegisterAPIRoutes initializes the routes for the Bifrost HTTP server.
func (s *BifrostHTTPServer) RegisterAPIRoutes(ctx context.Context, callbacks ServerCallbacks, middlewares ...schemas.BifrostHTTPMiddleware) error {
var err error
// Initializing plugin specific handlers
var loggingHandler *handlers.LoggingHandler
loggerPlugin, _ := lib.FindPluginAs[*logging.LoggerPlugin](s.Config, logging.PluginName)
if loggerPlugin != nil {
loggingHandler = handlers.NewLoggingHandler(loggerPlugin.GetPluginLogManager(), s, s.Config)
}
var governanceHandler *handlers.GovernanceHandler
governancePluginName := governance.PluginName
if name, ok := ctx.Value(schemas.BifrostContextKeyGovernancePluginName).(string); ok && name != "" {
governancePluginName = name
}
governancePlugin, _ := lib.FindPluginAs[schemas.LLMPlugin](s.Config, governancePluginName)
if governancePlugin != nil {
governanceHandler, err = handlers.NewGovernanceHandler(callbacks, s.Config.ConfigStore)
if err != nil {
return fmt.Errorf("failed to initialize governance handler: %v", err)
}
}
var cacheHandler *handlers.CacheHandler
semanticCachePlugin, _ := lib.FindPluginAs[*semanticcache.Plugin](s.Config, semanticcache.PluginName)
if semanticCachePlugin != nil {
cacheHandler = handlers.NewCacheHandler(semanticCachePlugin)
}
var promptsReloader handlers.PromptCacheReloader
if promptsPlugin, err := lib.FindPluginAs[handlers.PromptCacheReloader](s.Config, s.getPromptsPluginName()); err == nil && promptsPlugin != nil {
promptsReloader = promptsPlugin
}
// Websocket handler needs to go below UI handler
logger.Debug("initializing websocket server")
if s.WebSocketHandler == nil {
s.WebSocketHandler = handlers.NewWebSocketHandler(s.Ctx, s.Config.ClientConfig.AllowedOrigins)
}
// Start WebSocket heartbeat
s.WebSocketHandler.StartHeartbeat()
// Adding telemetry middleware
// Chaining all middlewares
// lib.ChainMiddlewares chains multiple middlewares together
healthHandler := handlers.NewHealthHandler(s.Config)
providerHandler := handlers.NewProviderHandler(callbacks, s.Config, s.Client)
oauthHandler := handlers.NewOAuthHandler(s.Config.OAuthProvider, s.Client, s.Config)
mcpHandler := handlers.NewMCPHandler(callbacks, callbacks, s.Client, s.Config, oauthHandler)
configHandler := handlers.NewConfigHandler(callbacks, s.Config)
pluginsHandler := handlers.NewPluginsHandler(callbacks, s.Config.ConfigStore)
sessionHandler := handlers.NewSessionHandler(s.Config.ConfigStore, s.WSTicketStore)
promptsHandler := handlers.NewPromptsHandler(s.Config.ConfigStore, promptsReloader)
// Going ahead with API handlers
healthHandler.RegisterRoutes(s.Router, middlewares...)
providerHandler.RegisterRoutes(s.Router, middlewares...)
mcpHandler.RegisterRoutes(s.Router, middlewares...)
configHandler.RegisterRoutes(s.Router, middlewares...)
oauthHandler.RegisterRoutes(s.Router, middlewares...)
// OAuth metadata + per-user OAuth endpoints (no auth middleware — must be publicly accessible)
oauthMetadataHandler := handlers.NewOAuthMetadataHandler(s.Config)
oauthMetadataHandler.RegisterRoutes(s.Router)
perUserOAuthHandler := handlers.NewPerUserOAuthHandler(s.Config)
perUserOAuthHandler.RegisterRoutes(s.Router)
consentHandler := handlers.NewConsentHandler(s.Config)
consentHandler.RegisterRoutes(s.Router)
if pluginsHandler != nil {
pluginsHandler.RegisterRoutes(s.Router, middlewares...)
}
if sessionHandler != nil {
sessionHandler.RegisterRoutes(s.Router, middlewares...)
}
if promptsHandler != nil {
promptsHandler.RegisterRoutes(s.Router, middlewares...)
}
if cacheHandler != nil {
cacheHandler.RegisterRoutes(s.Router, middlewares...)
}
if governanceHandler != nil {
governanceHandler.RegisterRoutes(s.Router, middlewares...)
}
if loggingHandler != nil {
loggingHandler.RegisterRoutes(s.Router, middlewares...)
}
if s.WebSocketHandler != nil {
s.WebSocketHandler.RegisterRoutes(s.Router, middlewares...)
}
// Register dev pprof handler only in dev mode
if handlers.IsDevMode() {
logger.Info("dev mode enabled, registering pprof endpoints")
s.devPprofHandler = handlers.NewDevPprofHandler()
s.devPprofHandler.RegisterRoutes(s.Router, middlewares...)
}
// Add Prometheus /metrics endpoint
prometheusPlugin, err := lib.FindPluginAs[*telemetry.PrometheusPlugin](s.Config, telemetry.PluginName)
if err == nil && prometheusPlugin.GetRegistry() != nil {
// Use the plugin's dedicated registry if available
metricsHandler := fasthttpadaptor.NewFastHTTPHandler(promhttp.HandlerFor(prometheusPlugin.GetRegistry(), promhttp.HandlerOpts{}))
s.Router.GET("/metrics", lib.ChainMiddlewares(metricsHandler, middlewares...))
} else {
logger.Warn("prometheus plugin not found or registry is nil, skipping metrics endpoint")
}
// 404 handler
s.Router.NotFound = func(ctx *fasthttp.RequestCtx) {
handlers.SendError(ctx, fasthttp.StatusNotFound, "Route not found: "+string(ctx.Path()))
}
return nil
}
// RegisterUIRoutes registers the UI handler with the specified router
func (s *BifrostHTTPServer) RegisterUIRoutes(middlewares ...schemas.BifrostHTTPMiddleware) {
// WARNING: This UI handler needs to be registered after all the other handlers
handlers.NewUIHandler(s.UIContent).RegisterRoutes(s.Router, middlewares...)
}
// GetAllRedactedKeys gets all redacted keys from the config store
func (s *BifrostHTTPServer) GetAllRedactedKeys(ctx context.Context, ids []string) []schemas.Key {
if s.Config == nil || s.Config.ConfigStore == nil {
return nil
}
redactedKeys, err := s.Config.ConfigStore.GetAllRedactedKeys(ctx, ids)
if err != nil {
logger.Error("failed to get all redacted keys: %v", err)
return nil
}
return redactedKeys
}
// GetAllRedactedVirtualKeys gets all redacted virtual keys from the config store
func (s *BifrostHTTPServer) GetAllRedactedVirtualKeys(ctx context.Context, ids []string) []tables.TableVirtualKey {
if s.Config == nil || s.Config.ConfigStore == nil {
return nil
}
virtualKeys, err := s.Config.ConfigStore.GetRedactedVirtualKeys(ctx, ids)
if err != nil {
logger.Error("failed to get all redacted virtual keys: %v", err)
return nil
}
return virtualKeys
}
// GetAllRedactedRoutingRules gets all redacted routing rules from the config store
func (s *BifrostHTTPServer) GetAllRedactedRoutingRules(ctx context.Context, ids []string) []tables.TableRoutingRule {
if s.Config == nil || s.Config.ConfigStore == nil {
return nil
}
routingRules, err := s.Config.ConfigStore.GetRedactedRoutingRules(ctx, ids)
if err != nil {
logger.Error("failed to get all redacted routing rules: %v", err)
return nil
}
return routingRules
}
// PrepareCommonMiddlewares gets the common middlewares for the Bifrost HTTP server
func (s *BifrostHTTPServer) PrepareCommonMiddlewares() []schemas.BifrostHTTPMiddleware {
commonMiddlewares := []schemas.BifrostHTTPMiddleware{}
// Preparing middlewares
// Initializing prometheus plugin
prometheusPlugin, err := lib.FindPluginAs[*telemetry.PrometheusPlugin](s.Config, telemetry.PluginName)
if err == nil {
commonMiddlewares = append(commonMiddlewares, prometheusPlugin.HTTPMiddleware)
} else {
logger.Warn("prometheus plugin not found, skipping telemetry middleware")
}
return commonMiddlewares
}
// Bootstrap initializes the Bifrost HTTP server with all necessary components.
// It:
// 1. Initializes Prometheus collectors for monitoring
// 2. Reads and parses configuration from the specified config file
// 3. Initializes the Bifrost client with the configuration
// 4. Sets up HTTP routes for text and chat completions
//
// The server exposes the following endpoints:
// - POST /v1/text/completions: For text completion requests
// - POST /v1/chat/completions: For chat completion requests
// - GET /metrics: For Prometheus metrics
func (s *BifrostHTTPServer) Bootstrap(ctx context.Context) error {
var err error
s.Ctx, s.cancel = schemas.NewBifrostContextWithCancel(ctx)
handlers.SetVersion(s.Version)
configDir := GetDefaultConfigDir(s.AppDir)
// Ensure app directory exists
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("failed to create app directory %s: %v", configDir, err)
}
// Initialize high-performance configuration store with dedicated database
s.Config, err = lib.LoadConfig(ctx, configDir)
if err != nil {
return fmt.Errorf("failed to load config %v", err)
}
if s.Config.KVStore != nil {
integrations.RegisterKVDecoders(s.Config.KVStore)
}
// Initialize WebSocket handler early so plugins can wire event broadcasters during Init.
// Log callbacks are registered later in RegisterAPIRoutes when logging plugin is available.
s.WebSocketHandler = handlers.NewWebSocketHandler(s.Ctx, s.Config.ClientConfig.AllowedOrigins)
s.Config.EventBroadcaster = s.WebSocketHandler.BroadcastEvent
// Initializing plugin loader
s.Config.PluginLoader = &dynamicPlugins.SharedObjectPluginLoader{}
// Initialize log retention cleaner if log store is configured
if s.Config.LogsStore != nil {
// If log retention days remains 0, then we wont be initializing the log retention cleaner
logRetentionDays := 0
if s.Config.ConfigStore != nil {
// Get logs store config from config store
clientConfig, err := s.Config.ConfigStore.GetClientConfig(ctx)
if err != nil {
logger.Warn("failed to get logs store config: %v", err)
// So we wont be initializing the log retention cleaner
}
if clientConfig != nil {
logRetentionDays = clientConfig.LogRetentionDays
}
} else {
// We will check if the config file has the log retention days set
logRetentionDays = s.Config.ClientConfig.LogRetentionDays
}
logger.Info("log retention days: %d", logRetentionDays)
if logRetentionDays > 0 {
// Type assert to get RDBLogStore (which implements LogRetentionManager)
if rdbStore, ok := s.Config.LogsStore.(logstore.LogRetentionManager); ok {
cleanerConfig := logstore.CleanerConfig{
RetentionDays: logRetentionDays,
}
s.LogsCleaner = logstore.NewLogsCleaner(rdbStore, cleanerConfig, logger)
s.LogsCleaner.StartCleanupRoutine()
logger.Info("log retention cleaner initialized with %d days retention",
logRetentionDays)
}
}
}
// Initialize async job cleaner if log store is configured
if s.Config.LogsStore != nil {
s.AsyncJobCleaner = logstore.NewAsyncJobCleaner(s.Config.LogsStore, logger)
s.AsyncJobCleaner.StartCleanupRoutine()
}
// Load all plugins
if err := s.LoadPlugins(ctx); err != nil {
return fmt.Errorf("failed to instantiate plugins: %v", err)
}
// Initialize async job executor (requires LogsStore + governance plugin)
if s.Config.LogsStore != nil {
governancePlugin, govErr := lib.FindPluginAs[governance.BaseGovernancePlugin](s.Config, s.getGovernancePluginName())
if govErr == nil {
s.Config.AsyncJobExecutor = logstore.NewAsyncJobExecutor(s.Config.LogsStore, governancePlugin.GetGovernanceStore(), logger)
logger.Info("async job executor initialized")
}
}
tableMCPConfig := s.Config.MCPConfig
var mcpConfig *schemas.MCPConfig
if tableMCPConfig != nil {
mcpConfig = s.Config.MCPConfig
if mcpConfig != nil {
mcpConfig.FetchNewRequestIDFunc = func(ctx *schemas.BifrostContext) string {
return uuid.New().String()
}
}
}
// Initialize bifrost client
// Create account backed by the high-performance store (all processing is done in LoadFromDatabase)
// The account interface now benefits from ultra-fast config access times via in-memory storage
account := lib.NewBaseAccount(s.Config)
s.Client, err = bifrost.Init(ctx, schemas.BifrostConfig{
Account: account,
InitialPoolSize: s.Config.ClientConfig.InitialPoolSize,
DropExcessRequests: s.Config.ClientConfig.DropExcessRequests,
LLMPlugins: s.Config.GetLoadedLLMPlugins(),
MCPPlugins: s.Config.GetLoadedMCPPlugins(),
MCPConfig: mcpConfig,
OAuth2Provider: s.Config.OAuthProvider,
Logger: logger,
KVStore: s.Config.KVStore,
})
if err != nil {
return fmt.Errorf("failed to initialize bifrost: %v", err)
}
logger.Info("bifrost client initialized")
// Sync plugin execution order from config to core (defensive — Init receives sorted list,
// but this ensures order consistency if the loading path changes in the future)
s.Client.ReorderPlugins(s.Config.GetPluginOrder())
// List all models and add to model catalog with per-provider status tracking
logger.Info("listing all models and adding to model catalog")
if s.Config.ModelCatalog != nil {
// Fetching keys for all providers and allowed models first
// Based on allowed models we will set the data in the model catalog
var wg sync.WaitGroup
for provider, providerConfig := range s.Config.Providers {
wg.Add(1)
go func(provider schemas.ModelProvider, providerConfig configstore.ProviderConfig) {
defer wg.Done()
bfCtx := schemas.NewBifrostContext(ctx, time.Now().Add(15*time.Second))
bfCtx.SetValue(schemas.BifrostContextKeySkipPluginPipeline, true)
defer bfCtx.Cancel()
modelData, listModelsErr := s.Client.ListModelsRequest(bfCtx, &schemas.BifrostListModelsRequest{
Provider: provider,
})
if modelData != nil && len(modelData.KeyStatuses) > 0 && s.Config.ConfigStore != nil {
s.updateKeyStatus(ctx, modelData.KeyStatuses)
}
if listModelsErr != nil {
if len(listModelsErr.ExtraFields.KeyStatuses) > 0 && s.Config.ConfigStore != nil {
s.updateKeyStatus(ctx, listModelsErr.ExtraFields.KeyStatuses)
}
logger.Error("failed to list models for provider %s: %v: falling back onto the static datasheet", provider, bifrost.GetErrorMessage(listModelsErr))
}
allowedModels := make([]schemas.Model, 0)
for _, key := range providerConfig.Keys {
if key.Models.IsUnrestricted() {
continue
}
for _, model := range key.Models {
allowedModels = append(allowedModels, schemas.Model{
ID: string(provider) + "/" + model,
})
}
}
s.Config.ModelCatalog.UpsertModelDataForProvider(provider, modelData, allowedModels)
unfilteredModelData, listModelsErr := s.Client.ListModelsRequest(bfCtx, &schemas.BifrostListModelsRequest{
Provider: provider,
Unfiltered: true,
})
if listModelsErr != nil {
logger.Error("failed to list unfiltered models for provider %s: %v: falling back onto the static datasheet", provider, bifrost.GetErrorMessage(listModelsErr))
} else {
s.Config.ModelCatalog.UpsertUnfilteredModelDataForProvider(provider, unfilteredModelData)
}
}(provider, providerConfig)
}
wg.Wait()
}
logger.Info("models added to catalog")
s.Config.SetBifrostClient(s.Client)
// Initialize routes
s.Router = router.New()
commonMiddlewares := s.PrepareCommonMiddlewares()
apiMiddlewares := commonMiddlewares
inferenceMiddlewares := commonMiddlewares
if s.Config.ConfigStore == nil {
logger.Error("auth middleware requires config store, skipping auth middleware initialization")
} else {
s.WSTicketStore = handlers.NewWSTicketStore()
s.AuthMiddleware, err = handlers.InitAuthMiddleware(s.Config.ConfigStore, s.WSTicketStore)
if err != nil {
s.WSTicketStore.Stop()
s.WSTicketStore = nil
return fmt.Errorf("failed to initialize auth middleware: %v", err)
}
if ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
apiMiddlewares = append(apiMiddlewares, s.AuthMiddleware.APIMiddleware())
}
}
// Register routes
err = s.RegisterAPIRoutes(s.Ctx, s, apiMiddlewares...)
if err != nil {
if s.WSTicketStore != nil {
s.WSTicketStore.Stop()
s.WSTicketStore = nil
}
return fmt.Errorf("failed to initialize routes: %v", err)
}
// Registering inference routes
if ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil && s.AuthMiddleware != nil {
inferenceMiddlewares = append(inferenceMiddlewares, s.AuthMiddleware.InferenceMiddleware())
}
// Once auth is done we will first add the Tracing middleware
// Always add tracing middleware when tracer is enabled - it creates traces and sets traceID in context
// The observability plugins are optional (can be empty if only logging is enabled)
// Curating observability plugins
observabilityPlugins := s.CollectObservabilityPlugins()
// This enables the central streaming accumulator for both use cases
// Initializing tracer with embedded streaming accumulator
traceStore := tracing.NewTraceStore(60*time.Minute, logger)
tracer := tracing.NewTracer(traceStore, s.Config.ModelCatalog, logger)
tracer.SetObservabilityPlugins(observabilityPlugins)
s.Client.SetTracer(tracer)
s.TracingMiddleware = handlers.NewTracingMiddleware(tracer)
// TransportInterceptor must be inside TracingMiddleware so that the tracing defer
// runs AFTER transport post-hooks (capturing HTTPTransportPostHook plugin logs).
// Order: Tracing.pre → TransportInterceptor.pre → handler → TransportInterceptor.post → Tracing.defer
inferenceMiddlewares = append([]schemas.BifrostHTTPMiddleware{handlers.TransportInterceptorMiddleware(s.Config)}, inferenceMiddlewares...)
inferenceMiddlewares = append([]schemas.BifrostHTTPMiddleware{s.TracingMiddleware.Middleware()}, inferenceMiddlewares...)
err = s.RegisterInferenceRoutes(s.Ctx, inferenceMiddlewares...)
if err != nil {
if s.WSTicketStore != nil {
s.WSTicketStore.Stop()
s.WSTicketStore = nil
}
return fmt.Errorf("failed to initialize inference routes: %v", err)
}
// Register UI handler
s.RegisterUIRoutes()
// Create fasthttp server instance
s.Server = &fasthttp.Server{
Handler: handlers.SecurityHeadersMiddleware()(handlers.CorsMiddleware(s.Config)(handlers.RequestDecompressionMiddleware(s.Config)(s.Router.Handler))),
MaxRequestBodySize: s.Config.ClientConfig.MaxRequestBodySizeMB * 1024 * 1024,
ReadBufferSize: 1024 * 64, // 64kb
}
return nil
}
// Start starts the HTTP server at the specified host and port
// Also watches signals and errors
func (s *BifrostHTTPServer) Start() error {
// Printing plugin status in a table
for _, pluginStatus := range s.Config.GetPluginStatus() {
logger.Info("plugin status: %s - %s", pluginStatus.Name, pluginStatus.Status)
}
// Create channels for signal and error handling
sigChan := make(chan os.Signal, 1)
errChan := make(chan error, 1)
// Watching for signals
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start server in a goroutine
serverAddr := net.JoinHostPort(s.Host, s.Port)
ln, err := net.Listen("tcp", serverAddr)
if err != nil {
return fmt.Errorf("failed to create listener on %s: %v", serverAddr, err)
}
go func() {
logger.Info("successfully started bifrost, serving UI on http://%s:%s", s.Host, s.Port)
if err := s.Server.Serve(ln); err != nil {
errChan <- err
}
}()
// Wait for either termination signal or server error
select {
case sig := <-sigChan:
logger.Info("received signal %v, initiating graceful shutdown...", sig)
if s.IntegrationHandler != nil {
logger.Info("closing realtime transport sessions...")
s.IntegrationHandler.Close()
}
// Create shutdown context with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Perform graceful shutdown
if err := s.Server.Shutdown(); err != nil {
logger.Error("error during graceful shutdown: %v", err)
} else {
logger.Info("server gracefully shutdown")
}
// Cancelling main context
if s.cancel != nil {
s.cancel()
}
// Wait for shutdown to complete or timeout
done := make(chan struct{})
go func() {
defer close(done)
logger.Info("shutting down bifrost client...")
s.Client.Shutdown()
logger.Info("bifrost client shutdown completed")
logger.Info("cleaning up storage engines...")
// Cleanup server-specific components
if s.LogsCleaner != nil {
logger.Info("stopping log retention cleaner...")
s.LogsCleaner.StopCleanupRoutine()
}
if s.AsyncJobCleaner != nil {
logger.Info("stopping async job cleaner...")
s.AsyncJobCleaner.StopCleanupRoutine()
}
if s.WSTicketStore != nil {
logger.Info("stopping ws ticket store...")
s.WSTicketStore.Stop()
}
if s.devPprofHandler != nil {
logger.Info("stopping dev pprof handler...")
s.devPprofHandler.Cleanup()
}
if s.wsPool != nil {
logger.Info("closing websocket connection pool...")
s.wsPool.Close()
}
// Cleanup Config and all its background components
if s.Config != nil {
s.Config.Close(shutdownCtx)
}
logger.Info("storage engines cleanup completed")
}()
select {
case <-done:
logger.Info("cleanup completed")
case <-shutdownCtx.Done():
logger.Warn("cleanup timed out after 30 seconds")
}
case err := <-errChan:
if s.IntegrationHandler != nil {
s.IntegrationHandler.Close()
}
if s.wsPool != nil {
s.wsPool.Close()
}
return err
}
return nil
}