diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 06916917..4cbe5188 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -128,6 +128,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { BackendModeEnabled: settings.BackendModeEnabled, EnableFingerprintUnification: settings.EnableFingerprintUnification, EnableMetadataPassthrough: settings.EnableMetadataPassthrough, + EnableCCHSigning: settings.EnableCCHSigning, }) } @@ -211,6 +212,7 @@ type UpdateSettingsRequest struct { // Gateway forwarding behavior EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"` EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"` + EnableCCHSigning *bool `json:"enable_cch_signing"` } // UpdateSettings 更新系统设置 @@ -614,6 +616,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } return previousSettings.EnableMetadataPassthrough }(), + EnableCCHSigning: func() bool { + if req.EnableCCHSigning != nil { + return *req.EnableCCHSigning + } + return previousSettings.EnableCCHSigning + }(), } if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil { @@ -693,6 +701,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { BackendModeEnabled: updatedSettings.BackendModeEnabled, EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification, EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough, + EnableCCHSigning: updatedSettings.EnableCCHSigning, }) } @@ -871,6 +880,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if before.EnableMetadataPassthrough != after.EnableMetadataPassthrough { changed = append(changed, "enable_metadata_passthrough") } + if before.EnableCCHSigning != after.EnableCCHSigning { + changed = append(changed, "enable_cch_signing") + } return changed } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index aecbf0c8..73707f79 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -97,6 +97,7 @@ type SystemSettings struct { // Gateway forwarding behavior EnableFingerprintUnification bool `json:"enable_fingerprint_unification"` EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"` + EnableCCHSigning bool `json:"enable_cch_signing"` } type DefaultSubscriptionSetting struct { diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index 52df52d6..92be3e06 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -218,6 +218,8 @@ const ( SettingKeyEnableFingerprintUnification = "enable_fingerprint_unification" // SettingKeyEnableMetadataPassthrough 是否透传客户端原始 metadata.user_id(默认 false) SettingKeyEnableMetadataPassthrough = "enable_metadata_passthrough" + // SettingKeyEnableCCHSigning 是否对 billing header 中的 cch 进行 xxHash64 签名(默认 false) + SettingKeyEnableCCHSigning = "enable_cch_signing" ) // AdminAPIKeyPrefix is the prefix for admin API keys (distinct from user "sk-" keys). diff --git a/backend/internal/service/gateway_billing_header.go b/backend/internal/service/gateway_billing_header.go new file mode 100644 index 00000000..2102e534 --- /dev/null +++ b/backend/internal/service/gateway_billing_header.go @@ -0,0 +1,73 @@ +package service + +import ( + "fmt" + "regexp" + "strings" + + "github.com/cespare/xxhash/v2" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// ccVersionInBillingRe matches the semver part of cc_version (X.Y.Z), preserving +// the trailing message-derived suffix (e.g. ".c02") if present. +var ccVersionInBillingRe = regexp.MustCompile(`cc_version=\d+\.\d+\.\d+`) + +// cchPlaceholderRe matches the cch=00000 placeholder in billing header text, +// scoped to x-anthropic-billing-header to avoid touching user content. +var cchPlaceholderRe = regexp.MustCompile(`(x-anthropic-billing-header:[^"]*?\bcch=)(00000)(;)`) + +const cchSeed uint64 = 0x6E52736AC806831E + +// syncBillingHeaderVersion rewrites cc_version in x-anthropic-billing-header +// system text blocks to match the version extracted from userAgent. +// Only touches system array blocks whose text starts with "x-anthropic-billing-header". +func syncBillingHeaderVersion(body []byte, userAgent string) []byte { + version := ExtractCLIVersion(userAgent) + if version == "" { + return body + } + + systemResult := gjson.GetBytes(body, "system") + if !systemResult.Exists() || !systemResult.IsArray() { + return body + } + + replacement := "cc_version=" + version + idx := 0 + systemResult.ForEach(func(_, item gjson.Result) bool { + text := item.Get("text") + if text.Exists() && text.Type == gjson.String && + strings.HasPrefix(text.String(), "x-anthropic-billing-header") { + newText := ccVersionInBillingRe.ReplaceAllString(text.String(), replacement) + if newText != text.String() { + if updated, err := sjson.SetBytes(body, fmt.Sprintf("system.%d.text", idx), newText); err == nil { + body = updated + } + } + } + idx++ + return true + }) + + return body +} + +// signBillingHeaderCCH computes the xxHash64-based CCH signature for the request +// body and replaces the cch=00000 placeholder with the computed 5-hex-char hash. +// The body must contain the placeholder when this function is called. +func signBillingHeaderCCH(body []byte) []byte { + if !cchPlaceholderRe.Match(body) { + return body + } + cch := fmt.Sprintf("%05x", xxHash64Seeded(body, cchSeed)&0xFFFFF) + return cchPlaceholderRe.ReplaceAll(body, []byte("${1}"+cch+"${3}")) +} + +// xxHash64Seeded computes xxHash64 of data with a custom seed. +func xxHash64Seeded(data []byte, seed uint64) uint64 { + d := xxhash.NewWithSeed(seed) + d.Write(data) + return d.Sum64() +} diff --git a/backend/internal/service/gateway_billing_header_test.go b/backend/internal/service/gateway_billing_header_test.go new file mode 100644 index 00000000..ffc4091c --- /dev/null +++ b/backend/internal/service/gateway_billing_header_test.go @@ -0,0 +1,165 @@ +package service + +import ( + "fmt" + "testing" + + "github.com/cespare/xxhash/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestSyncBillingHeaderVersion(t *testing.T) { + tests := []struct { + name string + body string + userAgent string + wantSub string // substring expected in result + unchanged bool // expect body to remain the same + }{ + { + name: "replaces cc_version preserving message-derived suffix", + body: `{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.81.df2; cc_entrypoint=cli; cch=00000;"},{"type":"text","text":"You are Claude Code.","cache_control":{"type":"ephemeral"}}],"messages":[]}`, + userAgent: "claude-cli/2.1.22 (external, cli)", + wantSub: "cc_version=2.1.22.df2", + }, + { + name: "no billing header in system", + body: `{"system":[{"type":"text","text":"You are Claude Code."}],"messages":[]}`, + userAgent: "claude-cli/2.1.22", + unchanged: true, + }, + { + name: "no system field", + body: `{"messages":[]}`, + userAgent: "claude-cli/2.1.22", + unchanged: true, + }, + { + name: "user-agent without version", + body: `{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.81; cc_entrypoint=cli; cch=00000;"}],"messages":[]}`, + userAgent: "Mozilla/5.0", + unchanged: true, + }, + { + name: "empty user-agent", + body: `{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.81; cc_entrypoint=cli; cch=00000;"}],"messages":[]}`, + userAgent: "", + unchanged: true, + }, + { + name: "version already matches", + body: `{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.22; cc_entrypoint=cli; cch=00000;"}],"messages":[]}`, + userAgent: "claude-cli/2.1.22", + unchanged: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := syncBillingHeaderVersion([]byte(tt.body), tt.userAgent) + if tt.unchanged { + assert.Equal(t, tt.body, string(result), "body should remain unchanged") + } else { + assert.Contains(t, string(result), tt.wantSub) + // Ensure old semver is gone + assert.NotContains(t, string(result), "cc_version=2.1.81") + } + }) + } +} + +func TestSignBillingHeaderCCH(t *testing.T) { + t.Run("replaces placeholder with hash", func(t *testing.T) { + body := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.63.a43; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) + result := signBillingHeaderCCH(body) + + // Should not have the placeholder anymore + assert.NotContains(t, string(result), "cch=00000") + + // Should have a 5 hex-char cch value + billingText := gjson.GetBytes(result, "system.0.text").String() + require.Contains(t, billingText, "cch=") + assert.Regexp(t, `cch=[0-9a-f]{5};`, billingText) + }) + + t.Run("no placeholder - body unchanged", func(t *testing.T) { + body := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.63; cc_entrypoint=cli; cch=abcde;"}],"messages":[]}`) + result := signBillingHeaderCCH(body) + assert.Equal(t, string(body), string(result)) + }) + + t.Run("no billing header - body unchanged", func(t *testing.T) { + body := []byte(`{"system":[{"type":"text","text":"You are Claude Code."}],"messages":[]}`) + result := signBillingHeaderCCH(body) + assert.Equal(t, string(body), string(result)) + }) + + t.Run("cch=00000 in user content is not touched", func(t *testing.T) { + body := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.63; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":[{"type":"text","text":"keep literal cch=00000 in this message"}]}]}`) + result := signBillingHeaderCCH(body) + + // Billing header should be signed + billingText := gjson.GetBytes(result, "system.0.text").String() + assert.NotContains(t, billingText, "cch=00000") + + // User message should keep its literal cch=00000 + userText := gjson.GetBytes(result, "messages.0.content.0.text").String() + assert.Contains(t, userText, "cch=00000") + }) + + t.Run("signing is deterministic", func(t *testing.T) { + body := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.63; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":"hi"}]}`) + r1 := signBillingHeaderCCH(body) + body2 := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.63; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":"hi"}]}`) + r2 := signBillingHeaderCCH(body2) + assert.Equal(t, string(r1), string(r2)) + }) + + t.Run("matches reference algorithm", func(t *testing.T) { + // Verify: signBillingHeaderCCH(body) produces cch = xxHash64(body_with_placeholder, seed) & 0xFFFFF + body := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.63.a43; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) + expectedCCH := fmt.Sprintf("%05x", xxHash64Seeded(body, cchSeed)&0xFFFFF) + + result := signBillingHeaderCCH(body) + billingText := gjson.GetBytes(result, "system.0.text").String() + assert.Contains(t, billingText, "cch="+expectedCCH+";") + }) +} + +func TestXXHash64Seeded(t *testing.T) { + t.Run("matches cespare/xxhash for seed 0", func(t *testing.T) { + inputs := []string{"", "a", "hello world", "The quick brown fox jumps over the lazy dog"} + for _, s := range inputs { + data := []byte(s) + expected := xxhash.Sum64(data) + got := xxHash64Seeded(data, 0) + assert.Equal(t, expected, got, "mismatch for input %q", s) + } + }) + + t.Run("large input matches cespare", func(t *testing.T) { + data := make([]byte, 256) + for i := range data { + data[i] = byte(i) + } + expected := xxhash.Sum64(data) + got := xxHash64Seeded(data, 0) + assert.Equal(t, expected, got) + }) + + t.Run("deterministic with custom seed", func(t *testing.T) { + data := []byte("hello world") + h1 := xxHash64Seeded(data, cchSeed) + h2 := xxHash64Seeded(data, cchSeed) + assert.Equal(t, h1, h2) + }) + + t.Run("different seeds produce different results", func(t *testing.T) { + data := []byte("test data for hashing") + h1 := xxHash64Seeded(data, 0) + h2 := xxHash64Seeded(data, cchSeed) + assert.NotEqual(t, h1, h2) + }) +} diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index fbbebc21..a4733649 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -4002,7 +4002,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header) if err == nil && fp != nil { // metadata 透传开启时跳过 metadata 注入 - _, mimicMPT := s.settingService.GetGatewayForwardingSettings(ctx) + _, mimicMPT, _ := s.settingService.GetGatewayForwardingSettings(ctx) if !mimicMPT { if metadataUserID := s.buildOAuthMetadataUserID(parsed, account, fp); metadataUserID != "" { normalizeOpts.injectMetadata = true @@ -5548,9 +5548,9 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex // OAuth账号:应用统一指纹和metadata重写(受设置开关控制) var fingerprint *Fingerprint - enableFP, enableMPT := true, false + enableFP, enableMPT, enableCCH := true, false, false if s.settingService != nil { - enableFP, enableMPT = s.settingService.GetGatewayForwardingSettings(ctx) + enableFP, enableMPT, enableCCH = s.settingService.GetGatewayForwardingSettings(ctx) } if account.IsOAuth() && s.identityService != nil { // 1. 获取或创建指纹(包含随机生成的ClientID) @@ -5577,6 +5577,15 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex } } + // 同步 billing header cc_version 与实际发送的 User-Agent 版本 + if fingerprint != nil { + body = syncBillingHeaderVersion(body, fingerprint.UserAgent) + } + // CCH 签名:将 cch=00000 占位符替换为 xxHash64 签名(需在所有 body 修改之后) + if enableCCH { + body = signBillingHeaderCCH(body) + } + req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(body)) if err != nil { return nil, err @@ -8461,9 +8470,9 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con // OAuth 账号:应用统一指纹和重写 userID(受设置开关控制) // 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值 - ctEnableFP, ctEnableMPT := true, false + ctEnableFP, ctEnableMPT, ctEnableCCH := true, false, false if s.settingService != nil { - ctEnableFP, ctEnableMPT = s.settingService.GetGatewayForwardingSettings(ctx) + ctEnableFP, ctEnableMPT, ctEnableCCH = s.settingService.GetGatewayForwardingSettings(ctx) } var ctFingerprint *Fingerprint if account.IsOAuth() && s.identityService != nil { @@ -8481,6 +8490,14 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con } } + // 同步 billing header cc_version 与实际发送的 User-Agent 版本 + if ctFingerprint != nil && ctEnableFP { + body = syncBillingHeaderVersion(body, ctFingerprint.UserAgent) + } + if ctEnableCCH { + body = signBillingHeaderCCH(body) + } + req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(body)) if err != nil { return nil, err diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index b7145121..7d0ef5bd 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -81,6 +81,7 @@ const backendModeDBTimeout = 5 * time.Second type cachedGatewayForwardingSettings struct { fingerprintUnification bool metadataPassthrough bool + cchSigning bool expiresAt int64 // unix nano } @@ -514,6 +515,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet // Gateway forwarding behavior updates[SettingKeyEnableFingerprintUnification] = strconv.FormatBool(settings.EnableFingerprintUnification) updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough) + updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning) err = s.settingRepo.SetMultiple(ctx, updates) if err == nil { @@ -533,6 +535,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{ fingerprintUnification: settings.EnableFingerprintUnification, metadataPassthrough: settings.EnableMetadataPassthrough, + cchSigning: settings.EnableCCHSigning, expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(), }) if s.onUpdate != nil { @@ -639,20 +642,20 @@ func (s *SettingService) IsBackendModeEnabled(ctx context.Context) bool { // GetGatewayForwardingSettings returns cached gateway forwarding settings. // Uses in-process atomic.Value cache with 60s TTL, zero-lock hot path. -// Returns (fingerprintUnification, metadataPassthrough). -func (s *SettingService) GetGatewayForwardingSettings(ctx context.Context) (fingerprintUnification, metadataPassthrough bool) { +// Returns (fingerprintUnification, metadataPassthrough, cchSigning). +func (s *SettingService) GetGatewayForwardingSettings(ctx context.Context) (fingerprintUnification, metadataPassthrough, cchSigning bool) { if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil { if time.Now().UnixNano() < cached.expiresAt { - return cached.fingerprintUnification, cached.metadataPassthrough + return cached.fingerprintUnification, cached.metadataPassthrough, cached.cchSigning } } type gwfResult struct { - fp, mp bool + fp, mp, cch bool } val, _, _ := gatewayForwardingSF.Do("gateway_forwarding", func() (any, error) { if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil { if time.Now().UnixNano() < cached.expiresAt { - return gwfResult{cached.fingerprintUnification, cached.metadataPassthrough}, nil + return gwfResult{cached.fingerprintUnification, cached.metadataPassthrough, cached.cchSigning}, nil } } dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) @@ -660,32 +663,36 @@ func (s *SettingService) GetGatewayForwardingSettings(ctx context.Context) (fing values, err := s.settingRepo.GetMultiple(dbCtx, []string{ SettingKeyEnableFingerprintUnification, SettingKeyEnableMetadataPassthrough, + SettingKeyEnableCCHSigning, }) if err != nil { slog.Warn("failed to get gateway forwarding settings", "error", err) gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{ fingerprintUnification: true, metadataPassthrough: false, + cchSigning: false, expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(), }) - return gwfResult{true, false}, nil + return gwfResult{true, false, false}, nil } fp := true if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" { fp = v == "true" } mp := values[SettingKeyEnableMetadataPassthrough] == "true" + cch := values[SettingKeyEnableCCHSigning] == "true" gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{ fingerprintUnification: fp, metadataPassthrough: mp, + cchSigning: cch, expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(), }) - return gwfResult{fp, mp}, nil + return gwfResult{fp, mp, cch}, nil }) if r, ok := val.(gwfResult); ok { - return r.fp, r.mp + return r.fp, r.mp, r.cch } - return true, false // fail-open defaults + return true, false, false // fail-open defaults } // IsEmailVerifyEnabled 检查是否开启邮件验证 @@ -983,13 +990,14 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin // 分组隔离 result.AllowUngroupedKeyScheduling = settings[SettingKeyAllowUngroupedKeyScheduling] == "true" - // Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false) + // Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false, cch_signing=false) if v, ok := settings[SettingKeyEnableFingerprintUnification]; ok && v != "" { result.EnableFingerprintUnification = v == "true" } else { result.EnableFingerprintUnification = true // default: enabled (current behavior) } result.EnableMetadataPassthrough = settings[SettingKeyEnableMetadataPassthrough] == "true" + result.EnableCCHSigning = settings[SettingKeyEnableCCHSigning] == "true" return result } diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 473d7297..fedb3f2f 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -78,6 +78,7 @@ type SystemSettings struct { // Gateway forwarding behavior EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true) EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false) + EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false) } type DefaultSubscriptionSetting struct { diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 013f2dfb..b7ee6be5 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -89,6 +89,7 @@ export interface SystemSettings { // Gateway forwarding behavior enable_fingerprint_unification: boolean enable_metadata_passthrough: boolean + enable_cch_signing: boolean } export interface UpdateSettingsRequest { @@ -146,6 +147,7 @@ export interface UpdateSettingsRequest { allow_ungrouped_key_scheduling?: boolean enable_fingerprint_unification?: boolean enable_metadata_passthrough?: boolean + enable_cch_signing?: boolean } /** diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index fc9297fd..d3b16d4a 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4268,6 +4268,8 @@ export default { fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.', metadataPassthrough: 'Metadata Passthrough', metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.', + cchSigning: 'CCH Signing', + cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.', }, site: { title: 'Site Settings', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 57bfefdc..fcaaf5ab 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4431,6 +4431,8 @@ export default { fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。', metadataPassthrough: 'Metadata 透传', metadataPassthroughHint: '透传客户端原始 metadata.user_id,不进行重写。可能提高上游缓存命中率。', + cchSigning: 'CCH 签名', + cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。', }, site: { title: '站点设置', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 9ae40aeb..f43140ab 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1376,6 +1376,19 @@ + + +
+
+ +

+ {{ t('admin.settings.gatewayForwarding.cchSigningHint') }} +

+
+ +
@@ -2248,7 +2261,8 @@ const form = reactive({ allow_ungrouped_key_scheduling: false, // Gateway forwarding behavior enable_fingerprint_unification: true, - enable_metadata_passthrough: false + enable_metadata_passthrough: false, + enable_cch_signing: false }) const defaultSubscriptionGroupOptions = computed(() => @@ -2556,7 +2570,8 @@ async function saveSettings() { max_claude_code_version: form.max_claude_code_version, allow_ungrouped_key_scheduling: form.allow_ungrouped_key_scheduling, enable_fingerprint_unification: form.enable_fingerprint_unification, - enable_metadata_passthrough: form.enable_metadata_passthrough + enable_metadata_passthrough: form.enable_metadata_passthrough, + enable_cch_signing: form.enable_cch_signing } const updated = await adminAPI.settings.updateSettings(payload) Object.assign(form, updated)