Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions framework/configstore/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,20 @@ type EnvKeyInfo struct {
// ClientConfig represents the core configuration for Bifrost HTTP transport and the Bifrost Client.
// It includes settings for excess request handling, Prometheus metrics, and initial pool size.
type ClientConfig struct {
DropExcessRequests bool `json:"drop_excess_requests"` // Drop excess requests if the provider queue is full
InitialPoolSize int `json:"initial_pool_size"` // The initial pool size for the bifrost client
PrometheusLabels []string `json:"prometheus_labels"` // The labels to be used for prometheus metrics
EnableLogging bool `json:"enable_logging"` // Enable logging of requests and responses
DisableContentLogging bool `json:"disable_content_logging"` // Disable logging of content
LogRetentionDays int `json:"log_retention_days" validate:"min=1"` // Number of days to retain logs (minimum 1 day)
EnableGovernance bool `json:"enable_governance"` // Enable governance on all requests
EnforceGovernanceHeader bool `json:"enforce_governance_header"` // Enforce governance on all requests
AllowDirectKeys bool `json:"allow_direct_keys"` // Allow direct keys to be used for requests
AllowedOrigins []string `json:"allowed_origins,omitempty"` // Additional allowed origins for CORS and WebSocket (localhost is always allowed)
MaxRequestBodySizeMB int `json:"max_request_body_size_mb"` // The maximum request body size in MB
EnableLiteLLMFallbacks bool `json:"enable_litellm_fallbacks"` // Enable litellm-specific fallbacks for text completion for Groq
ConfigHash string `json:"-"` // Config hash for reconciliation (not serialized)
DropExcessRequests bool `json:"drop_excess_requests"` // Drop excess requests if the provider queue is full
InitialPoolSize int `json:"initial_pool_size"` // The initial pool size for the bifrost client
PrometheusLabels []string `json:"prometheus_labels"` // The labels to be used for prometheus metrics
EnableLogging bool `json:"enable_logging"` // Enable logging of requests and responses
DisableContentLogging bool `json:"disable_content_logging"` // Disable logging of content
LogRetentionDays int `json:"log_retention_days" validate:"min=1"` // Number of days to retain logs (minimum 1 day)
EnableGovernance bool `json:"enable_governance"` // Enable governance on all requests
EnforceGovernanceHeader bool `json:"enforce_governance_header"` // Enforce governance on all requests
AllowDirectKeys bool `json:"allow_direct_keys"` // Allow direct keys to be used for requests
AllowedOrigins []string `json:"allowed_origins,omitempty"` // Additional allowed origins for CORS and WebSocket (localhost is always allowed)
MaxRequestBodySizeMB int `json:"max_request_body_size_mb"` // The maximum request body size in MB
EnableLiteLLMFallbacks bool `json:"enable_litellm_fallbacks"` // Enable litellm-specific fallbacks for text completion for Groq
HeaderFilterConfig *tables.GlobalHeaderFilterConfig `json:"header_filter_config,omitempty"` // Global header filtering configuration for x-bf-eh-* headers
ConfigHash string `json:"-"` // Config hash for reconciliation (not serialized)
}

// GenerateClientConfigHash generates a SHA256 hash of the client configuration.
Expand Down Expand Up @@ -140,6 +141,34 @@ func (c *ClientConfig) GenerateClientConfigHash() (string, error) {
hash.Write(data)
}

// Hash HeaderFilterConfig
if c.HeaderFilterConfig != nil {
// Hash Allowlist (sorted for deterministic hashing)
if len(c.HeaderFilterConfig.Allowlist) > 0 {
sortedAllowlist := make([]string, len(c.HeaderFilterConfig.Allowlist))
copy(sortedAllowlist, c.HeaderFilterConfig.Allowlist)
sort.Strings(sortedAllowlist)
data, err := sonic.Marshal(sortedAllowlist)
if err != nil {
return "", err
}
hash.Write([]byte("headerFilterConfig.allowlist:"))
hash.Write(data)
}
// Hash Denylist (sorted for deterministic hashing)
if len(c.HeaderFilterConfig.Denylist) > 0 {
sortedDenylist := make([]string, len(c.HeaderFilterConfig.Denylist))
copy(sortedDenylist, c.HeaderFilterConfig.Denylist)
sort.Strings(sortedDenylist)
data, err := sonic.Marshal(sortedDenylist)
if err != nil {
return "", err
}
hash.Write([]byte("headerFilterConfig.denylist:"))
hash.Write(data)
}
}

return hex.EncodeToString(hash.Sum(nil)), nil
}

Expand Down
31 changes: 31 additions & 0 deletions framework/configstore/rdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,37 @@ func (s *RDBConfigStore) UpdateProxyConfig(ctx context.Context, config *tables.G
}).Error
}

// GetHeaderFilterConfig retrieves the header filter configuration from the database.
func (s *RDBConfigStore) GetHeaderFilterConfig(ctx context.Context) (*tables.GlobalHeaderFilterConfig, error) {
var configEntry tables.TableGovernanceConfig
if err := s.db.WithContext(ctx).First(&configEntry, "key = ?", tables.ConfigHeaderFilterKey).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
if configEntry.Value == "" {
return nil, nil
}
var headerFilterConfig tables.GlobalHeaderFilterConfig
if err := json.Unmarshal([]byte(configEntry.Value), &headerFilterConfig); err != nil {
return nil, fmt.Errorf("failed to unmarshal header filter config: %w", err)
}
return &headerFilterConfig, nil
}

// UpdateHeaderFilterConfig updates the header filter configuration in the database.
func (s *RDBConfigStore) UpdateHeaderFilterConfig(ctx context.Context, config *tables.GlobalHeaderFilterConfig) error {
configJSON, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal header filter config: %w", err)
}
return s.db.WithContext(ctx).Save(&tables.TableGovernanceConfig{
Key: tables.ConfigHeaderFilterKey,
Value: string(configJSON),
}).Error
}

// GetRestartRequiredConfig retrieves the restart required configuration from the database.
func (s *RDBConfigStore) GetRestartRequiredConfig(ctx context.Context) (*tables.RestartRequiredConfig, error) {
var configEntry tables.TableGovernanceConfig
Expand Down
4 changes: 4 additions & 0 deletions framework/configstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ type ConfigStore interface {
GetProxyConfig(ctx context.Context) (*tables.GlobalProxyConfig, error)
UpdateProxyConfig(ctx context.Context, config *tables.GlobalProxyConfig) error

// Header filter config CRUD
GetHeaderFilterConfig(ctx context.Context) (*tables.GlobalHeaderFilterConfig, error)
UpdateHeaderFilterConfig(ctx context.Context, config *tables.GlobalHeaderFilterConfig) error

// Restart required config CRUD
GetRestartRequiredConfig(ctx context.Context) (*tables.RestartRequiredConfig, error)
SetRestartRequiredConfig(ctx context.Context, config *tables.RestartRequiredConfig) error
Expand Down
28 changes: 20 additions & 8 deletions framework/configstore/tables/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
ConfigDisableAuthOnInferenceKey = "disable_auth_on_inference"
ConfigProxyKey = "proxy_config"
ConfigRestartRequiredKey = "restart_required"
ConfigHeaderFilterKey = "header_filter_config"
)

// RestartRequiredConfig represents the restart required configuration
Expand All @@ -22,20 +23,31 @@ type RestartRequiredConfig struct {

// GlobalProxyConfig represents the global proxy configuration
type GlobalProxyConfig struct {
Enabled bool `json:"enabled"`
Type network.GlobalProxyType `json:"type"` // "http", "socks5", "tcp"
URL string `json:"url"` // Proxy URL (e.g., http://proxy.example.com:8080)
Username string `json:"username,omitempty"` // Optional authentication username
Password string `json:"password,omitempty"` // Optional authentication password
NoProxy string `json:"no_proxy,omitempty"` // Comma-separated list of hosts to bypass proxy
Timeout int `json:"timeout,omitempty"` // Connection timeout in seconds
SkipTLSVerify bool `json:"skip_tls_verify,omitempty"`// Skip TLS certificate verification
Enabled bool `json:"enabled"`
Type network.GlobalProxyType `json:"type"` // "http", "socks5", "tcp"
URL string `json:"url"` // Proxy URL (e.g., http://proxy.example.com:8080)
Username string `json:"username,omitempty"` // Optional authentication username
Password string `json:"password,omitempty"` // Optional authentication password
NoProxy string `json:"no_proxy,omitempty"` // Comma-separated list of hosts to bypass proxy
Timeout int `json:"timeout,omitempty"` // Connection timeout in seconds
SkipTLSVerify bool `json:"skip_tls_verify,omitempty"` // Skip TLS certificate verification
// Entity enablement flags
EnableForSCIM bool `json:"enable_for_scim"` // Enable proxy for SCIM requests (enterprise only)
EnableForInference bool `json:"enable_for_inference"` // Enable proxy for inference requests
EnableForAPI bool `json:"enable_for_api"` // Enable proxy for API requests
}

// GlobalHeaderFilterConfig represents global header filtering configuration
// for headers forwarded to LLM providers via the x-bf-eh-* prefix.
// Filter logic:
// - If allowlist is non-empty, only headers in the allowlist are forwarded
// - If denylist is non-empty, headers in the denylist are dropped
// - If both are non-empty, allowlist takes precedence first, then denylist filters the result
type GlobalHeaderFilterConfig struct {
Allowlist []string `json:"allowlist,omitempty"` // If non-empty, only these headers are allowed
Denylist []string `json:"denylist,omitempty"` // Headers to always block
}

// TableGovernanceConfig represents generic configuration key-value pairs
type TableGovernanceConfig struct {
Key string `gorm:"primaryKey;type:varchar(255)" json:"key"`
Expand Down
29 changes: 29 additions & 0 deletions transports/bifrost-http/handlers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type ConfigManager interface {
UpdateDropExcessRequests(ctx context.Context, value bool)
ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any) error
ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error
ReloadHeaderFilterConfig(ctx context.Context, config *configstoreTables.GlobalHeaderFilterConfig) error
}

// ConfigHandler manages runtime configuration updates for Bifrost.
Expand Down Expand Up @@ -157,6 +158,13 @@ func (h *ConfigHandler) getConfig(ctx *fasthttp.RequestCtx) {
}
mapConfig["proxy_config"] = proxyConfig
}
// Fetching header filter config
headerFilterConfig, err := h.store.ConfigStore.GetHeaderFilterConfig(ctx)
if err != nil {
logger.Warn(fmt.Sprintf("failed to get header filter config from store: %v", err))
} else if headerFilterConfig != nil {
mapConfig["header_filter_config"] = headerFilterConfig
}
// Fetching restart required config
restartConfig, err := h.store.ConfigStore.GetRestartRequiredConfig(ctx)
if err != nil {
Expand Down Expand Up @@ -265,6 +273,16 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) {

updatedConfig.EnableLiteLLMFallbacks = payload.ClientConfig.EnableLiteLLMFallbacks

// Handle HeaderFilterConfig changes
if !headerFilterConfigEqual(payload.ClientConfig.HeaderFilterConfig, currentConfig.HeaderFilterConfig) {
updatedConfig.HeaderFilterConfig = payload.ClientConfig.HeaderFilterConfig
if err := h.configManager.ReloadHeaderFilterConfig(ctx, payload.ClientConfig.HeaderFilterConfig); err != nil {
logger.Warn(fmt.Sprintf("failed to reload header filter config: %v", err))
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to reload header filter config: %v", err))
return
}
}

// Validate LogRetentionDays
if payload.ClientConfig.LogRetentionDays < 1 {
logger.Warn("log_retention_days must be at least 1")
Expand Down Expand Up @@ -611,3 +629,14 @@ func (h *ConfigHandler) updateProxyConfig(ctx *fasthttp.RequestCtx) {
"message": "proxy configuration updated successfully",
})
}

// headerFilterConfigEqual compares two GlobalHeaderFilterConfig for equality
func headerFilterConfigEqual(a, b *configstoreTables.GlobalHeaderFilterConfig) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return slices.Equal(a.Allowlist, b.Allowlist) && slices.Equal(a.Denylist, b.Denylist)
}
Loading