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') }} +
+