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
67 changes: 67 additions & 0 deletions plugins/governance/blocklist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package governance

import (
"testing"

"github.com/maximhq/bifrost/core/schemas"
)

func TestIsModelBlockedByList(t *testing.T) {
tests := []struct {
name string
blacklist schemas.BlackList
model string
want bool
}{
{
name: "empty blacklist allows",
blacklist: schemas.BlackList{},
model: "mistral:latest",
want: false,
},
{
name: "wildcard blocks all",
blacklist: schemas.BlackList{"*"},
model: "llama3.2:latest",
want: true,
},
{
name: "bare blocks bare",
blacklist: schemas.BlackList{"mistral:latest"},
model: "mistral:latest",
want: true,
},
{
name: "prefixed blacklist blocks bare request",
blacklist: schemas.BlackList{"ollama/mistral:latest"},
model: "mistral:latest",
want: true,
},
{
name: "bare blacklist blocks prefixed request",
blacklist: schemas.BlackList{"mistral:latest"},
model: "ollama/mistral:latest",
want: true,
},
{
name: "prefixed blocks prefixed",
blacklist: schemas.BlackList{"ollama/mistral:latest"},
model: "ollama/mistral:latest",
want: true,
},
{
name: "different model not blocked",
blacklist: schemas.BlackList{"mistral:latest"},
model: "llama3.2:latest",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isModelBlockedByList(tt.blacklist, tt.model); got != tt.want {
t.Fatalf("isModelBlockedByList(%v, %q) = %v, want %v", tt.blacklist, tt.model, got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion plugins/governance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,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 config.BlacklistedModels.IsBlocked(modelStr) {
if isModelBlockedByList(config.BlacklistedModels, modelStr) {
blacklistedProviders[config.Provider] = true
}
}
Expand Down
4 changes: 3 additions & 1 deletion plugins/governance/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,10 @@ func (r *BudgetResolver) isModelAllowed(vk *configstoreTables.TableVirtualKey, p
}

// Pass 1: if any matching provider config blacklists the model, block immediately.
// isModelBlockedByList handles entries stored with a provider prefix (e.g. "gemini/model")
// as well as bare names, matching how IsModelAllowedForProvider normalizes the allowlist.
for _, pc := range vk.ProviderConfigs {
if pc.Provider == string(provider) && pc.BlacklistedModels.IsBlocked(model) {
if pc.Provider == string(provider) && isModelBlockedByList(pc.BlacklistedModels, model) {
return false
}
}
Expand Down
27 changes: 26 additions & 1 deletion plugins/governance/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ func getWeight(w *float64) float64 {
return *w
}

// isModelBlockedByList checks if a model is blocked by a blacklist that may store entries
// with or without a provider prefix (e.g., "ollama/mistral:latest" or "mistral:latest").
// Both the blacklist entry and the incoming model are normalized before comparison so that
// bare and provider-prefixed forms are treated as equivalent.
func isModelBlockedByList(blacklist schemas.BlackList, model string) bool {
if blacklist.IsBlockAll() {
return true
}

_, normalizedModel := schemas.ParseModelString(model, "")

for _, blocked := range blacklist {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Vaibhav701161 I merged this but can you create a follow up pr to replace this with slices.Contains?

if strings.EqualFold(blocked, model) {
return true
}

_, normalizedBlocked := schemas.ParseModelString(blocked, "")
if strings.EqualFold(normalizedBlocked, normalizedModel) {
return true
}
}

return false
}

// filterModelsForVirtualKey filters models based on virtual key's provider configs
// Returns only models that are allowed by the virtual key's ProviderConfigs
func (p *GovernancePlugin) filterModelsForVirtualKey(
Expand Down Expand Up @@ -114,7 +139,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) && pc.BlacklistedModels.IsBlocked(modelName) {
if pc.Provider == string(provider) && isModelBlockedByList(pc.BlacklistedModels, modelName) {
isBlocked = true
break
}
Expand Down
111 changes: 111 additions & 0 deletions ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const providerConfigSchema = z.object({
.max(1, "Weight must be at most 1")
.optional(),
allowed_models: z.array(z.string()).optional(),
blacklisted_models: z.array(z.string()).optional(),
key_ids: z.array(z.string()).optional(), // Keys associated with this provider config
// Provider-level budget
budgets: z
Expand Down Expand Up @@ -286,6 +287,7 @@ export default function VirtualKeySheet({
provider: config.provider,
weight: config.weight ?? undefined,
allowed_models: config.allowed_models,
blacklisted_models: config.blacklisted_models || [],
key_ids: config.allow_all_keys
? ["*"]
: config.keys?.map((key) => key.key_id) || [],
Expand Down Expand Up @@ -433,6 +435,7 @@ export default function VirtualKeySheet({
provider: provider,
weight: undefined as number | undefined, // undefined = excluded from weighted routing until user sets a weight
allowed_models: ["*"],
blacklisted_models: [],
key_ids: ["*"],
};

Expand Down Expand Up @@ -1412,6 +1415,114 @@ export default function VirtualKeySheet({
</div>
</div>

{/* Blocked Models for this provider */}
<div className="flex w-full items-start gap-2">
<div className="w-1/4" />
<div className="w-3/4 space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">
Blocked Models
</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Info className="text-muted-foreground h-3 w-3" />
</span>
Comment thread
Vaibhav701161 marked this conversation as resolved.
</TooltipTrigger>
<TooltipContent>
<p>
Models this VK must never serve.
The denylist wins if a model
appears in both Allowed Models and
Blocked Models.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{(() => {
const hasWildcardBlocked = (
config.blacklisted_models || []
).includes("*");
return (
<ModelMultiselect
data-testid={`vk-models-blocked-multiselect-${index}`}
provider={config.provider}
keys={(() => {
const providerKeys =
availableKeys.filter(
(key) =>
key.provider ===
config.provider,
);
const configKeyIds =
config.key_ids || [];
return configKeyIds.includes("*")
? providerKeys.map(
(key) => key.key_id,
)
: providerKeys
.filter((key) =>
configKeyIds.includes(
key.key_id,
),
)
.map((key) => key.key_id);
})()}
allowAllOption={true}
value={
hasWildcardBlocked
? ["*"]
: config.blacklisted_models || []
}
onChange={(models: string[]) => {
const hadStar = (
config.blacklisted_models || []
).includes("*");
const hasStar =
models.includes("*");
if (!hadStar && hasStar) {
handleUpdateProviderConfig(
index,
"blacklisted_models",
["*"],
);
} else if (
hadStar &&
hasStar &&
models.length > 1
) {
handleUpdateProviderConfig(
index,
"blacklisted_models",
models.filter((m) => m !== "*"),
);
} else {
handleUpdateProviderConfig(
index,
"blacklisted_models",
models,
);
}
}}
placeholder={
hasWildcardBlocked
? "All models blocked"
: (
config.blacklisted_models ||
[]
).length === 0
? "No models blocked"
: "Search models..."
}
className="min-h-10 max-w-[500px] min-w-[200px]"
/>
);
})()}
</div>
</div>

{/* Allowed Keys for this provider */}
{(() => {
const providerKeys = availableKeys.filter(
Expand Down
Loading