mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
feat: Anthropic平台可配置 anthropic-beta 策略
This commit is contained in:
@@ -1405,6 +1405,61 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBetaPolicySettings 获取 Beta 策略配置
|
||||||
|
// GET /api/v1/admin/settings/beta-policy
|
||||||
|
func (h *SettingHandler) GetBetaPolicySettings(c *gin.Context) {
|
||||||
|
settings, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := make([]dto.BetaPolicyRule, len(settings.Rules))
|
||||||
|
for i, r := range settings.Rules {
|
||||||
|
rules[i] = dto.BetaPolicyRule(r)
|
||||||
|
}
|
||||||
|
response.Success(c, dto.BetaPolicySettings{Rules: rules})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBetaPolicySettingsRequest 更新 Beta 策略配置请求
|
||||||
|
type UpdateBetaPolicySettingsRequest struct {
|
||||||
|
Rules []dto.BetaPolicyRule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBetaPolicySettings 更新 Beta 策略配置
|
||||||
|
// PUT /api/v1/admin/settings/beta-policy
|
||||||
|
func (h *SettingHandler) UpdateBetaPolicySettings(c *gin.Context) {
|
||||||
|
var req UpdateBetaPolicySettingsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := make([]service.BetaPolicyRule, len(req.Rules))
|
||||||
|
for i, r := range req.Rules {
|
||||||
|
rules[i] = service.BetaPolicyRule(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := &service.BetaPolicySettings{Rules: rules}
|
||||||
|
if err := h.settingService.SetBetaPolicySettings(c.Request.Context(), settings); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch to return updated settings
|
||||||
|
updated, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outRules := make([]dto.BetaPolicyRule, len(updated.Rules))
|
||||||
|
for i, r := range updated.Rules {
|
||||||
|
outRules[i] = dto.BetaPolicyRule(r)
|
||||||
|
}
|
||||||
|
response.Success(c, dto.BetaPolicySettings{Rules: outRules})
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateStreamTimeoutSettingsRequest 更新流超时配置请求
|
// UpdateStreamTimeoutSettingsRequest 更新流超时配置请求
|
||||||
type UpdateStreamTimeoutSettingsRequest struct {
|
type UpdateStreamTimeoutSettingsRequest struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
|||||||
@@ -168,6 +168,19 @@ type RectifierSettings struct {
|
|||||||
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
|
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BetaPolicySettings Beta 策略配置 DTO
|
||||||
|
type BetaPolicySettings struct {
|
||||||
|
Rules []BetaPolicyRule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
// ParseCustomMenuItems parses a JSON string into a slice of CustomMenuItem.
|
// ParseCustomMenuItems parses a JSON string into a slice of CustomMenuItem.
|
||||||
// Returns empty slice on empty/invalid input.
|
// Returns empty slice on empty/invalid input.
|
||||||
func ParseCustomMenuItems(raw string) []CustomMenuItem {
|
func ParseCustomMenuItems(raw string) []CustomMenuItem {
|
||||||
|
|||||||
@@ -652,6 +652,13 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
accountReleaseFunc()
|
accountReleaseFunc()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Beta policy block: return 400 immediately, no failover
|
||||||
|
var betaBlockedErr *service.BetaBlockedError
|
||||||
|
if errors.As(err, &betaBlockedErr) {
|
||||||
|
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", betaBlockedErr.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var promptTooLongErr *service.PromptTooLongError
|
var promptTooLongErr *service.PromptTooLongError
|
||||||
if errors.As(err, &promptTooLongErr) {
|
if errors.As(err, &promptTooLongErr) {
|
||||||
reqLog.Warn("gateway.prompt_too_long_from_antigravity",
|
reqLog.Warn("gateway.prompt_too_long_from_antigravity",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const (
|
|||||||
|
|
||||||
// DroppedBetas 是转发时需要从 anthropic-beta header 中移除的 beta token 列表。
|
// DroppedBetas 是转发时需要从 anthropic-beta header 中移除的 beta token 列表。
|
||||||
// 这些 token 是客户端特有的,不应透传给上游 API。
|
// 这些 token 是客户端特有的,不应透传给上游 API。
|
||||||
var DroppedBetas = []string{BetaFastMode}
|
var DroppedBetas = []string{}
|
||||||
|
|
||||||
// DefaultBetaHeader Claude Code 客户端默认的 anthropic-beta header
|
// DefaultBetaHeader Claude Code 客户端默认的 anthropic-beta header
|
||||||
const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming
|
const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming
|
||||||
|
|||||||
@@ -398,6 +398,9 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
// 请求整流器配置
|
// 请求整流器配置
|
||||||
adminSettings.GET("/rectifier", h.Admin.Setting.GetRectifierSettings)
|
adminSettings.GET("/rectifier", h.Admin.Setting.GetRectifierSettings)
|
||||||
adminSettings.PUT("/rectifier", h.Admin.Setting.UpdateRectifierSettings)
|
adminSettings.PUT("/rectifier", h.Admin.Setting.UpdateRectifierSettings)
|
||||||
|
// Beta 策略配置
|
||||||
|
adminSettings.GET("/beta-policy", h.Admin.Setting.GetBetaPolicySettings)
|
||||||
|
adminSettings.PUT("/beta-policy", h.Admin.Setting.UpdateBetaPolicySettings)
|
||||||
// Sora S3 存储配置
|
// Sora S3 存储配置
|
||||||
adminSettings.GET("/sora-s3", h.Admin.Setting.GetSoraS3Settings)
|
adminSettings.GET("/sora-s3", h.Admin.Setting.GetSoraS3Settings)
|
||||||
adminSettings.PUT("/sora-s3", h.Admin.Setting.UpdateSoraS3Settings)
|
adminSettings.PUT("/sora-s3", h.Admin.Setting.UpdateSoraS3Settings)
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ const (
|
|||||||
// SettingKeyRectifierSettings stores JSON config for rectifier settings (thinking signature + budget).
|
// SettingKeyRectifierSettings stores JSON config for rectifier settings (thinking signature + budget).
|
||||||
SettingKeyRectifierSettings = "rectifier_settings"
|
SettingKeyRectifierSettings = "rectifier_settings"
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Beta Policy Settings
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
// SettingKeyBetaPolicySettings stores JSON config for beta policy rules.
|
||||||
|
SettingKeyBetaPolicySettings = "beta_policy_settings"
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Sora S3 存储配置
|
// Sora S3 存储配置
|
||||||
// =========================
|
// =========================
|
||||||
|
|||||||
@@ -86,10 +86,10 @@ func TestStripBetaTokens(t *testing.T) {
|
|||||||
want: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
want: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DroppedBetas removes fast-mode only",
|
name: "DroppedBetas is empty (filtering moved to configurable beta policy)",
|
||||||
header: "oauth-2025-04-20,context-1m-2025-08-07,fast-mode-2026-02-01,interleaved-thinking-2025-05-14",
|
header: "oauth-2025-04-20,context-1m-2025-08-07,fast-mode-2026-02-01,interleaved-thinking-2025-05-14",
|
||||||
tokens: claude.DroppedBetas,
|
tokens: claude.DroppedBetas,
|
||||||
want: "oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14",
|
want: "oauth-2025-04-20,context-1m-2025-08-07,fast-mode-2026-02-01,interleaved-thinking-2025-05-14",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,25 +114,23 @@ func TestMergeAnthropicBetaDropping_Context1M(t *testing.T) {
|
|||||||
func TestMergeAnthropicBetaDropping_DroppedBetas(t *testing.T) {
|
func TestMergeAnthropicBetaDropping_DroppedBetas(t *testing.T) {
|
||||||
required := []string{"oauth-2025-04-20", "interleaved-thinking-2025-05-14"}
|
required := []string{"oauth-2025-04-20", "interleaved-thinking-2025-05-14"}
|
||||||
incoming := "context-1m-2025-08-07,fast-mode-2026-02-01,foo-beta,oauth-2025-04-20"
|
incoming := "context-1m-2025-08-07,fast-mode-2026-02-01,foo-beta,oauth-2025-04-20"
|
||||||
|
// DroppedBetas is now empty — filtering moved to configurable beta policy.
|
||||||
|
// Without a policy filter set, nothing gets dropped from the static set.
|
||||||
drop := droppedBetaSet()
|
drop := droppedBetaSet()
|
||||||
|
|
||||||
got := mergeAnthropicBetaDropping(required, incoming, drop)
|
got := mergeAnthropicBetaDropping(required, incoming, drop)
|
||||||
require.Equal(t, "oauth-2025-04-20,interleaved-thinking-2025-05-14,context-1m-2025-08-07,foo-beta", got)
|
require.Equal(t, "oauth-2025-04-20,interleaved-thinking-2025-05-14,context-1m-2025-08-07,fast-mode-2026-02-01,foo-beta", got)
|
||||||
require.Contains(t, got, "context-1m-2025-08-07")
|
require.Contains(t, got, "context-1m-2025-08-07")
|
||||||
require.NotContains(t, got, "fast-mode-2026-02-01")
|
require.Contains(t, got, "fast-mode-2026-02-01")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDroppedBetaSet(t *testing.T) {
|
func TestDroppedBetaSet(t *testing.T) {
|
||||||
// Base set contains DroppedBetas
|
// Base set contains DroppedBetas (now empty — filtering moved to configurable beta policy)
|
||||||
base := droppedBetaSet()
|
base := droppedBetaSet()
|
||||||
require.NotContains(t, base, claude.BetaContext1M)
|
|
||||||
require.Contains(t, base, claude.BetaFastMode)
|
|
||||||
require.Len(t, base, len(claude.DroppedBetas))
|
require.Len(t, base, len(claude.DroppedBetas))
|
||||||
|
|
||||||
// With extra tokens
|
// With extra tokens
|
||||||
extended := droppedBetaSet(claude.BetaClaudeCode)
|
extended := droppedBetaSet(claude.BetaClaudeCode)
|
||||||
require.NotContains(t, extended, claude.BetaContext1M)
|
|
||||||
require.Contains(t, extended, claude.BetaFastMode)
|
|
||||||
require.Contains(t, extended, claude.BetaClaudeCode)
|
require.Contains(t, extended, claude.BetaClaudeCode)
|
||||||
require.Len(t, extended, len(claude.DroppedBetas)+1)
|
require.Len(t, extended, len(claude.DroppedBetas)+1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3948,6 +3948,20 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
return s.forwardAnthropicAPIKeyPassthrough(ctx, c, account, passthroughBody, passthroughModel, parsed.Stream, startTime)
|
return s.forwardAnthropicAPIKeyPassthrough(ctx, c, account, passthroughBody, passthroughModel, parsed.Stream, startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if policy.blockErr != nil {
|
||||||
|
return nil, policy.blockErr
|
||||||
|
}
|
||||||
|
filterSet := policy.filterSet
|
||||||
|
if filterSet == nil {
|
||||||
|
filterSet = map[string]struct{}{}
|
||||||
|
}
|
||||||
|
c.Set(betaPolicyFilterSetKey, filterSet)
|
||||||
|
}
|
||||||
|
|
||||||
body := parsed.Body
|
body := parsed.Body
|
||||||
reqModel := parsed.Model
|
reqModel := parsed.Model
|
||||||
reqStream := parsed.Stream
|
reqStream := parsed.Stream
|
||||||
@@ -5133,6 +5147,11 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
|
|||||||
applyClaudeOAuthHeaderDefaults(req, reqStream)
|
applyClaudeOAuthHeaderDefaults(req, reqStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build effective drop set: merge static defaults with dynamic beta policy filter rules
|
||||||
|
policyFilterSet := s.getBetaPolicyFilterSet(ctx, c, account)
|
||||||
|
effectiveDropSet := mergeDropSets(policyFilterSet)
|
||||||
|
effectiveDropWithClaudeCodeSet := mergeDropSets(policyFilterSet, claude.BetaClaudeCode)
|
||||||
|
|
||||||
// 处理 anthropic-beta header(OAuth 账号需要包含 oauth beta)
|
// 处理 anthropic-beta header(OAuth 账号需要包含 oauth beta)
|
||||||
if tokenType == "oauth" {
|
if tokenType == "oauth" {
|
||||||
if mimicClaudeCode {
|
if mimicClaudeCode {
|
||||||
@@ -5146,17 +5165,22 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
|
|||||||
// messages requests typically use only oauth + interleaved-thinking.
|
// messages requests typically use only oauth + interleaved-thinking.
|
||||||
// Also drop claude-code beta if a downstream client added it.
|
// Also drop claude-code beta if a downstream client added it.
|
||||||
requiredBetas := []string{claude.BetaOAuth, claude.BetaInterleavedThinking}
|
requiredBetas := []string{claude.BetaOAuth, claude.BetaInterleavedThinking}
|
||||||
req.Header.Set("anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, droppedBetasWithClaudeCodeSet))
|
req.Header.Set("anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, effectiveDropWithClaudeCodeSet))
|
||||||
} else {
|
} else {
|
||||||
// Claude Code 客户端:尽量透传原始 header,仅补齐 oauth beta
|
// Claude Code 客户端:尽量透传原始 header,仅补齐 oauth beta
|
||||||
clientBetaHeader := req.Header.Get("anthropic-beta")
|
clientBetaHeader := req.Header.Get("anthropic-beta")
|
||||||
req.Header.Set("anthropic-beta", stripBetaTokensWithSet(s.getBetaHeader(modelID, clientBetaHeader), defaultDroppedBetasSet))
|
req.Header.Set("anthropic-beta", stripBetaTokensWithSet(s.getBetaHeader(modelID, clientBetaHeader), effectiveDropSet))
|
||||||
}
|
}
|
||||||
} else if s.cfg != nil && s.cfg.Gateway.InjectBetaForAPIKey && req.Header.Get("anthropic-beta") == "" {
|
} else {
|
||||||
// API-key:仅在请求显式使用 beta 特性且客户端未提供时,按需补齐(默认关闭)
|
// API-key accounts: apply beta policy filter to strip controlled tokens
|
||||||
if requestNeedsBetaFeatures(body) {
|
if existingBeta := req.Header.Get("anthropic-beta"); existingBeta != "" {
|
||||||
if beta := defaultAPIKeyBetaHeader(body); beta != "" {
|
req.Header.Set("anthropic-beta", stripBetaTokensWithSet(existingBeta, effectiveDropSet))
|
||||||
req.Header.Set("anthropic-beta", beta)
|
} else if s.cfg != nil && s.cfg.Gateway.InjectBetaForAPIKey {
|
||||||
|
// API-key:仅在请求显式使用 beta 特性且客户端未提供时,按需补齐(默认关闭)
|
||||||
|
if requestNeedsBetaFeatures(body) {
|
||||||
|
if beta := defaultAPIKeyBetaHeader(body); beta != "" {
|
||||||
|
req.Header.Set("anthropic-beta", beta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5334,6 +5358,104 @@ func stripBetaTokensWithSet(header string, drop map[string]struct{}) string {
|
|||||||
return strings.Join(out, ",")
|
return strings.Join(out, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BetaBlockedError indicates a request was blocked by a beta policy rule.
|
||||||
|
type BetaBlockedError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BetaBlockedError) Error() string { return e.Message }
|
||||||
|
|
||||||
|
// betaPolicyResult holds the evaluated result of beta policy rules for a single request.
|
||||||
|
type betaPolicyResult struct {
|
||||||
|
blockErr *BetaBlockedError // non-nil if a block rule matched
|
||||||
|
filterSet map[string]struct{} // tokens to filter (may be nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluateBetaPolicy loads settings once and evaluates all rules against the given request.
|
||||||
|
func (s *GatewayService) evaluateBetaPolicy(ctx context.Context, betaHeader string, account *Account) betaPolicyResult {
|
||||||
|
if s.settingService == nil {
|
||||||
|
return betaPolicyResult{}
|
||||||
|
}
|
||||||
|
settings, err := s.settingService.GetBetaPolicySettings(ctx)
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return betaPolicyResult{}
|
||||||
|
}
|
||||||
|
isOAuth := account.IsOAuth()
|
||||||
|
var result betaPolicyResult
|
||||||
|
for _, rule := range settings.Rules {
|
||||||
|
if !betaPolicyScopeMatches(rule.Scope, isOAuth) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch rule.Action {
|
||||||
|
case BetaPolicyActionBlock:
|
||||||
|
if result.blockErr == nil && betaHeader != "" && containsBetaToken(betaHeader, rule.BetaToken) {
|
||||||
|
msg := rule.ErrorMessage
|
||||||
|
if msg == "" {
|
||||||
|
msg = "beta feature " + rule.BetaToken + " is not allowed"
|
||||||
|
}
|
||||||
|
result.blockErr = &BetaBlockedError{Message: msg}
|
||||||
|
}
|
||||||
|
case BetaPolicyActionFilter:
|
||||||
|
if result.filterSet == nil {
|
||||||
|
result.filterSet = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
result.filterSet[rule.BetaToken] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeDropSets merges the static defaultDroppedBetasSet with dynamic policy filter tokens.
|
||||||
|
// Returns defaultDroppedBetasSet directly when policySet is empty (zero allocation).
|
||||||
|
func mergeDropSets(policySet map[string]struct{}, extra ...string) map[string]struct{} {
|
||||||
|
if len(policySet) == 0 && len(extra) == 0 {
|
||||||
|
return defaultDroppedBetasSet
|
||||||
|
}
|
||||||
|
m := make(map[string]struct{}, len(defaultDroppedBetasSet)+len(policySet)+len(extra))
|
||||||
|
for t := range defaultDroppedBetasSet {
|
||||||
|
m[t] = struct{}{}
|
||||||
|
}
|
||||||
|
for t := range policySet {
|
||||||
|
m[t] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, t := range extra {
|
||||||
|
m[t] = struct{}{}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// betaPolicyFilterSetKey is the gin.Context key for caching the policy filter set within a request.
|
||||||
|
const betaPolicyFilterSetKey = "betaPolicyFilterSet"
|
||||||
|
|
||||||
|
// getBetaPolicyFilterSet returns the beta policy filter set, using the gin context cache if available.
|
||||||
|
// 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{} {
|
||||||
|
if c != nil {
|
||||||
|
if v, ok := c.Get(betaPolicyFilterSetKey); ok {
|
||||||
|
if fs, ok := v.(map[string]struct{}); ok {
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.evaluateBetaPolicy(ctx, "", account).filterSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// betaPolicyScopeMatches checks whether a rule's scope matches the current account type.
|
||||||
|
func betaPolicyScopeMatches(scope string, isOAuth bool) bool {
|
||||||
|
switch scope {
|
||||||
|
case BetaPolicyScopeAll:
|
||||||
|
return true
|
||||||
|
case BetaPolicyScopeOAuth:
|
||||||
|
return isOAuth
|
||||||
|
case BetaPolicyScopeAPIKey:
|
||||||
|
return !isOAuth
|
||||||
|
default:
|
||||||
|
return true // unknown scope → match all (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))
|
||||||
@@ -5370,10 +5492,7 @@ func buildBetaTokenSet(tokens []string) map[string]struct{} {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var defaultDroppedBetasSet = buildBetaTokenSet(claude.DroppedBetas)
|
||||||
defaultDroppedBetasSet = buildBetaTokenSet(claude.DroppedBetas)
|
|
||||||
droppedBetasWithClaudeCodeSet = droppedBetaSet(claude.BetaClaudeCode)
|
|
||||||
)
|
|
||||||
|
|
||||||
// applyClaudeCodeMimicHeaders forces "Claude Code-like" request headers.
|
// applyClaudeCodeMimicHeaders forces "Claude Code-like" request headers.
|
||||||
// This mirrors opencode-anthropic-auth behavior: do not trust downstream
|
// This mirrors opencode-anthropic-auth behavior: do not trust downstream
|
||||||
@@ -7311,6 +7430,9 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
|
|||||||
applyClaudeOAuthHeaderDefaults(req, false)
|
applyClaudeOAuthHeaderDefaults(req, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build effective drop set for count_tokens: merge static defaults with dynamic beta policy filter rules
|
||||||
|
ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account))
|
||||||
|
|
||||||
// OAuth 账号:处理 anthropic-beta header
|
// OAuth 账号:处理 anthropic-beta header
|
||||||
if tokenType == "oauth" {
|
if tokenType == "oauth" {
|
||||||
if mimicClaudeCode {
|
if mimicClaudeCode {
|
||||||
@@ -7318,8 +7440,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
|
|||||||
|
|
||||||
incomingBeta := req.Header.Get("anthropic-beta")
|
incomingBeta := req.Header.Get("anthropic-beta")
|
||||||
requiredBetas := []string{claude.BetaClaudeCode, claude.BetaOAuth, claude.BetaInterleavedThinking, claude.BetaTokenCounting}
|
requiredBetas := []string{claude.BetaClaudeCode, claude.BetaOAuth, claude.BetaInterleavedThinking, claude.BetaTokenCounting}
|
||||||
drop := droppedBetaSet()
|
req.Header.Set("anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, ctEffectiveDropSet))
|
||||||
req.Header.Set("anthropic-beta", mergeAnthropicBetaDropping(requiredBetas, incomingBeta, drop))
|
|
||||||
} else {
|
} else {
|
||||||
clientBetaHeader := req.Header.Get("anthropic-beta")
|
clientBetaHeader := req.Header.Get("anthropic-beta")
|
||||||
if clientBetaHeader == "" {
|
if clientBetaHeader == "" {
|
||||||
@@ -7329,14 +7450,19 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
|
|||||||
if !strings.Contains(beta, claude.BetaTokenCounting) {
|
if !strings.Contains(beta, claude.BetaTokenCounting) {
|
||||||
beta = beta + "," + claude.BetaTokenCounting
|
beta = beta + "," + claude.BetaTokenCounting
|
||||||
}
|
}
|
||||||
req.Header.Set("anthropic-beta", stripBetaTokensWithSet(beta, defaultDroppedBetasSet))
|
req.Header.Set("anthropic-beta", stripBetaTokensWithSet(beta, ctEffectiveDropSet))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if s.cfg != nil && s.cfg.Gateway.InjectBetaForAPIKey && req.Header.Get("anthropic-beta") == "" {
|
} else {
|
||||||
// API-key:与 messages 同步的按需 beta 注入(默认关闭)
|
// API-key accounts: apply beta policy filter to strip controlled tokens
|
||||||
if requestNeedsBetaFeatures(body) {
|
if existingBeta := req.Header.Get("anthropic-beta"); existingBeta != "" {
|
||||||
if beta := defaultAPIKeyBetaHeader(body); beta != "" {
|
req.Header.Set("anthropic-beta", stripBetaTokensWithSet(existingBeta, ctEffectiveDropSet))
|
||||||
req.Header.Set("anthropic-beta", beta)
|
} else if s.cfg != nil && s.cfg.Gateway.InjectBetaForAPIKey {
|
||||||
|
// API-key:与 messages 同步的按需 beta 注入(默认关闭)
|
||||||
|
if requestNeedsBetaFeatures(body) {
|
||||||
|
if beta := defaultAPIKeyBetaHeader(body); beta != "" {
|
||||||
|
req.Header.Set("anthropic-beta", beta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1247,6 +1247,60 @@ func (s *SettingService) IsBudgetRectifierEnabled(ctx context.Context) bool {
|
|||||||
return settings.Enabled && settings.ThinkingBudgetEnabled
|
return settings.Enabled && settings.ThinkingBudgetEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBetaPolicySettings 获取 Beta 策略配置
|
||||||
|
func (s *SettingService) GetBetaPolicySettings(ctx context.Context) (*BetaPolicySettings, error) {
|
||||||
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyBetaPolicySettings)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrSettingNotFound) {
|
||||||
|
return DefaultBetaPolicySettings(), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get beta policy settings: %w", err)
|
||||||
|
}
|
||||||
|
if value == "" {
|
||||||
|
return DefaultBetaPolicySettings(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings BetaPolicySettings
|
||||||
|
if err := json.Unmarshal([]byte(value), &settings); err != nil {
|
||||||
|
return DefaultBetaPolicySettings(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBetaPolicySettings 设置 Beta 策略配置
|
||||||
|
func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *BetaPolicySettings) error {
|
||||||
|
if settings == nil {
|
||||||
|
return fmt.Errorf("settings cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
validActions := map[string]bool{
|
||||||
|
BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true,
|
||||||
|
}
|
||||||
|
validScopes := map[string]bool{
|
||||||
|
BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, rule := range settings.Rules {
|
||||||
|
if rule.BetaToken == "" {
|
||||||
|
return fmt.Errorf("rule[%d]: beta_token cannot be empty", i)
|
||||||
|
}
|
||||||
|
if !validActions[rule.Action] {
|
||||||
|
return fmt.Errorf("rule[%d]: invalid action %q", i, rule.Action)
|
||||||
|
}
|
||||||
|
if !validScopes[rule.Scope] {
|
||||||
|
return fmt.Errorf("rule[%d]: invalid scope %q", i, rule.Scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal beta policy settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.settingRepo.Set(ctx, SettingKeyBetaPolicySettings, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
// SetStreamTimeoutSettings 设置流超时处理配置
|
// SetStreamTimeoutSettings 设置流超时处理配置
|
||||||
func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings *StreamTimeoutSettings) error {
|
func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings *StreamTimeoutSettings) error {
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
|
|||||||
@@ -191,3 +191,45 @@ func DefaultRectifierSettings() *RectifierSettings {
|
|||||||
ThinkingBudgetEnabled: true,
|
ThinkingBudgetEnabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beta Policy 策略常量
|
||||||
|
const (
|
||||||
|
BetaPolicyActionPass = "pass" // 透传,不做任何处理
|
||||||
|
BetaPolicyActionFilter = "filter" // 过滤,从 beta header 中移除该 token
|
||||||
|
BetaPolicyActionBlock = "block" // 拦截,直接返回错误
|
||||||
|
|
||||||
|
BetaPolicyScopeAll = "all" // 所有账号类型
|
||||||
|
BetaPolicyScopeOAuth = "oauth" // 仅 OAuth 账号
|
||||||
|
BetaPolicyScopeAPIKey = "apikey" // 仅 API Key 账号
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"` // 自定义错误消息 (action=block 时生效)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BetaPolicySettings Beta 策略配置
|
||||||
|
type BetaPolicySettings struct {
|
||||||
|
Rules []BetaPolicyRule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBetaPolicySettings 返回默认的 Beta 策略配置
|
||||||
|
func DefaultBetaPolicySettings() *BetaPolicySettings {
|
||||||
|
return &BetaPolicySettings{
|
||||||
|
Rules: []BetaPolicyRule{
|
||||||
|
{
|
||||||
|
BetaToken: "fast-mode-2026-02-01",
|
||||||
|
Action: BetaPolicyActionFilter,
|
||||||
|
Scope: BetaPolicyScopeAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BetaToken: "context-1m-2025-08-07",
|
||||||
|
Action: BetaPolicyActionFilter,
|
||||||
|
Scope: BetaPolicyScopeAll,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -308,6 +308,49 @@ export async function updateRectifierSettings(
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Beta Policy Settings ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beta policy rule interface
|
||||||
|
*/
|
||||||
|
export interface BetaPolicyRule {
|
||||||
|
beta_token: string
|
||||||
|
action: 'pass' | 'filter' | 'block'
|
||||||
|
scope: 'all' | 'oauth' | 'apikey'
|
||||||
|
error_message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beta policy settings interface
|
||||||
|
*/
|
||||||
|
export interface BetaPolicySettings {
|
||||||
|
rules: BetaPolicyRule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get beta policy settings
|
||||||
|
* @returns Beta policy settings
|
||||||
|
*/
|
||||||
|
export async function getBetaPolicySettings(): Promise<BetaPolicySettings> {
|
||||||
|
const { data } = await apiClient.get<BetaPolicySettings>('/admin/settings/beta-policy')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update beta policy settings
|
||||||
|
* @param settings - Beta policy settings to update
|
||||||
|
* @returns Updated settings
|
||||||
|
*/
|
||||||
|
export async function updateBetaPolicySettings(
|
||||||
|
settings: BetaPolicySettings
|
||||||
|
): Promise<BetaPolicySettings> {
|
||||||
|
const { data } = await apiClient.put<BetaPolicySettings>(
|
||||||
|
'/admin/settings/beta-policy',
|
||||||
|
settings
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Sora S3 Settings ====================
|
// ==================== Sora S3 Settings ====================
|
||||||
|
|
||||||
export interface SoraS3Settings {
|
export interface SoraS3Settings {
|
||||||
@@ -456,6 +499,8 @@ export const settingsAPI = {
|
|||||||
updateStreamTimeoutSettings,
|
updateStreamTimeoutSettings,
|
||||||
getRectifierSettings,
|
getRectifierSettings,
|
||||||
updateRectifierSettings,
|
updateRectifierSettings,
|
||||||
|
getBetaPolicySettings,
|
||||||
|
updateBetaPolicySettings,
|
||||||
getSoraS3Settings,
|
getSoraS3Settings,
|
||||||
updateSoraS3Settings,
|
updateSoraS3Settings,
|
||||||
testSoraS3Connection,
|
testSoraS3Connection,
|
||||||
|
|||||||
@@ -4043,6 +4043,23 @@ export default {
|
|||||||
saved: 'Rectifier settings saved',
|
saved: 'Rectifier settings saved',
|
||||||
saveFailed: 'Failed to save rectifier settings'
|
saveFailed: 'Failed to save rectifier settings'
|
||||||
},
|
},
|
||||||
|
betaPolicy: {
|
||||||
|
title: 'Beta Policy',
|
||||||
|
description: 'How to handle Beta features when configuring the forwarding of Anthropic API requests. Applicable only to the /v1/messages endpoint.',
|
||||||
|
action: 'Action',
|
||||||
|
actionPass: 'Pass (transparent)',
|
||||||
|
actionFilter: 'Filter (remove)',
|
||||||
|
actionBlock: 'Block (reject)',
|
||||||
|
scope: 'Scope',
|
||||||
|
scopeAll: 'All accounts',
|
||||||
|
scopeOAuth: 'OAuth only',
|
||||||
|
scopeAPIKey: 'API Key only',
|
||||||
|
errorMessage: 'Error message',
|
||||||
|
errorMessagePlaceholder: 'Custom error message when blocked',
|
||||||
|
errorMessageHint: 'Leave empty for default message',
|
||||||
|
saved: 'Beta policy settings saved',
|
||||||
|
saveFailed: 'Failed to save beta policy settings'
|
||||||
|
},
|
||||||
saveSettings: 'Save Settings',
|
saveSettings: 'Save Settings',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
settingsSaved: 'Settings saved successfully',
|
settingsSaved: 'Settings saved successfully',
|
||||||
|
|||||||
@@ -4216,6 +4216,23 @@ export default {
|
|||||||
saved: '整流器设置保存成功',
|
saved: '整流器设置保存成功',
|
||||||
saveFailed: '保存整流器设置失败'
|
saveFailed: '保存整流器设置失败'
|
||||||
},
|
},
|
||||||
|
betaPolicy: {
|
||||||
|
title: 'Beta 策略',
|
||||||
|
description: '配置转发 Anthropic API 请求时如何处理 Beta 特性。仅适用于 /v1/messages 接口。',
|
||||||
|
action: '处理方式',
|
||||||
|
actionPass: '透传(不处理)',
|
||||||
|
actionFilter: '过滤(移除)',
|
||||||
|
actionBlock: '拦截(拒绝请求)',
|
||||||
|
scope: '生效范围',
|
||||||
|
scopeAll: '全部账号',
|
||||||
|
scopeOAuth: '仅 OAuth 账号',
|
||||||
|
scopeAPIKey: '仅 API Key 账号',
|
||||||
|
errorMessage: '错误消息',
|
||||||
|
errorMessagePlaceholder: '拦截时返回的自定义错误消息',
|
||||||
|
errorMessageHint: '留空则使用默认错误消息',
|
||||||
|
saved: 'Beta 策略设置保存成功',
|
||||||
|
saveFailed: '保存 Beta 策略设置失败'
|
||||||
|
},
|
||||||
saveSettings: '保存设置',
|
saveSettings: '保存设置',
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
settingsSaved: '设置保存成功',
|
settingsSaved: '设置保存成功',
|
||||||
|
|||||||
@@ -405,6 +405,117 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Beta Policy Settings -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('admin.settings.betaPolicy.title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.betaPolicy.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5 p-6">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="betaPolicyLoading" class="flex items-center gap-2 text-gray-500">
|
||||||
|
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"></div>
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- Rule Cards -->
|
||||||
|
<div
|
||||||
|
v-for="rule in betaPolicyForm.rules"
|
||||||
|
:key="rule.beta_token"
|
||||||
|
class="rounded-lg border border-gray-200 p-4 dark:border-dark-600"
|
||||||
|
>
|
||||||
|
<div class="mb-3 flex items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ getBetaDisplayName(rule.beta_token) }}
|
||||||
|
</span>
|
||||||
|
<span class="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-dark-700 dark:text-gray-400">
|
||||||
|
{{ rule.beta_token }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- Action -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.betaPolicy.action') }}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
:modelValue="rule.action"
|
||||||
|
@update:modelValue="rule.action = $event as any"
|
||||||
|
:options="betaPolicyActionOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scope -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.betaPolicy.scope') }}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
:modelValue="rule.scope"
|
||||||
|
@update:modelValue="rule.scope = $event as any"
|
||||||
|
:options="betaPolicyScopeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message (only when action=block) -->
|
||||||
|
<div v-if="rule.action === 'block'" class="mt-3">
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{{ t('admin.settings.betaPolicy.errorMessage') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="rule.error_message"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.settings.betaPolicy.errorMessagePlaceholder')"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{{ t('admin.settings.betaPolicy.errorMessageHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div class="flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="saveBetaPolicySettings"
|
||||||
|
:disabled="betaPolicySaving"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="betaPolicySaving"
|
||||||
|
class="mr-1 h-4 w-4 animate-spin"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
{{ betaPolicySaving ? t('common.saving') : t('common.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /Tab: Gateway -->
|
</div><!-- /Tab: Gateway -->
|
||||||
|
|
||||||
<!-- Tab: Security — Registration, Turnstile, LinuxDo -->
|
<!-- Tab: Security — Registration, Turnstile, LinuxDo -->
|
||||||
@@ -1627,6 +1738,18 @@ const rectifierForm = reactive({
|
|||||||
thinking_budget_enabled: true
|
thinking_budget_enabled: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Beta Policy 状态
|
||||||
|
const betaPolicyLoading = ref(true)
|
||||||
|
const betaPolicySaving = ref(false)
|
||||||
|
const betaPolicyForm = reactive({
|
||||||
|
rules: [] as Array<{
|
||||||
|
beta_token: string
|
||||||
|
action: 'pass' | 'filter' | 'block'
|
||||||
|
scope: 'all' | 'oauth' | 'apikey'
|
||||||
|
error_message?: string
|
||||||
|
}>
|
||||||
|
})
|
||||||
|
|
||||||
interface DefaultSubscriptionGroupOption {
|
interface DefaultSubscriptionGroupOption {
|
||||||
value: number
|
value: number
|
||||||
label: string
|
label: string
|
||||||
@@ -2165,12 +2288,64 @@ async function saveRectifierSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const betaPolicyActionOptions = computed(() => [
|
||||||
|
{ value: 'pass', label: t('admin.settings.betaPolicy.actionPass') },
|
||||||
|
{ value: 'filter', label: t('admin.settings.betaPolicy.actionFilter') },
|
||||||
|
{ value: 'block', label: t('admin.settings.betaPolicy.actionBlock') }
|
||||||
|
])
|
||||||
|
|
||||||
|
const betaPolicyScopeOptions = computed(() => [
|
||||||
|
{ value: 'all', label: t('admin.settings.betaPolicy.scopeAll') },
|
||||||
|
{ value: 'oauth', label: t('admin.settings.betaPolicy.scopeOAuth') },
|
||||||
|
{ value: 'apikey', label: t('admin.settings.betaPolicy.scopeAPIKey') }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Beta Policy 方法
|
||||||
|
const betaDisplayNames: Record<string, string> = {
|
||||||
|
'fast-mode-2026-02-01': 'Fast Mode',
|
||||||
|
'context-1m-2025-08-07': 'Context 1M'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBetaDisplayName(token: string): string {
|
||||||
|
return betaDisplayNames[token] || token
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBetaPolicySettings() {
|
||||||
|
betaPolicyLoading.value = true
|
||||||
|
try {
|
||||||
|
const settings = await adminAPI.settings.getBetaPolicySettings()
|
||||||
|
betaPolicyForm.rules = settings.rules
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load beta policy settings:', error)
|
||||||
|
} finally {
|
||||||
|
betaPolicyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBetaPolicySettings() {
|
||||||
|
betaPolicySaving.value = true
|
||||||
|
try {
|
||||||
|
const updated = await adminAPI.settings.updateBetaPolicySettings({
|
||||||
|
rules: betaPolicyForm.rules
|
||||||
|
})
|
||||||
|
betaPolicyForm.rules = updated.rules
|
||||||
|
appStore.showSuccess(t('admin.settings.betaPolicy.saved'))
|
||||||
|
} catch (error: any) {
|
||||||
|
appStore.showError(
|
||||||
|
t('admin.settings.betaPolicy.saveFailed') + ': ' + (error.message || t('common.unknownError'))
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
betaPolicySaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSettings()
|
loadSettings()
|
||||||
loadSubscriptionGroups()
|
loadSubscriptionGroups()
|
||||||
loadAdminApiKey()
|
loadAdminApiKey()
|
||||||
loadStreamTimeoutSettings()
|
loadStreamTimeoutSettings()
|
||||||
loadRectifierSettings()
|
loadRectifierSettings()
|
||||||
|
loadBetaPolicySettings()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user