Skip to content

Commit 01d8286

Browse files
committed
feat: add max_claude_code_version setting and disable auto-upgrade env var
Add maximum Claude Code version limit to complement the existing minimum version check. Refactor the version cache from single-value to unified bounds struct (min+max) with a single atomic.Value and singleflight group. - Backend: new constant, struct field, cache refactor, validation (semver format + cross-validation max >= min), gateway enforcement, audit diff - Frontend: settings UI input, TypeScript types, zh/en i18n - Add CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 to all Claude Code tutorials on /keys page (unix/cmd/powershell/vscode settings.json)
1 parent 0236b97 commit 01d8286

11 files changed

Lines changed: 130 additions & 49 deletions

File tree

backend/internal/handler/admin/setting_handler.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
125125
OpsQueryModeDefault: settings.OpsQueryModeDefault,
126126
OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds,
127127
MinClaudeCodeVersion: settings.MinClaudeCodeVersion,
128+
MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion,
128129
AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling,
129130
BackendModeEnabled: settings.BackendModeEnabled,
130131
})
@@ -199,6 +200,7 @@ type UpdateSettingsRequest struct {
199200
OpsMetricsIntervalSeconds *int `json:"ops_metrics_interval_seconds"`
200201

201202
MinClaudeCodeVersion string `json:"min_claude_code_version"`
203+
MaxClaudeCodeVersion string `json:"max_claude_code_version"`
202204

203205
// 分组隔离
204206
AllowUngroupedKeyScheduling bool `json:"allow_ungrouped_key_scheduling"`
@@ -442,6 +444,22 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
442444
}
443445
}
444446

447+
// 验证最高版本号格式(空字符串=禁用,或合法 semver)
448+
if req.MaxClaudeCodeVersion != "" {
449+
if !semverPattern.MatchString(req.MaxClaudeCodeVersion) {
450+
response.Error(c, http.StatusBadRequest, "max_claude_code_version must be empty or a valid semver (e.g. 3.0.0)")
451+
return
452+
}
453+
}
454+
455+
// 交叉验证:如果同时设置了最低和最高版本号,最高版本号必须 >= 最低版本号
456+
if req.MinClaudeCodeVersion != "" && req.MaxClaudeCodeVersion != "" {
457+
if service.CompareVersions(req.MaxClaudeCodeVersion, req.MinClaudeCodeVersion) < 0 {
458+
response.Error(c, http.StatusBadRequest, "max_claude_code_version must be greater than or equal to min_claude_code_version")
459+
return
460+
}
461+
}
462+
445463
settings := &service.SystemSettings{
446464
RegistrationEnabled: req.RegistrationEnabled,
447465
EmailVerifyEnabled: req.EmailVerifyEnabled,
@@ -488,6 +506,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
488506
EnableIdentityPatch: req.EnableIdentityPatch,
489507
IdentityPatchPrompt: req.IdentityPatchPrompt,
490508
MinClaudeCodeVersion: req.MinClaudeCodeVersion,
509+
MaxClaudeCodeVersion: req.MaxClaudeCodeVersion,
491510
AllowUngroupedKeyScheduling: req.AllowUngroupedKeyScheduling,
492511
BackendModeEnabled: req.BackendModeEnabled,
493512
OpsMonitoringEnabled: func() bool {
@@ -588,6 +607,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
588607
OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault,
589608
OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds,
590609
MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion,
610+
MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion,
591611
AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling,
592612
BackendModeEnabled: updatedSettings.BackendModeEnabled,
593613
})
@@ -744,6 +764,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
744764
if before.MinClaudeCodeVersion != after.MinClaudeCodeVersion {
745765
changed = append(changed, "min_claude_code_version")
746766
}
767+
if before.MaxClaudeCodeVersion != after.MaxClaudeCodeVersion {
768+
changed = append(changed, "max_claude_code_version")
769+
}
747770
if before.AllowUngroupedKeyScheduling != after.AllowUngroupedKeyScheduling {
748771
changed = append(changed, "allow_ungrouped_key_scheduling")
749772
}

backend/internal/handler/dto/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type SystemSettings struct {
7979
OpsMetricsIntervalSeconds int `json:"ops_metrics_interval_seconds"`
8080

8181
MinClaudeCodeVersion string `json:"min_claude_code_version"`
82+
MaxClaudeCodeVersion string `json:"max_claude_code_version"`
8283

8384
// 分组隔离
8485
AllowUngroupedKeyScheduling bool `json:"allow_ungrouped_key_scheduling"`

backend/internal/handler/gateway_handler.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,7 @@ func (h *GatewayHandler) ensureForwardErrorResponse(c *gin.Context, streamStarte
12811281
return true
12821282
}
12831283

1284-
// checkClaudeCodeVersion 检查 Claude Code 客户端版本是否满足最低要求
1284+
// checkClaudeCodeVersion 检查 Claude Code 客户端版本是否满足版本要求
12851285
// 仅对已识别的 Claude Code 客户端执行,count_tokens 路径除外
12861286
func (h *GatewayHandler) checkClaudeCodeVersion(c *gin.Context) bool {
12871287
ctx := c.Request.Context()
@@ -1294,8 +1294,8 @@ func (h *GatewayHandler) checkClaudeCodeVersion(c *gin.Context) bool {
12941294
return true
12951295
}
12961296

1297-
minVersion := h.settingService.GetMinClaudeCodeVersion(ctx)
1298-
if minVersion == "" {
1297+
minVersion, maxVersion := h.settingService.GetClaudeCodeVersionBounds(ctx)
1298+
if minVersion == "" && maxVersion == "" {
12991299
return true // 未设置,不检查
13001300
}
13011301

@@ -1306,13 +1306,22 @@ func (h *GatewayHandler) checkClaudeCodeVersion(c *gin.Context) bool {
13061306
return false
13071307
}
13081308

1309-
if service.CompareVersions(clientVersion, minVersion) < 0 {
1309+
if minVersion != "" && service.CompareVersions(clientVersion, minVersion) < 0 {
13101310
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error",
13111311
fmt.Sprintf("Your Claude Code version (%s) is below the minimum required version (%s). Please update: npm update -g @anthropic-ai/claude-code",
13121312
clientVersion, minVersion))
13131313
return false
13141314
}
13151315

1316+
if maxVersion != "" && service.CompareVersions(clientVersion, maxVersion) > 0 {
1317+
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error",
1318+
fmt.Sprintf("Your Claude Code version (%s) exceeds the maximum allowed version (%s). "+
1319+
"Please downgrade: npm install -g @anthropic-ai/claude-code@%s && "+
1320+
"set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 to prevent auto-upgrade",
1321+
clientVersion, maxVersion, maxVersion))
1322+
return false
1323+
}
1324+
13161325
return true
13171326
}
13181327

backend/internal/service/domain_constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ const (
226226
// SettingKeyMinClaudeCodeVersion 最低 Claude Code 版本号要求 (semver, 如 "2.1.0",空值=不检查)
227227
SettingKeyMinClaudeCodeVersion = "min_claude_code_version"
228228

229+
// SettingKeyMaxClaudeCodeVersion 最高 Claude Code 版本号限制 (semver, 如 "3.0.0",空值=不检查)
230+
SettingKeyMaxClaudeCodeVersion = "max_claude_code_version"
231+
229232
// SettingKeyAllowUngroupedKeyScheduling 允许未分组 API Key 调度(默认 false:未分组 Key 返回 403)
230233
SettingKeyAllowUngroupedKeyScheduling = "allow_ungrouped_key_scheduling"
231234

backend/internal/service/setting_service.go

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,27 @@ type SettingRepository interface {
4444
Delete(ctx context.Context, key string) error
4545
}
4646

47-
// cachedMinVersion 缓存最低 Claude Code 版本号(进程内缓存,60s TTL)
48-
type cachedMinVersion struct {
49-
value string // 空字符串 = 不检查
47+
// cachedVersionBounds 缓存 Claude Code 版本号上下限(进程内缓存,60s TTL)
48+
type cachedVersionBounds struct {
49+
min string // 空字符串 = 不检查
50+
max string // 空字符串 = 不检查
5051
expiresAt int64 // unix nano
5152
}
5253

53-
// minVersionCache 最低版本号进程内缓存
54-
var minVersionCache atomic.Value // *cachedMinVersion
54+
// versionBoundsCache 版本号上下限进程内缓存
55+
var versionBoundsCache atomic.Value // *cachedVersionBounds
5556

56-
// minVersionSF 防止缓存过期时 thundering herd
57-
var minVersionSF singleflight.Group
57+
// versionBoundsSF 防止缓存过期时 thundering herd
58+
var versionBoundsSF singleflight.Group
5859

59-
// minVersionCacheTTL 缓存有效期
60-
const minVersionCacheTTL = 60 * time.Second
60+
// versionBoundsCacheTTL 缓存有效期
61+
const versionBoundsCacheTTL = 60 * time.Second
6162

62-
// minVersionErrorTTL DB 错误时的短缓存,快速重试
63-
const minVersionErrorTTL = 5 * time.Second
63+
// versionBoundsErrorTTL DB 错误时的短缓存,快速重试
64+
const versionBoundsErrorTTL = 5 * time.Second
6465

65-
// minVersionDBTimeout singleflight 内 DB 查询超时,独立于请求 context
66-
const minVersionDBTimeout = 5 * time.Second
66+
// versionBoundsDBTimeout singleflight 内 DB 查询超时,独立于请求 context
67+
const versionBoundsDBTimeout = 5 * time.Second
6768

6869
// cachedBackendMode Backend Mode cache (in-process, 60s TTL)
6970
type cachedBackendMode struct {
@@ -484,6 +485,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
484485

485486
// Claude Code version check
486487
updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion
488+
updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion
487489

488490
// 分组隔离
489491
updates[SettingKeyAllowUngroupedKeyScheduling] = strconv.FormatBool(settings.AllowUngroupedKeyScheduling)
@@ -494,10 +496,11 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
494496
err = s.settingRepo.SetMultiple(ctx, updates)
495497
if err == nil {
496498
// 先使 inflight singleflight 失效,再刷新缓存,缩小旧值覆盖新值的竞态窗口
497-
minVersionSF.Forget("min_version")
498-
minVersionCache.Store(&cachedMinVersion{
499-
value: settings.MinClaudeCodeVersion,
500-
expiresAt: time.Now().Add(minVersionCacheTTL).UnixNano(),
499+
versionBoundsSF.Forget("version_bounds")
500+
versionBoundsCache.Store(&cachedVersionBounds{
501+
min: settings.MinClaudeCodeVersion,
502+
max: settings.MaxClaudeCodeVersion,
503+
expiresAt: time.Now().Add(versionBoundsCacheTTL).UnixNano(),
501504
})
502505
backendModeSF.Forget("backend_mode")
503506
backendModeCache.Store(&cachedBackendMode{
@@ -760,6 +763,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
760763

761764
// Claude Code version check (default: empty = disabled)
762765
SettingKeyMinClaudeCodeVersion: "",
766+
SettingKeyMaxClaudeCodeVersion: "",
763767

764768
// 分组隔离(默认不允许未分组 Key 调度)
765769
SettingKeyAllowUngroupedKeyScheduling: "false",
@@ -895,6 +899,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
895899

896900
// Claude Code version check
897901
result.MinClaudeCodeVersion = settings[SettingKeyMinClaudeCodeVersion]
902+
result.MaxClaudeCodeVersion = settings[SettingKeyMaxClaudeCodeVersion]
898903

899904
// 分组隔离
900905
result.AllowUngroupedKeyScheduling = settings[SettingKeyAllowUngroupedKeyScheduling] == "true"
@@ -1281,51 +1286,61 @@ func (s *SettingService) IsUngroupedKeySchedulingAllowed(ctx context.Context) bo
12811286
return value == "true"
12821287
}
12831288

1284-
// GetMinClaudeCodeVersion 获取最低 Claude Code 版本号要求
1289+
// GetClaudeCodeVersionBounds 获取 Claude Code 版本号上下限要求
12851290
// 使用进程内 atomic.Value 缓存,60 秒 TTL,热路径零锁开销
12861291
// singleflight 防止缓存过期时 thundering herd
1287-
// 返回空字符串表示不做版本检查
1288-
func (s *SettingService) GetMinClaudeCodeVersion(ctx context.Context) string {
1289-
if cached, ok := minVersionCache.Load().(*cachedMinVersion); ok {
1292+
// 返回空字符串表示不做对应方向的版本检查
1293+
func (s *SettingService) GetClaudeCodeVersionBounds(ctx context.Context) (min, max string) {
1294+
if cached, ok := versionBoundsCache.Load().(*cachedVersionBounds); ok {
12901295
if time.Now().UnixNano() < cached.expiresAt {
1291-
return cached.value
1296+
return cached.min, cached.max
12921297
}
12931298
}
12941299
// singleflight: 同一时刻只有一个 goroutine 查询 DB,其余复用结果
1295-
result, err, _ := minVersionSF.Do("min_version", func() (any, error) {
1300+
type bounds struct{ min, max string }
1301+
result, err, _ := versionBoundsSF.Do("version_bounds", func() (any, error) {
12961302
// 二次检查,避免排队的 goroutine 重复查询
1297-
if cached, ok := minVersionCache.Load().(*cachedMinVersion); ok {
1303+
if cached, ok := versionBoundsCache.Load().(*cachedVersionBounds); ok {
12981304
if time.Now().UnixNano() < cached.expiresAt {
1299-
return cached.value, nil
1305+
return bounds{cached.min, cached.max}, nil
13001306
}
13011307
}
13021308
// 使用独立 context:断开请求取消链,避免客户端断连导致空值被长期缓存
1303-
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), minVersionDBTimeout)
1309+
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), versionBoundsDBTimeout)
13041310
defer cancel()
1305-
value, err := s.settingRepo.GetValue(dbCtx, SettingKeyMinClaudeCodeVersion)
1311+
values, err := s.settingRepo.GetMultiple(dbCtx, []string{
1312+
SettingKeyMinClaudeCodeVersion,
1313+
SettingKeyMaxClaudeCodeVersion,
1314+
})
13061315
if err != nil {
13071316
// fail-open: DB 错误时不阻塞请求,但记录日志并使用短 TTL 快速重试
1308-
slog.Warn("failed to get min claude code version setting, skipping version check", "error", err)
1309-
minVersionCache.Store(&cachedMinVersion{
1310-
value: "",
1311-
expiresAt: time.Now().Add(minVersionErrorTTL).UnixNano(),
1317+
slog.Warn("failed to get claude code version bounds setting, skipping version check", "error", err)
1318+
versionBoundsCache.Store(&cachedVersionBounds{
1319+
min: "",
1320+
max: "",
1321+
expiresAt: time.Now().Add(versionBoundsErrorTTL).UnixNano(),
13121322
})
1313-
return "", nil
1323+
return bounds{"", ""}, nil
13141324
}
1315-
minVersionCache.Store(&cachedMinVersion{
1316-
value: value,
1317-
expiresAt: time.Now().Add(minVersionCacheTTL).UnixNano(),
1325+
b := bounds{
1326+
min: values[SettingKeyMinClaudeCodeVersion],
1327+
max: values[SettingKeyMaxClaudeCodeVersion],
1328+
}
1329+
versionBoundsCache.Store(&cachedVersionBounds{
1330+
min: b.min,
1331+
max: b.max,
1332+
expiresAt: time.Now().Add(versionBoundsCacheTTL).UnixNano(),
13181333
})
1319-
return value, nil
1334+
return b, nil
13201335
})
13211336
if err != nil {
1322-
return ""
1337+
return "", ""
13231338
}
1324-
ver, ok := result.(string)
1339+
b, ok := result.(bounds)
13251340
if !ok {
1326-
return ""
1341+
return "", ""
13271342
}
1328-
return ver
1343+
return b.min, b.max
13291344
}
13301345

13311346
// GetRectifierSettings 获取请求整流器配置

backend/internal/service/settings_view.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type SystemSettings struct {
6767

6868
// Claude Code version check
6969
MinClaudeCodeVersion string
70+
MaxClaudeCodeVersion string
7071

7172
// 分组隔离:允许未分组 Key 调度(默认 false → 403)
7273
AllowUngroupedKeyScheduling bool

frontend/src/api/admin/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export interface SystemSettings {
8181

8282
// Claude Code version check
8383
min_claude_code_version: string
84+
max_claude_code_version: string
8485

8586
// 分组隔离
8687
allow_ungrouped_key_scheduling: boolean
@@ -137,6 +138,7 @@ export interface UpdateSettingsRequest {
137138
ops_query_mode_default?: 'auto' | 'raw' | 'preagg' | string
138139
ops_metrics_interval_seconds?: number
139140
min_claude_code_version?: string
141+
max_claude_code_version?: string
140142
allow_ungrouped_key_scheduling?: boolean
141143
}
142144

frontend/src/components/keys/UseKeyModal.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,17 +441,20 @@ function generateAnthropicFiles(baseUrl: string, apiKey: string): FileConfig[] {
441441
case 'unix':
442442
path = 'Terminal'
443443
content = `export ANTHROPIC_BASE_URL="${baseUrl}"
444-
export ANTHROPIC_AUTH_TOKEN="${apiKey}"`
444+
export ANTHROPIC_AUTH_TOKEN="${apiKey}"
445+
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`
445446
break
446447
case 'cmd':
447448
path = 'Command Prompt'
448449
content = `set ANTHROPIC_BASE_URL=${baseUrl}
449-
set ANTHROPIC_AUTH_TOKEN=${apiKey}`
450+
set ANTHROPIC_AUTH_TOKEN=${apiKey}
451+
set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`
450452
break
451453
case 'powershell':
452454
path = 'PowerShell'
453455
content = `$env:ANTHROPIC_BASE_URL="${baseUrl}"
454-
$env:ANTHROPIC_AUTH_TOKEN="${apiKey}"`
456+
$env:ANTHROPIC_AUTH_TOKEN="${apiKey}"
457+
$env:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`
455458
break
456459
default:
457460
path = 'Terminal'
@@ -466,6 +469,7 @@ $env:ANTHROPIC_AUTH_TOKEN="${apiKey}"`
466469
"env": {
467470
"ANTHROPIC_BASE_URL": "${baseUrl}",
468471
"ANTHROPIC_AUTH_TOKEN": "${apiKey}",
472+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
469473
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
470474
}
471475
}`

frontend/src/i18n/locales/en.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4119,7 +4119,11 @@ export default {
41194119
minVersion: 'Minimum Version',
41204120
minVersionPlaceholder: 'e.g. 2.1.63',
41214121
minVersionHint:
4122-
'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.'
4122+
'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.',
4123+
maxVersion: 'Maximum Version',
4124+
maxVersionPlaceholder: 'e.g. 2.5.0',
4125+
maxVersionHint:
4126+
'Reject Claude Code clients above this version (semver format). Leave empty to allow any version.'
41234127
},
41244128
scheduling: {
41254129
title: 'Gateway Scheduling Settings',

frontend/src/i18n/locales/zh.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4283,7 +4283,10 @@ export default {
42834283
description: '控制 Claude Code 客户端访问要求',
42844284
minVersion: '最低版本号',
42854285
minVersionPlaceholder: '例如 2.1.63',
4286-
minVersionHint: '拒绝低于此版本的 Claude Code 客户端请求(semver 格式)。留空则不检查版本。'
4286+
minVersionHint: '拒绝低于此版本的 Claude Code 客户端请求(semver 格式)。留空则不检查版本。',
4287+
maxVersion: '最高版本号',
4288+
maxVersionPlaceholder: '例如 2.5.0',
4289+
maxVersionHint: '拒绝高于此版本的 Claude Code 客户端请求(semver 格式)。留空则不限制最高版本。'
42874290
},
42884291
scheduling: {
42894292
title: '网关调度设置',

0 commit comments

Comments
 (0)