Commit 7c60ee3c authored by shaw's avatar shaw
Browse files

feat: Beta策略支持按模型区分处理(模型白名单)

parent b2e379cf
...@@ -157,10 +157,13 @@ type RectifierSettings struct { ...@@ -157,10 +157,13 @@ type RectifierSettings struct {
// BetaPolicyRule Beta 策略规则 DTO // BetaPolicyRule Beta 策略规则 DTO
type BetaPolicyRule struct { type BetaPolicyRule struct {
BetaToken string `json:"beta_token"` BetaToken string `json:"beta_token"`
Action string `json:"action"` Action string `json:"action"`
Scope string `json:"scope"` Scope string `json:"scope"`
ErrorMessage string `json:"error_message,omitempty"` 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 // BetaPolicySettings Beta 策略配置 DTO
......
...@@ -3946,7 +3946,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A ...@@ -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. // 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. // Always overwrite the cache to prevent stale values from a previous retry with a different account.
if account.Platform == PlatformAnthropic && c != nil { 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 { if policy.blockErr != nil {
return nil, policy.blockErr return nil, policy.blockErr
} }
...@@ -5603,7 +5603,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex ...@@ -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 // 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) effectiveDropSet := mergeDropSets(policyFilterSet)
effectiveDropWithClaudeCodeSet := mergeDropSets(policyFilterSet, claude.BetaClaudeCode) effectiveDropWithClaudeCodeSet := mergeDropSets(policyFilterSet, claude.BetaClaudeCode)
...@@ -5843,7 +5843,7 @@ type betaPolicyResult struct { ...@@ -5843,7 +5843,7 @@ type betaPolicyResult struct {
} }
// evaluateBetaPolicy loads settings once and evaluates all rules against the given request. // 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 { if s.settingService == nil {
return betaPolicyResult{} return betaPolicyResult{}
} }
...@@ -5858,10 +5858,11 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri ...@@ -5858,10 +5858,11 @@ func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader stri
if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) { if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
continue continue
} }
switch rule.Action { effectiveAction, effectiveErrMsg := resolveRuleAction(rule, model)
switch effectiveAction {
case BetaPolicyActionBlock: case BetaPolicyActionBlock:
if result.blockErr == nil && betaHeader != "" && containsBetaToken(betaHeader, rule.BetaToken) { if result.blockErr == nil && betaHeader != "" && containsBetaToken(betaHeader, rule.BetaToken) {
msg := rule.ErrorMessage msg := effectiveErrMsg
if msg == "" { if msg == "" {
msg = "beta feature " + rule.BetaToken + " is not allowed" msg = "beta feature " + rule.BetaToken + " is not allowed"
} }
...@@ -5903,7 +5904,7 @@ const betaPolicyFilterSetKey = "betaPolicyFilterSet" ...@@ -5903,7 +5904,7 @@ const betaPolicyFilterSetKey = "betaPolicyFilterSet"
// In the /v1/messages path, Forward() evaluates the policy first and caches the result; // 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 // buildUpstreamRequest reuses it (zero extra DB calls). In the count_tokens path, this
// evaluates on demand (one DB call). // 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 c != nil {
if v, ok := c.Get(betaPolicyFilterSetKey); ok { if v, ok := c.Get(betaPolicyFilterSetKey); ok {
if fs, ok := v.(map[string]struct{}); ok { if fs, ok := v.(map[string]struct{}); ok {
...@@ -5911,7 +5912,7 @@ func (s *GatewayService) getBetaPolicyFilterSet(ctx context.Context, c *gin.Cont ...@@ -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. // 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 { ...@@ -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. // droppedBetaSet returns claude.DroppedBetas as a set, with optional extra tokens.
func droppedBetaSet(extra ...string) map[string]struct{} { func droppedBetaSet(extra ...string) map[string]struct{} {
m := make(map[string]struct{}, len(defaultDroppedBetasSet)+len(extra)) m := make(map[string]struct{}, len(defaultDroppedBetasSet)+len(extra))
...@@ -5976,7 +6004,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest( ...@@ -5976,7 +6004,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
modelID string, modelID string,
) ([]string, error) { ) ([]string, error) {
// 1. 对原始 header 中的 beta token 做 block 检查(快速失败) // 1. 对原始 header 中的 beta token 做 block 检查(快速失败)
policy := s.evaluateBetaPolicy(ctx, betaHeader, account) policy := s.evaluateBetaPolicy(ctx, betaHeader, account, modelID)
if policy.blockErr != nil { if policy.blockErr != nil {
return nil, policy.blockErr return nil, policy.blockErr
} }
...@@ -5988,7 +6016,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest( ...@@ -5988,7 +6016,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
// 例如:管理员 block 了 interleaved-thinking,客户端不在 header 中带该 token, // 例如:管理员 block 了 interleaved-thinking,客户端不在 header 中带该 token,
// 但请求体中包含 thinking 字段 → autoInjectBedrockBetaTokens 会自动补齐 → // 但请求体中包含 thinking 字段 → autoInjectBedrockBetaTokens 会自动补齐 →
// 如果不做此检查,block 规则会被绕过。 // 如果不做此检查,block 规则会被绕过。
if blockErr := s.checkBetaPolicyBlockForTokens(ctx, betaTokens, account); blockErr != nil { if blockErr := s.checkBetaPolicyBlockForTokens(ctx, betaTokens, account, modelID); blockErr != nil {
return nil, blockErr return nil, blockErr
} }
...@@ -5997,7 +6025,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest( ...@@ -5997,7 +6025,7 @@ func (s *GatewayService) resolveBedrockBetaTokensForRequest(
// checkBetaPolicyBlockForTokens 检查 token 列表中是否有被管理员 block 规则命中的 token。 // checkBetaPolicyBlockForTokens 检查 token 列表中是否有被管理员 block 规则命中的 token。
// 用于补充 evaluateBetaPolicy 对 header 的检查,覆盖 body 自动注入的 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 { if s.settingService == nil || len(tokens) == 0 {
return nil return nil
} }
...@@ -6009,14 +6037,15 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke ...@@ -6009,14 +6037,15 @@ func (s *GatewayService) checkBetaPolicyBlockForTokens(ctx context.Context, toke
isBedrock := account.IsBedrock() isBedrock := account.IsBedrock()
tokenSet := buildBetaTokenSet(tokens) tokenSet := buildBetaTokenSet(tokens)
for _, rule := range settings.Rules { for _, rule := range settings.Rules {
if rule.Action != BetaPolicyActionBlock { effectiveAction, effectiveErrMsg := resolveRuleAction(rule, model)
if effectiveAction != BetaPolicyActionBlock {
continue continue
} }
if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) { if !betaPolicyScopeMatches(rule.Scope, isOAuth, isBedrock) {
continue continue
} }
if _, present := tokenSet[rule.BetaToken]; present { if _, present := tokenSet[rule.BetaToken]; present {
msg := rule.ErrorMessage msg := effectiveErrMsg
if msg == "" { if msg == "" {
msg = "beta feature " + rule.BetaToken + " is not allowed" msg = "beta feature " + rule.BetaToken + " is not allowed"
} }
...@@ -8474,7 +8503,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con ...@@ -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 // 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 // OAuth 账号:处理 anthropic-beta header
if tokenType == "oauth" { if tokenType == "oauth" {
......
...@@ -1527,6 +1527,18 @@ func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *Be ...@@ -1527,6 +1527,18 @@ func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *Be
if !validScopes[rule.Scope] { if !validScopes[rule.Scope] {
return fmt.Errorf("rule[%d]: invalid scope %q", i, 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) data, err := json.Marshal(settings)
......
...@@ -178,10 +178,13 @@ const ( ...@@ -178,10 +178,13 @@ const (
// BetaPolicyRule 单条 Beta 策略规则 // BetaPolicyRule 单条 Beta 策略规则
type BetaPolicyRule struct { type BetaPolicyRule struct {
BetaToken string `json:"beta_token"` // beta token 值 BetaToken string `json:"beta_token"` // beta token 值
Action string `json:"action"` // "pass" | "filter" | "block" Action string `json:"action"` // "pass" | "filter" | "block"
Scope string `json:"scope"` // "all" | "oauth" | "apikey" | "bedrock" Scope string `json:"scope"` // "all" | "oauth" | "apikey" | "bedrock"
ErrorMessage string `json:"error_message,omitempty"` // 自定义错误消息 (action=block 时生效) 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 策略配置 // BetaPolicySettings Beta 策略配置
......
...@@ -359,6 +359,9 @@ export interface BetaPolicyRule { ...@@ -359,6 +359,9 @@ export interface BetaPolicyRule {
action: 'pass' | 'filter' | 'block' action: 'pass' | 'filter' | 'block'
scope: 'all' | 'oauth' | 'apikey' | 'bedrock' scope: 'all' | 'oauth' | 'apikey' | 'bedrock'
error_message?: string error_message?: string
model_whitelist?: string[]
fallback_action?: 'pass' | 'filter' | 'block'
fallback_error_message?: string
} }
/** /**
......
...@@ -4587,7 +4587,19 @@ export default { ...@@ -4587,7 +4587,19 @@ export default {
errorMessagePlaceholder: 'Custom error message when blocked', errorMessagePlaceholder: 'Custom error message when blocked',
errorMessageHint: 'Leave empty for default message', errorMessageHint: 'Leave empty for default message',
saved: 'Beta policy settings saved', 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', saveSettings: 'Save Settings',
saving: 'Saving...', saving: 'Saving...',
......
...@@ -4751,7 +4751,19 @@ export default { ...@@ -4751,7 +4751,19 @@ export default {
errorMessagePlaceholder: '拦截时返回的自定义错误消息', errorMessagePlaceholder: '拦截时返回的自定义错误消息',
errorMessageHint: '留空则使用默认错误消息', errorMessageHint: '留空则使用默认错误消息',
saved: 'Beta 策略设置保存成功', 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: '保存设置', saveSettings: '保存设置',
saving: '保存中...', saving: '保存中...',
......
...@@ -630,6 +630,108 @@ ...@@ -630,6 +630,108 @@
{{ t('admin.settings.betaPolicy.errorMessageHint') }} {{ t('admin.settings.betaPolicy.errorMessageHint') }}
</p> </p>
</div> </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> </div>
<!-- Save Button --> <!-- Save Button -->
...@@ -2058,6 +2160,9 @@ const betaPolicyForm = reactive({ ...@@ -2058,6 +2160,9 @@ const betaPolicyForm = reactive({
action: 'pass' | 'filter' | 'block' action: 'pass' | 'filter' | 'block'
scope: 'all' | 'oauth' | 'apikey' | 'bedrock' scope: 'all' | 'oauth' | 'apikey' | 'bedrock'
error_message?: string error_message?: string
model_whitelist?: string[]
fallback_action?: 'pass' | 'filter' | 'block'
fallback_error_message?: string
}> }>
}) })
...@@ -2716,10 +2821,48 @@ const betaDisplayNames: Record<string, string> = { ...@@ -2716,10 +2821,48 @@ const betaDisplayNames: Record<string, string> = {
'context-1m-2025-08-07': 'Context 1M' '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 { function getBetaDisplayName(token: string): string {
return betaDisplayNames[token] || token 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() { async function loadBetaPolicySettings() {
betaPolicyLoading.value = true betaPolicyLoading.value = true
try { try {
...@@ -2735,8 +2878,22 @@ async function loadBetaPolicySettings() { ...@@ -2735,8 +2878,22 @@ async function loadBetaPolicySettings() {
async function saveBetaPolicySettings() { async function saveBetaPolicySettings() {
betaPolicySaving.value = true betaPolicySaving.value = true
try { 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({ const updated = await adminAPI.settings.updateBetaPolicySettings({
rules: betaPolicyForm.rules rules: cleanedRules
}) })
betaPolicyForm.rules = updated.rules betaPolicyForm.rules = updated.rules
appStore.showSuccess(t('admin.settings.betaPolicy.saved')) appStore.showSuccess(t('admin.settings.betaPolicy.saved'))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment