Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
00a0a121
Commit
00a0a121
authored
Mar 10, 2026
by
shaw
Browse files
feat: Anthropic平台可配置 anthropic-beta 策略
parent
ac6bde7a
Changes
14
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/setting_handler.go
View file @
00a0a121
...
...
@@ -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 更新流超时配置请求
type
UpdateStreamTimeoutSettingsRequest
struct
{
Enabled
bool
`json:"enabled"`
...
...
backend/internal/handler/dto/settings.go
View file @
00a0a121
...
...
@@ -168,6 +168,19 @@ type RectifierSettings struct {
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.
// Returns empty slice on empty/invalid input.
func
ParseCustomMenuItems
(
raw
string
)
[]
CustomMenuItem
{
...
...
backend/internal/handler/gateway_handler.go
View file @
00a0a121
...
...
@@ -652,6 +652,13 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
accountReleaseFunc
()
}
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
if
errors
.
As
(
err
,
&
promptTooLongErr
)
{
reqLog
.
Warn
(
"gateway.prompt_too_long_from_antigravity"
,
...
...
backend/internal/pkg/claude/constants.go
View file @
00a0a121
...
...
@@ -16,7 +16,7 @@ const (
// DroppedBetas 是转发时需要从 anthropic-beta header 中移除的 beta token 列表。
// 这些 token 是客户端特有的,不应透传给上游 API。
var
DroppedBetas
=
[]
string
{
BetaFastMode
}
var
DroppedBetas
=
[]
string
{}
// DefaultBetaHeader Claude Code 客户端默认的 anthropic-beta header
const
DefaultBetaHeader
=
BetaClaudeCode
+
","
+
BetaOAuth
+
","
+
BetaInterleavedThinking
+
","
+
BetaFineGrainedToolStreaming
...
...
backend/internal/server/routes/admin.go
View file @
00a0a121
...
...
@@ -398,6 +398,9 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
// 请求整流器配置
adminSettings
.
GET
(
"/rectifier"
,
h
.
Admin
.
Setting
.
GetRectifierSettings
)
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 存储配置
adminSettings
.
GET
(
"/sora-s3"
,
h
.
Admin
.
Setting
.
GetSoraS3Settings
)
adminSettings
.
PUT
(
"/sora-s3"
,
h
.
Admin
.
Setting
.
UpdateSoraS3Settings
)
...
...
backend/internal/service/domain_constants.go
View file @
00a0a121
...
...
@@ -182,6 +182,13 @@ const (
// SettingKeyRectifierSettings stores JSON config for rectifier settings (thinking signature + budget).
SettingKeyRectifierSettings
=
"rectifier_settings"
// =========================
// Beta Policy Settings
// =========================
// SettingKeyBetaPolicySettings stores JSON config for beta policy rules.
SettingKeyBetaPolicySettings
=
"beta_policy_settings"
// =========================
// Sora S3 存储配置
// =========================
...
...
backend/internal/service/gateway_beta_test.go
View file @
00a0a121
...
...
@@ -86,10 +86,10 @@ func TestStripBetaTokens(t *testing.T) {
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"
,
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
)
{
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"
// DroppedBetas is now empty — filtering moved to configurable beta policy.
// Without a policy filter set, nothing gets dropped from the static set.
drop
:=
droppedBetaSet
()
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
.
Not
Contains
(
t
,
got
,
"fast-mode-2026-02-01"
)
require
.
Contains
(
t
,
got
,
"fast-mode-2026-02-01"
)
}
func
TestDroppedBetaSet
(
t
*
testing
.
T
)
{
// Base set contains DroppedBetas
// Base set contains DroppedBetas
(now empty — filtering moved to configurable beta policy)
base
:=
droppedBetaSet
()
require
.
NotContains
(
t
,
base
,
claude
.
BetaContext1M
)
require
.
Contains
(
t
,
base
,
claude
.
BetaFastMode
)
require
.
Len
(
t
,
base
,
len
(
claude
.
DroppedBetas
))
// With extra tokens
extended
:=
droppedBetaSet
(
claude
.
BetaClaudeCode
)
require
.
NotContains
(
t
,
extended
,
claude
.
BetaContext1M
)
require
.
Contains
(
t
,
extended
,
claude
.
BetaFastMode
)
require
.
Contains
(
t
,
extended
,
claude
.
BetaClaudeCode
)
require
.
Len
(
t
,
extended
,
len
(
claude
.
DroppedBetas
)
+
1
)
}
...
...
backend/internal/service/gateway_service.go
View file @
00a0a121
...
...
@@ -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
)
}
// 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
reqModel
:=
parsed
.
Model
reqStream
:=
parsed
.
Stream
...
...
@@ -5133,6 +5147,11 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
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)
if
tokenType
==
"oauth"
{
if
mimicClaudeCode
{
...
...
@@ -5146,13 +5165,17 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
// messages requests typically use only oauth + interleaved-thinking.
// Also drop claude-code beta if a downstream client added it.
requiredBetas
:=
[]
string
{
claude
.
BetaOAuth
,
claude
.
BetaInterleavedThinking
}
req
.
Header
.
Set
(
"anthropic-beta"
,
mergeAnthropicBetaDropping
(
requiredBetas
,
incomingBeta
,
droppedBetas
WithClaudeCodeSet
))
req
.
Header
.
Set
(
"anthropic-beta"
,
mergeAnthropicBetaDropping
(
requiredBetas
,
incomingBeta
,
effectiveDrop
WithClaudeCodeSet
))
}
else
{
// Claude Code 客户端:尽量透传原始 header,仅补齐 oauth beta
clientBetaHeader
:=
req
.
Header
.
Get
(
"anthropic-beta"
)
req
.
Header
.
Set
(
"anthropic-beta"
,
stripBetaTokensWithSet
(
s
.
getBetaHeader
(
modelID
,
clientBetaHeader
),
d
ef
aultDroppedBetas
Set
))
req
.
Header
.
Set
(
"anthropic-beta"
,
stripBetaTokensWithSet
(
s
.
getBetaHeader
(
modelID
,
clientBetaHeader
),
ef
fectiveDrop
Set
))
}
}
else
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
Gateway
.
InjectBetaForAPIKey
&&
req
.
Header
.
Get
(
"anthropic-beta"
)
==
""
{
}
else
{
// API-key accounts: apply beta policy filter to strip controlled tokens
if
existingBeta
:=
req
.
Header
.
Get
(
"anthropic-beta"
);
existingBeta
!=
""
{
req
.
Header
.
Set
(
"anthropic-beta"
,
stripBetaTokensWithSet
(
existingBeta
,
effectiveDropSet
))
}
else
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
Gateway
.
InjectBetaForAPIKey
{
// API-key:仅在请求显式使用 beta 特性且客户端未提供时,按需补齐(默认关闭)
if
requestNeedsBetaFeatures
(
body
)
{
if
beta
:=
defaultAPIKeyBetaHeader
(
body
);
beta
!=
""
{
...
...
@@ -5160,6 +5183,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
}
}
}
}
// Always capture a compact fingerprint line for later error diagnostics.
// We only print it when needed (or when the explicit debug flag is enabled).
...
...
@@ -5334,6 +5358,104 @@ func stripBetaTokensWithSet(header string, drop map[string]struct{}) string {
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.
func
droppedBetaSet
(
extra
...
string
)
map
[
string
]
struct
{}
{
m
:=
make
(
map
[
string
]
struct
{},
len
(
defaultDroppedBetasSet
)
+
len
(
extra
))
...
...
@@ -5370,10 +5492,7 @@ func buildBetaTokenSet(tokens []string) map[string]struct{} {
return
m
}
var
(
defaultDroppedBetasSet
=
buildBetaTokenSet
(
claude
.
DroppedBetas
)
droppedBetasWithClaudeCodeSet
=
droppedBetaSet
(
claude
.
BetaClaudeCode
)
)
var
defaultDroppedBetasSet
=
buildBetaTokenSet
(
claude
.
DroppedBetas
)
// applyClaudeCodeMimicHeaders forces "Claude Code-like" request headers.
// 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
)
}
// 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
if
tokenType
==
"oauth"
{
if
mimicClaudeCode
{
...
...
@@ -7318,8 +7440,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
incomingBeta
:=
req
.
Header
.
Get
(
"anthropic-beta"
)
requiredBetas
:=
[]
string
{
claude
.
BetaClaudeCode
,
claude
.
BetaOAuth
,
claude
.
BetaInterleavedThinking
,
claude
.
BetaTokenCounting
}
drop
:=
droppedBetaSet
()
req
.
Header
.
Set
(
"anthropic-beta"
,
mergeAnthropicBetaDropping
(
requiredBetas
,
incomingBeta
,
drop
))
req
.
Header
.
Set
(
"anthropic-beta"
,
mergeAnthropicBetaDropping
(
requiredBetas
,
incomingBeta
,
ctEffectiveDropSet
))
}
else
{
clientBetaHeader
:=
req
.
Header
.
Get
(
"anthropic-beta"
)
if
clientBetaHeader
==
""
{
...
...
@@ -7329,10 +7450,14 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
if
!
strings
.
Contains
(
beta
,
claude
.
BetaTokenCounting
)
{
beta
=
beta
+
","
+
claude
.
BetaTokenCounting
}
req
.
Header
.
Set
(
"anthropic-beta"
,
stripBetaTokensWithSet
(
beta
,
defaultDroppedBetas
Set
))
req
.
Header
.
Set
(
"anthropic-beta"
,
stripBetaTokensWithSet
(
beta
,
ctEffectiveDrop
Set
))
}
}
}
else
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
Gateway
.
InjectBetaForAPIKey
&&
req
.
Header
.
Get
(
"anthropic-beta"
)
==
""
{
}
else
{
// API-key accounts: apply beta policy filter to strip controlled tokens
if
existingBeta
:=
req
.
Header
.
Get
(
"anthropic-beta"
);
existingBeta
!=
""
{
req
.
Header
.
Set
(
"anthropic-beta"
,
stripBetaTokensWithSet
(
existingBeta
,
ctEffectiveDropSet
))
}
else
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
Gateway
.
InjectBetaForAPIKey
{
// API-key:与 messages 同步的按需 beta 注入(默认关闭)
if
requestNeedsBetaFeatures
(
body
)
{
if
beta
:=
defaultAPIKeyBetaHeader
(
body
);
beta
!=
""
{
...
...
@@ -7340,6 +7465,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
}
}
}
}
if
c
!=
nil
&&
tokenType
==
"oauth"
{
c
.
Set
(
claudeMimicDebugInfoKey
,
buildClaudeMimicDebugLine
(
req
,
body
,
account
,
tokenType
,
mimicClaudeCode
))
...
...
backend/internal/service/setting_service.go
View file @
00a0a121
...
...
@@ -1247,6 +1247,60 @@ func (s *SettingService) IsBudgetRectifierEnabled(ctx context.Context) bool {
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 设置流超时处理配置
func
(
s
*
SettingService
)
SetStreamTimeoutSettings
(
ctx
context
.
Context
,
settings
*
StreamTimeoutSettings
)
error
{
if
settings
==
nil
{
...
...
backend/internal/service/settings_view.go
View file @
00a0a121
...
...
@@ -191,3 +191,45 @@ func DefaultRectifierSettings() *RectifierSettings {
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
,
},
},
}
}
frontend/src/api/admin/settings.ts
View file @
00a0a121
...
...
@@ -308,6 +308,49 @@ export async function updateRectifierSettings(
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 ====================
export
interface
SoraS3Settings
{
...
...
@@ -456,6 +499,8 @@ export const settingsAPI = {
updateStreamTimeoutSettings
,
getRectifierSettings
,
updateRectifierSettings
,
getBetaPolicySettings
,
updateBetaPolicySettings
,
getSoraS3Settings
,
updateSoraS3Settings
,
testSoraS3Connection
,
...
...
frontend/src/i18n/locales/en.ts
View file @
00a0a121
...
...
@@ -4043,6 +4043,23 @@ export default {
saved
:
'
Rectifier settings saved
'
,
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
'
,
saving
:
'
Saving...
'
,
settingsSaved
:
'
Settings saved successfully
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
00a0a121
...
...
@@ -4216,6 +4216,23 @@ export default {
saved
:
'
整流器设置保存成功
'
,
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
:
'
保存设置
'
,
saving
:
'
保存中...
'
,
settingsSaved
:
'
设置保存成功
'
,
...
...
frontend/src/views/admin/SettingsView.vue
View file @
00a0a121
...
...
@@ -405,6 +405,117 @@
<
/template
>
<
/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
-->
<!--
Tab
:
Security
—
Registration
,
Turnstile
,
LinuxDo
-->
...
...
@@ -1627,6 +1738,18 @@ const rectifierForm = reactive({
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
{
value
:
number
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
(()
=>
{
loadSettings
()
loadSubscriptionGroups
()
loadAdminApiKey
()
loadStreamTimeoutSettings
()
loadRectifierSettings
()
loadBetaPolicySettings
()
}
)
<
/script
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment