Skip to content
Merged
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
60 changes: 60 additions & 0 deletions framework/configstore/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error {
if err := migrationRefreshConfigHashAfterMCPExternalServerURLRemoval(ctx, db); err != nil {
return err
}
if err := migrationAddVirtualKeyBlacklistedModelsColumn(ctx, db); err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -8539,3 +8542,60 @@ func migrationAddTempTokensTable(ctx context.Context, db *gorm.DB) error {
}
return nil
}

// migrationAddVirtualKeyBlacklistedModelsColumn adds the blacklisted_models JSON column
// to governance_virtual_key_provider_configs, matching the provider-key blacklist pattern.
func migrationAddVirtualKeyBlacklistedModelsColumn(ctx context.Context, db *gorm.DB) error {
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
ID: "add_vk_provider_config_blacklisted_models_column",
Migrate: func(tx *gorm.DB) error {
tx = tx.WithContext(ctx)
mg := tx.Migrator()
if !mg.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "blacklisted_models") {
if err := mg.AddColumn(&tables.TableVirtualKeyProviderConfig{}, "blacklisted_models"); err != nil {
return fmt.Errorf("failed to add blacklisted_models column: %w", err)
}
// Backfill empty arrays for existing rows (only when column is newly added)
if err := tx.Exec("UPDATE governance_virtual_key_provider_configs SET blacklisted_models = '[]' WHERE blacklisted_models IS NULL OR blacklisted_models = ''").Error; err != nil {
return fmt.Errorf("failed to backfill blacklisted_models: %w", err)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Recompute config_hash for all VKs affected by the backfill so they
// do not appear stale after upgrade (same pattern as allow_all_keys migration).
var virtualKeys []tables.TableVirtualKey
if err := tx.
Preload("ProviderConfigs").
Preload("ProviderConfigs.Keys").
Preload("MCPConfigs").
Find(&virtualKeys).Error; err != nil {
return fmt.Errorf("failed to fetch virtual keys for hash recomputation: %w", err)
}
for _, vk := range virtualKeys {
newHash, err := GenerateVirtualKeyHash(vk)
if err != nil {
return fmt.Errorf("failed to generate hash for VK %s: %w", vk.ID, err)
}
if err := tx.Model(&tables.TableVirtualKey{}).
Where("id = ?", vk.ID).
Update("config_hash", newHash).Error; err != nil {
return fmt.Errorf("failed to update config_hash for VK %s: %w", vk.ID, err)
}
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
tx = tx.WithContext(ctx)
mg := tx.Migrator()
if mg.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "blacklisted_models") {
if err := mg.DropColumn(&tables.TableVirtualKeyProviderConfig{}, "blacklisted_models"); err != nil {
return fmt.Errorf("failed to drop blacklisted_models column: %w", err)
}
}
return nil
},
}})
if err := m.Migrate(); err != nil {
return fmt.Errorf("error running add_vk_provider_config_blacklisted_models_column migration: %s", err.Error())
}
return nil
}
34 changes: 22 additions & 12 deletions framework/configstore/tables/virtualkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ func (TableVirtualKeyProviderConfigKey) TableName() string {

// TableVirtualKeyProviderConfig represents a provider configuration for a virtual key
type TableVirtualKeyProviderConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
VirtualKeyID string `gorm:"type:varchar(255);not null" json:"virtual_key_id"`
Provider string `gorm:"type:varchar(50);not null" json:"provider"`
Weight *float64 `json:"weight"`
AllowedModels schemas.WhiteList `gorm:"type:text;serializer:json" json:"allowed_models"` // ["*"] allows all models; empty denies all (deny-by-default)
AllowAllKeys bool `gorm:"default:false" json:"allow_all_keys"` // True means all keys allowed; false with empty Keys means no keys allowed (deny-by-default)
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
VirtualKeyID string `gorm:"type:varchar(255);not null" json:"virtual_key_id"`
Provider string `gorm:"type:varchar(50);not null" json:"provider"`
Weight *float64 `json:"weight"`
AllowedModels schemas.WhiteList `gorm:"type:text;serializer:json" json:"allowed_models"` // ["*"] allows all models; empty denies all (deny-by-default)
BlacklistedModels schemas.BlackList `gorm:"type:text;serializer:json" json:"blacklisted_models"` // ["*"] blocks all models; empty blocks none
AllowAllKeys bool `gorm:"default:false" json:"allow_all_keys"` // True means all keys allowed; false with empty Keys means no keys allowed (deny-by-default)
RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"`

// Relationships
Expand Down Expand Up @@ -77,30 +78,39 @@ func (pc *TableVirtualKeyProviderConfig) UnmarshalJSON(data []byte) error {
return nil
}

// BeforeSave validates WhiteList fields before GORM persists the record.
// BeforeSave validates WhiteList and BlackList fields before GORM persists the record.
func (pc *TableVirtualKeyProviderConfig) BeforeSave(tx *gorm.DB) error {
if err := pc.AllowedModels.Validate(); err != nil {
return fmt.Errorf("invalid allowed_models: %w", err)
}
if err := pc.BlacklistedModels.Validate(); err != nil {
return fmt.Errorf("invalid blacklisted_models: %w", err)
}
return nil
}

// MarshalJSON custom marshaller to ensure AllowedModels is always an array (never null)
// MarshalJSON custom marshaller to ensure AllowedModels and BlacklistedModels are always arrays (never null)
func (pc TableVirtualKeyProviderConfig) MarshalJSON() ([]byte, error) {
type Alias TableVirtualKeyProviderConfig

// Ensure AllowedModels is an empty slice instead of nil
// Ensure arrays are empty slices instead of nil
allowedModels := pc.AllowedModels
if allowedModels == nil {
allowedModels = []string{}
}
blacklistedModels := pc.BlacklistedModels
if blacklistedModels == nil {
blacklistedModels = []string{}
}

return json.Marshal(&struct {
Alias
AllowedModels []string `json:"allowed_models"`
AllowedModels []string `json:"allowed_models"`
BlacklistedModels []string `json:"blacklisted_models"`
}{
Alias: Alias(pc),
AllowedModels: allowedModels,
Alias: Alias(pc),
AllowedModels: allowedModels,
BlacklistedModels: blacklistedModels,
})
}

Expand Down
14 changes: 14 additions & 0 deletions plugins/governance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,8 +767,22 @@ func (p *GovernancePlugin) loadBalanceProvider(ctx *schemas.BifrostContext, req
p.logger.Debug("[Governance] Virtual key has %d provider configs: %v", len(providerConfigs), configuredProviders)
ctx.AppendRoutingEngineLog(schemas.RoutingEngineGovernance, schemas.LogLevelInfo, fmt.Sprintf("Load balancing model %s across %d configured providers: %v", modelStr, len(providerConfigs), configuredProviders))

// Pre-pass: if any config for a provider blacklists the model, that provider is fully blocked.
blacklistedProviders := make(map[string]bool)
for _, config := range providerConfigs {
if config.BlacklistedModels.IsBlocked(modelStr) {
blacklistedProviders[config.Provider] = true
}
}

allowedProviderConfigs := make([]configstoreTables.TableVirtualKeyProviderConfig, 0)
for _, config := range providerConfigs {
// Blacklist check wins over allowlist (same as provider-key enforcement)
if blacklistedProviders[config.Provider] {
ctx.AppendRoutingEngineLog(schemas.RoutingEngineGovernance, schemas.LogLevelInfo, fmt.Sprintf("Provider %s excluded: model %s is blacklisted", config.Provider, modelStr))
continue
}

// Delegate model allowance check to model catalog
// This handles all cross-provider logic (OpenRouter, Vertex, Groq, Bedrock)
// and provider-prefixed allowed_models entries
Expand Down
24 changes: 16 additions & 8 deletions plugins/governance/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,29 +322,37 @@ func (r *BudgetResolver) EvaluateVirtualKeyRequest(ctx *schemas.BifrostContext,
}
}

// isModelAllowed checks if the requested model is allowed for this VK
// isModelAllowed checks if the requested model is allowed for this VK.
// Blacklisted models win over allowed models (same semantics as provider-key enforcement).
// Two-pass: blacklist scan across all matching configs first, then allowlist scan.
func (r *BudgetResolver) isModelAllowed(vk *configstoreTables.TableVirtualKey, provider schemas.ModelProvider, model string) bool {
// Empty ProviderConfigs means no models are allowed (deny-by-default)
if len(vk.ProviderConfigs) == 0 {
return false
}

// Pass 1: if any matching provider config blacklists the model, block immediately.
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) && pc.BlacklistedModels.IsBlocked(model) {
return false
}
}

// Pass 2: allowlist check — model is allowed if any matching config permits it.
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) {
// Delegate model allowance check to model catalog
// This handles all cross-provider logic (OpenRouter, Vertex, Groq, Bedrock)
// and provider-prefixed allowed_models entries
if r.modelCatalog != nil && r.governanceInMemoryStore != nil {
providerConfig, ok := r.governanceInMemoryStore.GetConfiguredProviders()[provider]
providerConfigPtr := &providerConfig
if !ok {
providerConfigPtr = nil
}
return r.modelCatalog.IsModelAllowedForProvider(provider, model, providerConfigPtr, pc.AllowedModels)
if r.modelCatalog.IsModelAllowedForProvider(provider, model, providerConfigPtr, pc.AllowedModels) {
return true
}
} else if pc.AllowedModels.IsAllowed(model) {
return true
}
// Fallback when model catalog is not available: simple string matching
// ["*"] = allow all models; [] = deny all models
return pc.AllowedModels.IsAllowed(model)
}
}

Expand Down
14 changes: 13 additions & 1 deletion plugins/governance/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,19 @@ func (p *GovernancePlugin) filterModelsForVirtualKey(
for _, model := range models {
provider, modelName := schemas.ParseModelString(model.ID, "")

// Check if this provider/model combination is allowed
// Pre-pass: if any matching config blacklists the model, block it entirely.
isBlocked := false
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) && pc.BlacklistedModels.IsBlocked(modelName) {
isBlocked = true
break
}
}
if isBlocked {
continue
}

// Allowlist check — model is allowed if any matching config permits it.
isAllowed := false
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) {
Expand Down
64 changes: 39 additions & 25 deletions transports/bifrost-http/handlers/governance.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ type CreateVirtualKeyRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description,omitempty"`
ProviderConfigs []struct {
Provider string `json:"provider" validate:"required"`
Weight *float64 `json:"weight,omitempty"`
AllowedModels schemas.WhiteList `json:"allowed_models,omitempty"` // ["*"] allows all models; empty denies all
Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget for provider config
RateLimit *CreateRateLimitRequest `json:"rate_limit,omitempty"` // Provider-level rate limit
KeyIDs schemas.WhiteList `json:"key_ids,omitempty"` // List of DBKey UUIDs to associate with this provider config
Provider string `json:"provider" validate:"required"`
Weight *float64 `json:"weight,omitempty"`
AllowedModels schemas.WhiteList `json:"allowed_models,omitempty"` // ["*"] allows all models; empty denies all
BlacklistedModels schemas.BlackList `json:"blacklisted_models,omitempty"` // ["*"] blocks all models; empty blocks none
Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget for provider config
RateLimit *CreateRateLimitRequest `json:"rate_limit,omitempty"` // Provider-level rate limit
KeyIDs schemas.WhiteList `json:"key_ids,omitempty"` // List of DBKey UUIDs to associate with this provider config
} `json:"provider_configs,omitempty"` // Empty means no providers allowed (deny-by-default)
MCPConfigs []struct {
MCPClientName string `json:"mcp_client_name" validate:"required"`
Expand All @@ -105,13 +106,14 @@ type UpdateVirtualKeyRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
ProviderConfigs []struct {
ID *uint `json:"id,omitempty"` // null for new entries
Provider string `json:"provider" validate:"required"`
Weight *float64 `json:"weight,omitempty"`
AllowedModels schemas.WhiteList `json:"allowed_models,omitempty"` // ["*"] allows all models; empty denies all
Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget for provider config
RateLimit *UpdateRateLimitRequest `json:"rate_limit,omitempty"` // Provider-level rate limit
KeyIDs schemas.WhiteList `json:"key_ids,omitempty"` // List of DBKey UUIDs to associate with this provider config
ID *uint `json:"id,omitempty"` // null for new entries
Provider string `json:"provider" validate:"required"`
Weight *float64 `json:"weight,omitempty"`
AllowedModels schemas.WhiteList `json:"allowed_models,omitempty"` // ["*"] allows all models; empty denies all
BlacklistedModels schemas.BlackList `json:"blacklisted_models,omitempty"` // ["*"] blocks all models; empty blocks none
Budgets []CreateBudgetRequest `json:"budgets,omitempty"` // Multi-budget for provider config
RateLimit *UpdateRateLimitRequest `json:"rate_limit,omitempty"` // Provider-level rate limit
KeyIDs schemas.WhiteList `json:"key_ids,omitempty"` // List of DBKey UUIDs to associate with this provider config
} `json:"provider_configs,omitempty"`
MCPConfigs []struct {
ID *uint `json:"id,omitempty"` // null for new entries
Expand Down Expand Up @@ -692,6 +694,9 @@ func (h *GovernanceHandler) createVirtualKey(ctx *fasthttp.RequestCtx) {
if err := pc.AllowedModels.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid allowed_models for provider %s: %w", pc.Provider, err)}
}
if err := pc.BlacklistedModels.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid blacklisted_models for provider %s: %w", pc.Provider, err)}
}
if err := pc.KeyIDs.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid key_ids for provider %s: %w", pc.Provider, err)}
}
Expand All @@ -713,12 +718,13 @@ func (h *GovernanceHandler) createVirtualKey(ctx *fasthttp.RequestCtx) {
}

providerConfig := &configstoreTables.TableVirtualKeyProviderConfig{
VirtualKeyID: vk.ID,
Provider: string(providerName),
Weight: pc.Weight,
AllowedModels: pc.AllowedModels,
AllowAllKeys: allowAllKeys,
Keys: keys,
VirtualKeyID: vk.ID,
Provider: string(providerName),
Weight: pc.Weight,
AllowedModels: pc.AllowedModels,
BlacklistedModels: pc.BlacklistedModels,
AllowAllKeys: allowAllKeys,
Keys: keys,
}

// Create rate limit for provider config if provided
Expand Down Expand Up @@ -1125,6 +1131,9 @@ func (h *GovernanceHandler) updateVirtualKey(ctx *fasthttp.RequestCtx) {
if err := pc.AllowedModels.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid allowed_models for provider %s: %w", pc.Provider, err)}
}
if err := pc.BlacklistedModels.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid blacklisted_models for provider %s: %w", pc.Provider, err)}
}
if err := pc.KeyIDs.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid key_ids for provider %s: %w", pc.Provider, err)}
}
Expand All @@ -1147,12 +1156,13 @@ func (h *GovernanceHandler) updateVirtualKey(ctx *fasthttp.RequestCtx) {

// Create new provider config
providerConfig := &configstoreTables.TableVirtualKeyProviderConfig{
VirtualKeyID: vk.ID,
Provider: string(providerName),
Weight: pc.Weight,
AllowedModels: pc.AllowedModels,
AllowAllKeys: allowAllKeys,
Keys: keys,
VirtualKeyID: vk.ID,
Provider: string(providerName),
Weight: pc.Weight,
AllowedModels: pc.AllowedModels,
BlacklistedModels: pc.BlacklistedModels,
AllowAllKeys: allowAllKeys,
Keys: keys,
}
// Create rate limit for provider config if provided
if pc.RateLimit != nil {
Expand Down Expand Up @@ -1214,12 +1224,16 @@ func (h *GovernanceHandler) updateVirtualKey(ctx *fasthttp.RequestCtx) {
if err := pc.AllowedModels.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid allowed_models for provider %s: %w", pc.Provider, err)}
}
if err := pc.BlacklistedModels.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid blacklisted_models for provider %s: %w", pc.Provider, err)}
}
if err := pc.KeyIDs.Validate(); err != nil {
return &badRequestError{err: fmt.Errorf("invalid key_ids for provider %s: %w", pc.Provider, err)}
}
existing.Provider = string(providerName)
existing.Weight = pc.Weight
existing.AllowedModels = pc.AllowedModels
existing.BlacklistedModels = pc.BlacklistedModels

// Get keys for this provider config if specified
var keys []configstoreTables.TableKey
Expand Down
Loading
Loading