feat: Beta策略支持按模型区分处理(模型白名单)
This commit is contained in:
@@ -161,6 +161,9 @@ type BetaPolicyRule struct {
|
|||||||
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
|
|||||||
// 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
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|||||||
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"
|
|||||||
// 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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
|||||||
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(
|
|||||||
// 例如:管理员 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(
|
|||||||
|
|
||||||
// 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
|
|||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|||||||
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)
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ type BetaPolicyRule struct {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 @@
|
|||||||
{{ 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({
|
|||||||
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> = {
|
|||||||
'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() {
|
|||||||
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'))
|
||||||
|
|||||||
Reference in New Issue
Block a user