feat: Beta策略支持按模型区分处理(模型白名单)
This commit is contained in:
@ -157,10 +157,13 @@ type RectifierSettings struct {
|
||||
|
||||
// BetaPolicyRule Beta 策略规则 DTO
|
||||
type BetaPolicyRule struct {
|
||||
BetaToken string `json:"beta_token"`
|
||||
Action string `json:"action"`
|
||||
Scope string `json:"scope"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
BetaToken string `json:"beta_token"`
|
||||
Action string `json:"action"`
|
||||
Scope string `json:"scope"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
ModelWhitelist []string `json:"model_whitelist,omitempty"`
|
||||
FallbackAction string `json:"fallback_action,omitempty"`
|
||||
FallbackErrorMessage string `json:"fallback_error_message,omitempty"`
|
||||
}
|
||||
|
||||
// BetaPolicySettings Beta 策略配置 DTO
|
||||
|
||||
@ -3946,7 +3946,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
// Beta policy: evaluate once; block check + cache filter set for buildUpstreamRequest.
|
||||
// Always overwrite the cache to prevent stale values from a previous retry with a different account.
|
||||
if account.Platform == PlatformAnthropic && c != nil {
|
||||
policy := s.evaluateBetaPolicy(ctx, c.GetHeader("anthropic-beta"), account)
|
||||
policy := s.evaluateBetaPolicy(ctx, c.GetHeader("anthropic-beta"), account, parsed.Model)
|
||||
if policy.blockErr != nil {
|
||||
return nil, policy.blockErr
|
||||
}
|
||||
@ -5603,7 +5603,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
|
||||
}
|
||||
|
||||
// Build effective drop set: merge static defaults with dynamic beta policy filter rules
|
||||
policyFilterSet := s.getBetaPolicyFilterSet(ctx, c, account)
|
||||
policyFilterSet := s.getBetaPolicyFilterSet(ctx, c, account, modelID)
|
||||
effectiveDropSet := mergeDropSets(policyFilterSet)
|
||||
effectiveDropWithClaudeCodeSet := mergeDropSets(policyFilterSet, claude.BetaClaudeCode)
|
||||
|
||||
@ -5843,7 +5843,7 @@ type betaPolicyResult struct {
|
||||
}
|
||||
|
||||
// evaluateBetaPolicy loads settings once and evaluates all rules against the given request.
|
||||
func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader string, account *Account) betaPolicyResult {
|
||||
func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader string, account *Account, model string) betaPolicyResult {
|
||||
if s.settingService == nil {
|
||||
return betaPolicyResult{}
|
||||
}
|
||||
@ -5858,10 +5858,11 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri
|
||||
if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
|
||||
continue
|
||||
}
|
||||
switch rule.Action {
|
||||
effectiveAction, effectiveErrMsg := resolveRuleAction(rule, model)
|
||||
switch effectiveAction {
|
||||
case BetaPolicyActionBlock:
|
||||
if result.blockErr == nil && betaHeader != "" && containsBetaToken(betaHeader, rule.BetaToken) {
|
||||
msg := rule.ErrorMessage
|
||||
msg := effectiveErrMsg
|
||||
if msg == "" {
|
||||
msg = "beta feature " + rule.BetaToken + " is not allowed"
|
||||
}
|
||||
@ -5903,7 +5904,7 @@ const betaPolicyFilterSetKey = "betaPolicyFilterSet"
|
||||
// In the /v1/messages path, Forward() evaluates the policy first and caches the result;
|
||||
// buildUpstreamRequest reuses it (zero extra DB calls). In the count_tokens path, this
|
||||
// evaluates on demand (one DB call).
|
||||
func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Context, account *Account) map[string]struct{} {
|
||||
func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Context, account *Account, model string) map[string]struct{} {
|
||||
if c != nil {
|
||||
if v, ok := c.Get(betaPolicyFilterSetKey); ok {
|
||||
if fs, ok := v.(map[string]struct{}); ok {
|
||||
@ -5911,7 +5912,7 @@ func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Cont
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.evaluateBetaPolicy(ctx, "", account).filterSet
|
||||
return s.evaluateBetaPolicy(ctx, "", account, model).filterSet
|
||||
}
|
||||
|
||||
// betaPolicyScopeMatches checks whether a rule's scope matches the current account type.
|
||||
@ -5930,6 +5931,33 @@ func betaPolicyScopeMatches(scope string, isOAuth bool, isBedrock bool) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// matchModelWhitelist checks if a model matches any pattern in the whitelist.
|
||||
// Reuses matchModelPattern from group.go which supports exact and wildcard prefix matching.
|
||||
func matchModelWhitelist(model string, whitelist []string) bool {
|
||||
for _, pattern := range whitelist {
|
||||
if matchModelPattern(pattern, model) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveRuleAction determines the effective action and error message for a rule given the request model.
|
||||
// When ModelWhitelist is empty, the rule's primary Action/ErrorMessage applies unconditionally.
|
||||
// When non-empty, Action applies to matching models; FallbackAction/FallbackErrorMessage applies to others.
|
||||
func resolveRuleAction(rule BetaPolicyRule, model string) (action, errorMessage string) {
|
||||
if len(rule.ModelWhitelist) == 0 {
|
||||
return rule.Action, rule.ErrorMessage
|
||||
}
|
||||
if matchModelWhitelist(model, rule.ModelWhitelist) {
|
||||
return rule.Action, rule.ErrorMessage
|
||||
}
|
||||
if rule.FallbackAction != "" {
|
||||
return rule.FallbackAction, rule.FallbackErrorMessage
|
||||
}
|
||||
return BetaPolicyActionPass, "" // default fallback: pass (fail-open)
|
||||
}
|
||||
|
||||
// droppedBetaSet returns claude.DroppedBetas as a set, with optional extra tokens.
|
||||
func droppedBetaSet(extra ...string) map[string]struct{} {
|
||||
m := make(map[string]struct{}, len(defaultDroppedBetasSet)+len(extra))
|
||||
@ -5976,7 +6004,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
|
||||
modelID string,
|
||||
) ([]string, error) {
|
||||
// 1. 对原始 header 中的 beta token 做 block 检查(快速失败)
|
||||
policy := s.evaluateBetaPolicy(ctx, betaHeader, account)
|
||||
policy := s.evaluateBetaPolicy(ctx, betaHeader, account, modelID)
|
||||
if policy.blockErr != nil {
|
||||
return nil, policy.blockErr
|
||||
}
|
||||
@ -5988,7 +6016,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
|
||||
// 例如:管理员 block 了 interleaved-thinking,客户端不在 header 中带该 token,
|
||||
// 但请求体中包含 thinking 字段 → autoInjectBedrockBetaTokens 会自动补齐 →
|
||||
// 如果不做此检查,block 规则会被绕过。
|
||||
if blockErr := s.checkBetaPolicyBlockForTokens(ctx, betaTokens, account); blockErr != nil {
|
||||
if blockErr := s.checkBetaPolicyBlockForTokens(ctx, betaTokens, account, modelID); blockErr != nil {
|
||||
return nil, blockErr
|
||||
}
|
||||
|
||||
@ -5997,7 +6025,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
|
||||
|
||||
// checkBetaPolicyBlockForTokens 检查 token 列表中是否有被管理员 block 规则命中的 token。
|
||||
// 用于补充 evaluateBetaPolicy 对 header 的检查,覆盖 body 自动注入的 token。
|
||||
func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, tokens []string, account *Account) *BetaBlockedError {
|
||||
func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, tokens []string, account *Account, model string) *BetaBlockedError {
|
||||
if s.settingService == nil || len(tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -6009,14 +6037,15 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke
|
||||
isBedrock := account.IsBedrock()
|
||||
tokenSet := buildBetaTokenSet(tokens)
|
||||
for _, rule := range settings.Rules {
|
||||
if rule.Action != BetaPolicyActionBlock {
|
||||
effectiveAction, effectiveErrMsg := resolveRuleAction(rule, model)
|
||||
if effectiveAction != BetaPolicyActionBlock {
|
||||
continue
|
||||
}
|
||||
if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
|
||||
continue
|
||||
}
|
||||
if _, present := tokenSet[rule.BetaToken]; present {
|
||||
msg := rule.ErrorMessage
|
||||
msg := effectiveErrMsg
|
||||
if msg == "" {
|
||||
msg = "beta feature " + rule.BetaToken + " is not allowed"
|
||||
}
|
||||
@ -8474,7 +8503,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
|
||||
}
|
||||
|
||||
// Build effective drop set for count_tokens: merge static defaults with dynamic beta policy filter rules
|
||||
ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account))
|
||||
ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account, modelID))
|
||||
|
||||
// OAuth 账号:处理 anthropic-beta header
|
||||
if tokenType == "oauth" {
|
||||
|
||||
@ -1527,6 +1527,18 @@ func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *Be
|
||||
if !validScopes[rule.Scope] {
|
||||
return fmt.Errorf("rule[%d]: invalid scope %q", i, rule.Scope)
|
||||
}
|
||||
// Validate model_whitelist patterns
|
||||
for j, pattern := range rule.ModelWhitelist {
|
||||
trimmed := strings.TrimSpace(pattern)
|
||||
if trimmed == "" {
|
||||
return fmt.Errorf("rule[%d]: model_whitelist[%d] cannot be empty", i, j)
|
||||
}
|
||||
settings.Rules[i].ModelWhitelist[j] = trimmed
|
||||
}
|
||||
// Validate fallback_action
|
||||
if rule.FallbackAction != "" && !validActions[rule.FallbackAction] {
|
||||
return fmt.Errorf("rule[%d]: invalid fallback_action %q", i, rule.FallbackAction)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(settings)
|
||||
|
||||
@ -178,10 +178,13 @@ const (
|
||||
|
||||
// BetaPolicyRule 单条 Beta 策略规则
|
||||
type BetaPolicyRule struct {
|
||||
BetaToken string `json:"beta_token"` // beta token 值
|
||||
Action string `json:"action"` // "pass" | "filter" | "block"
|
||||
Scope string `json:"scope"` // "all" | "oauth" | "apikey" | "bedrock"
|
||||
ErrorMessage string `json:"error_message,omitempty"` // 自定义错误消息 (action=block 时生效)
|
||||
BetaToken string `json:"beta_token"` // beta token 值
|
||||
Action string `json:"action"` // "pass" | "filter" | "block"
|
||||
Scope string `json:"scope"` // "all" | "oauth" | "apikey" | "bedrock"
|
||||
ErrorMessage string `json:"error_message,omitempty"` // 自定义错误消息 (action=block 时生效)
|
||||
ModelWhitelist []string `json:"model_whitelist,omitempty"` // 模型匹配模式列表(为空=对所有模型生效)
|
||||
FallbackAction string `json:"fallback_action,omitempty"` // 未匹配白名单的模型的处理方式
|
||||
FallbackErrorMessage string `json:"fallback_error_message,omitempty"` // 未匹配白名单时的自定义错误消息 (fallback_action=block 时生效)
|
||||
}
|
||||
|
||||
// BetaPolicySettings Beta 策略配置
|
||||
|
||||
@ -359,6 +359,9 @@ export interface BetaPolicyRule {
|
||||
action: 'pass' | 'filter' | 'block'
|
||||
scope: 'all' | 'oauth' | 'apikey' | 'bedrock'
|
||||
error_message?: string
|
||||
model_whitelist?: string[]
|
||||
fallback_action?: 'pass' | 'filter' | 'block'
|
||||
fallback_error_message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4587,7 +4587,19 @@ export default {
|
||||
errorMessagePlaceholder: 'Custom error message when blocked',
|
||||
errorMessageHint: 'Leave empty for default message',
|
||||
saved: 'Beta policy settings saved',
|
||||
saveFailed: 'Failed to save beta policy settings'
|
||||
saveFailed: 'Failed to save beta policy settings',
|
||||
modelWhitelist: 'Model Whitelist',
|
||||
modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., claude-opus-*)',
|
||||
modelPatternPlaceholder: 'e.g., claude-opus-* or claude-opus-4-6',
|
||||
addModelPattern: 'Add model pattern',
|
||||
removePattern: 'Remove',
|
||||
fallbackAction: 'Fallback Action',
|
||||
fallbackActionHint: 'Action for models not matching the whitelist',
|
||||
fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked',
|
||||
quickPresets: 'Quick Presets',
|
||||
presetOpusOnly: 'Opus only for 1M',
|
||||
presetOpusOnlyDesc: 'Pass for Opus, filter others',
|
||||
commonPatterns: 'Common patterns'
|
||||
},
|
||||
saveSettings: 'Save Settings',
|
||||
saving: 'Saving...',
|
||||
|
||||
@ -4751,7 +4751,19 @@ export default {
|
||||
errorMessagePlaceholder: '拦截时返回的自定义错误消息',
|
||||
errorMessageHint: '留空则使用默认错误消息',
|
||||
saved: 'Beta 策略设置保存成功',
|
||||
saveFailed: '保存 Beta 策略设置失败'
|
||||
saveFailed: '保存 Beta 策略设置失败',
|
||||
modelWhitelist: '模型白名单',
|
||||
modelWhitelistHint: '留空则对所有模型生效。支持精确匹配和通配符前缀(如 claude-opus-*)',
|
||||
modelPatternPlaceholder: '例如: claude-opus-* 或 claude-opus-4-6',
|
||||
addModelPattern: '添加模型规则',
|
||||
removePattern: '移除',
|
||||
fallbackAction: '未匹配模型处理方式',
|
||||
fallbackActionHint: '当请求模型不在白名单中时的处理方式',
|
||||
fallbackErrorMessagePlaceholder: '未匹配模型被拦截时返回的自定义错误消息',
|
||||
quickPresets: '快捷预设',
|
||||
presetOpusOnly: '仅 Opus 允许 1M',
|
||||
presetOpusOnlyDesc: 'Opus 透传,其他模型过滤',
|
||||
commonPatterns: '常用模式'
|
||||
},
|
||||
saveSettings: '保存设置',
|
||||
saving: '保存中...',
|
||||
|
||||
@ -630,6 +630,108 @@
|
||||
{{ t('admin.settings.betaPolicy.errorMessageHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Presets (only for tokens with presets) -->
|
||||
<div v-if="betaPresets[rule.beta_token]?.length" class="mt-3">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.betaPolicy.quickPresets') }}
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in betaPresets[rule.beta_token]"
|
||||
:key="preset.label"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-primary-200 bg-primary-50 px-2.5 py-1 text-xs font-medium text-primary-700 transition-colors hover:bg-primary-100 dark:border-primary-800 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/50"
|
||||
@click="applyBetaPreset(rule, preset)"
|
||||
:title="preset.description"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Whitelist -->
|
||||
<div class="mt-3">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.betaPolicy.modelWhitelist') }}
|
||||
</label>
|
||||
<p class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('admin.settings.betaPolicy.modelWhitelistHint') }}
|
||||
</p>
|
||||
<!-- Existing patterns -->
|
||||
<div
|
||||
v-for="(_, index) in (rule.model_whitelist || [])"
|
||||
:key="index"
|
||||
class="mb-1.5 flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="rule.model_whitelist![index]"
|
||||
type="text"
|
||||
class="input input-sm flex-1"
|
||||
:placeholder="t('admin.settings.betaPolicy.modelPatternPlaceholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="rule.model_whitelist!.splice(index, 1)"
|
||||
class="shrink-0 rounded p-1 text-red-400 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Add pattern button -->
|
||||
<button
|
||||
type="button"
|
||||
@click="if (!rule.model_whitelist) rule.model_whitelist = []; rule.model_whitelist.push('')"
|
||||
class="mb-2 inline-flex items-center gap-1 text-xs text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ t('admin.settings.betaPolicy.addModelPattern') }}
|
||||
</button>
|
||||
<!-- Common pattern chips -->
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('admin.settings.betaPolicy.commonPatterns') }}:</span>
|
||||
<button
|
||||
v-for="pattern in commonModelPatterns"
|
||||
:key="pattern"
|
||||
type="button"
|
||||
class="rounded border border-gray-200 px-2 py-0.5 text-xs text-gray-600 transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-700 dark:hover:bg-primary-900/30 dark:hover:text-primary-300"
|
||||
@click="addQuickPattern(rule, pattern)"
|
||||
>
|
||||
{{ pattern }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback Action (only when model_whitelist is non-empty) -->
|
||||
<div v-if="rule.model_whitelist && rule.model_whitelist.length > 0" class="mt-3">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.betaPolicy.fallbackAction') }}
|
||||
</label>
|
||||
<Select
|
||||
:modelValue="rule.fallback_action || 'pass'"
|
||||
@update:modelValue="rule.fallback_action = $event as any"
|
||||
:options="betaPolicyActionOptions"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('admin.settings.betaPolicy.fallbackActionHint') }}
|
||||
</p>
|
||||
<!-- Fallback Error Message (only when fallback_action=block) -->
|
||||
<div v-if="rule.fallback_action === 'block'" class="mt-2">
|
||||
<input
|
||||
v-model="rule.fallback_error_message"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('admin.settings.betaPolicy.fallbackErrorMessagePlaceholder')"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('admin.settings.betaPolicy.errorMessageHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
@ -2058,6 +2160,9 @@ const betaPolicyForm = reactive({
|
||||
action: 'pass' | 'filter' | 'block'
|
||||
scope: 'all' | 'oauth' | 'apikey' | 'bedrock'
|
||||
error_message?: string
|
||||
model_whitelist?: string[]
|
||||
fallback_action?: 'pass' | 'filter' | 'block'
|
||||
fallback_error_message?: string
|
||||
}>
|
||||
})
|
||||
|
||||
@ -2716,10 +2821,48 @@ const betaDisplayNames: Record<string, string> = {
|
||||
'context-1m-2025-08-07': 'Context 1M'
|
||||
}
|
||||
|
||||
// 快捷预设:按 beta_token 定义预设方案
|
||||
const betaPresets: Record<string, Array<{
|
||||
label: string
|
||||
description: string
|
||||
action: 'pass' | 'filter' | 'block'
|
||||
model_whitelist: string[]
|
||||
fallback_action: 'pass' | 'filter' | 'block'
|
||||
}>> = {
|
||||
'context-1m-2025-08-07': [
|
||||
{
|
||||
label: t('admin.settings.betaPolicy.presetOpusOnly'),
|
||||
description: t('admin.settings.betaPolicy.presetOpusOnlyDesc'),
|
||||
action: 'pass',
|
||||
model_whitelist: ['claude-opus-4-6'],
|
||||
fallback_action: 'filter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 常用模型模式(具体 ID + 通配符示例)
|
||||
const commonModelPatterns = ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-*', 'claude-sonnet-*']
|
||||
|
||||
function getBetaDisplayName(token: string): string {
|
||||
return betaDisplayNames[token] || token
|
||||
}
|
||||
|
||||
function applyBetaPreset(
|
||||
rule: (typeof betaPolicyForm.rules)[number],
|
||||
preset: { action: 'pass' | 'filter' | 'block'; model_whitelist: string[]; fallback_action: 'pass' | 'filter' | 'block' }
|
||||
) {
|
||||
rule.action = preset.action
|
||||
rule.model_whitelist = [...preset.model_whitelist]
|
||||
rule.fallback_action = preset.fallback_action
|
||||
}
|
||||
|
||||
function addQuickPattern(rule: (typeof betaPolicyForm.rules)[number], pattern: string) {
|
||||
if (!rule.model_whitelist) rule.model_whitelist = []
|
||||
if (!rule.model_whitelist.includes(pattern)) {
|
||||
rule.model_whitelist.push(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBetaPolicySettings() {
|
||||
betaPolicyLoading.value = true
|
||||
try {
|
||||
@ -2735,8 +2878,22 @@ async function loadBetaPolicySettings() {
|
||||
async function saveBetaPolicySettings() {
|
||||
betaPolicySaving.value = true
|
||||
try {
|
||||
// Clean up empty patterns before saving
|
||||
const cleanedRules = betaPolicyForm.rules.map(rule => {
|
||||
const whitelist = rule.model_whitelist?.filter(p => p.trim() !== '')
|
||||
const hasWhitelist = whitelist && whitelist.length > 0
|
||||
return {
|
||||
beta_token: rule.beta_token,
|
||||
action: rule.action,
|
||||
scope: rule.scope,
|
||||
error_message: rule.error_message,
|
||||
model_whitelist: hasWhitelist ? whitelist : undefined,
|
||||
fallback_action: hasWhitelist ? (rule.fallback_action || 'pass') : undefined,
|
||||
fallback_error_message: hasWhitelist && rule.fallback_action === 'block' ? rule.fallback_error_message : undefined,
|
||||
}
|
||||
})
|
||||
const updated = await adminAPI.settings.updateBetaPolicySettings({
|
||||
rules: betaPolicyForm.rules
|
||||
rules: cleanedRules
|
||||
})
|
||||
betaPolicyForm.rules = updated.rules
|
||||
appStore.showSuccess(t('admin.settings.betaPolicy.saved'))
|
||||
|
||||
Reference in New Issue
Block a user