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
73 changes: 0 additions & 73 deletions plugins/governance/blocklist_test.go

This file was deleted.

50 changes: 35 additions & 15 deletions plugins/governance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ func (p *GovernancePlugin) loadBalanceProvider(ctx *schemas.BifrostContext, req
// 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 isModelBlockedByList(config.BlacklistedModels, modelStr) {
if config.BlacklistedModels.IsBlocked(modelStr) {
blacklistedProviders[config.Provider] = true
}
}
Expand Down Expand Up @@ -595,6 +595,32 @@ func (p *GovernancePlugin) loadBalanceProvider(ctx *schemas.BifrostContext, req
return nil
}

// publishRoutingAllowlist records, for downstream routing layers, which of the VK's configured
// providers permit modelStr according to the VK's own allowed_models / blocked_models. It is a
// coarse provider gate (BifrostContextKeyRoutingAllowedProviders) layered on top of the model
// catalog checks those layers already run — its purpose is to stop a later routing layer (load
// balancing, model-catalog resolution) from selecting a provider the VK forbids for this model,
// even when governance itself couldn't pick one. An empty slice means "no provider is permitted"
// (fail-closed via the empty-provider validation in handleRequest); a nil VK publishes nothing.
//
// Provider prefixes on the request model are already split into req.Provider + bare model at the
// HTTP layer (resolveModelAndProvider), so VK allowed_models / blocked_models are matched against
// bare names and plain membership checks are sufficient here.
func (p *GovernancePlugin) publishRoutingAllowlist(ctx *schemas.BifrostContext, virtualKey *configstoreTables.TableVirtualKey, modelStr string) {
if virtualKey == nil {
return
}
allowed := make([]schemas.ModelProvider, 0, len(virtualKey.ProviderConfigs))
for _, pc := range virtualKey.ProviderConfigs {
// No model to filter on → keep the provider so we don't over-restrict.
if modelStr == "" ||
(pc.AllowedModels.IsAllowed(modelStr) && !pc.BlacklistedModels.IsBlocked(modelStr)) {
allowed = append(allowed, schemas.ModelProvider(pc.Provider))
}
}
ctx.SetValue(schemas.BifrostContextKeyRoutingAllowedProviders, allowed)
}

// applyRoutingRules evaluates routing rules against req and mutates
// req.Provider/req.Model/req.Fallbacks when a rule matches. Returns the matched RoutingDecision
// (nil if no rule matched). Integrations normalize req.Model (and Provider when applicable) before
Expand Down Expand Up @@ -1013,20 +1039,6 @@ func (p *GovernancePlugin) PreRequestHook(ctx *schemas.BifrostContext, req *sche

stampGovernanceCtxFromVK(ctx, virtualKey)

// Publish the VK's allowed-provider set so downstream routing layers (enterprise LB,
// model-catalog-resolver) intersect their candidates with it. This guards against the case
// where governance fails to pick a provider (every VK entry rejected by allowed_models /
// budget / rate limit) and a downstream layer would otherwise pick a provider the VK does
// not permit. Empty slice means "no provider is permitted" → fail-closed via the empty-
// provider validation in handleRequest.
if virtualKey != nil {
allowed := make([]schemas.ModelProvider, 0, len(virtualKey.ProviderConfigs))
for _, pc := range virtualKey.ProviderConfigs {
allowed = append(allowed, schemas.ModelProvider(pc.Provider))
}
ctx.SetValue(schemas.BifrostContextKeyRoutingAllowedProviders, allowed)
}

// Large-payload mode: the body streams to the provider unparsed, so req.Model is
// empty for routes where the model lives in the body (OpenAI/Anthropic chat,
// responses, etc.). Route on LargePayloadMetadata.Model — the provider's
Expand All @@ -1041,6 +1053,8 @@ func (p *GovernancePlugin) PreRequestHook(ctx *schemas.BifrostContext, req *sche
if newModel != "" && newModel != metadata.Model {
metadata.Model = newModel
}
_, routedModel := schemas.ParseModelString(metadata.Model, "")
p.publishRoutingAllowlist(ctx, virtualKey, routedModel)
return nil
}

Expand All @@ -1050,6 +1064,12 @@ func (p *GovernancePlugin) PreRequestHook(ctx *schemas.BifrostContext, req *sche
}
}

// Publish the VK provider allowlist for the (post routing-rules) model so downstream routing
// layers (load balancing, model-catalog resolution) and core enforcement intersect their
// candidates with it — a later layer must not select a provider the VK forbids for this model.
_, routedModel, _ := req.GetRequestFields()
p.publishRoutingAllowlist(ctx, virtualKey, routedModel)

if virtualKey != nil {
if err := p.loadBalanceProvider(ctx, req, virtualKey); err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion plugins/governance/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func (r *BudgetResolver) isModelAllowed(vk *configstoreTables.TableVirtualKey, p

// Pass 1: if any matching provider config blacklists the model, block immediately.
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) && isModelBlockedByList(pc.BlacklistedModels, model) {
if pc.Provider == string(provider) && pc.BlacklistedModels.IsBlocked(model) {
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
return false
}
}
Expand Down
33 changes: 1 addition & 32 deletions plugins/governance/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package governance
import (
"context"
"fmt"
"slices"
"strings"

bifrost "github.com/maximhq/bifrost/core"
Expand Down Expand Up @@ -64,36 +63,6 @@ func getWeight(w *float64) float64 {
return *w
}

func blockedModelCandidates(model string) []string {
_, normalized := schemas.ParseModelString(model, "")

if strings.EqualFold(model, normalized) {
return []string{model}
}

return []string{model, normalized}
}

func isModelBlockedByList(blacklist schemas.BlackList, model string) bool {
if blacklist.IsBlockAll() {
return true
}

modelForms := blockedModelCandidates(model)
for _, blocked := range blacklist {
blockedForms := blockedModelCandidates(blocked)
for _, form := range modelForms {
if slices.ContainsFunc(blockedForms, func(blockedForm string) bool {
return strings.EqualFold(blockedForm, form)
}) {
return true
}
}
}

return false
}

// stampGovernanceCtxFromVK copies team/customer identifiers from the VK onto ctx so
// downstream plugins (logging, observability) see the governance scope.
func stampGovernanceCtxFromVK(ctx *schemas.BifrostContext, vk *configstoreTables.TableVirtualKey) {
Expand Down Expand Up @@ -148,7 +117,7 @@ func (p *GovernancePlugin) filterModelsForVirtualKey(
// Pre-pass: if any matching config blacklists the model, block it entirely.
isBlocked := false
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) && isModelBlockedByList(pc.BlacklistedModels, modelName) {
if pc.Provider == string(provider) && pc.BlacklistedModels.IsBlocked(modelName) {
isBlocked = true
break
}
Expand Down
Loading