feat(gateway): add billing attribution block with cc_version fingerprint
Real Claude Code CLI always sends a 2-block system array:
[0] {"type":"text", "text":"x-anthropic-billing-header: cc_version=X.Y.Z.{fp}; cc_entrypoint=cli; cch=00000;"}
[1] {"type":"text", "text":"You are Claude Code...", "cache_control":{...}}
Before this commit, sub2api's mimicry path only produced block [1].
The missing billing block is one of the primary third-party detection
signals Anthropic uses for Claude-Code-scoped OAuth tokens.
New file gateway_billing_block.go ports the fingerprint algorithm
(byte-for-byte from Parrot cc_mimicry.py:compute_fingerprint):
pick chars at positions [4,7,20] of the first user text, then
`sha256(SALT + chars + cc_version)[:3]`.
- claude/constants.go: CLICurrentVersion = "2.1.92" (must match UA)
- gateway_billing_block.go: computeClaudeCodeFingerprint +
buildBillingAttributionBlockJSON + extractFirstUserText
- gateway_service.go: rewriteSystemForNonClaudeCode now emits both
blocks in order; cch=00000 is filled in later by
signBillingHeaderCCH in buildUpstreamRequest.
Downstream compat note: syncBillingHeaderVersion's regex
`cc_version=\d+\.\d+\.\d+` only matches the semver triple,
leaving the `.{fp}` suffix intact when rewriting in buildUpstreamRequest.
This commit is contained in:
@@ -62,6 +62,11 @@ const APIKeyHaikuBetaHeader = BetaInterleavedThinking
|
||||
// 客户端缺省时统一使用 5m",这样既不浪费 1h 缓存额度,也保留客户端自定义能力。
|
||||
const DefaultCacheControlTTL = "5m"
|
||||
|
||||
// CLICurrentVersion 是 sub2api 当前对外伪装的 Claude Code CLI 版本号(三段 semver)。
|
||||
// 用于 billing attribution block 中的 cc_version=X.Y.Z.{fp} 前缀以及 fingerprint 计算。
|
||||
// 必须与 DefaultHeaders["User-Agent"] 中的版本号严格一致;不一致会被 Anthropic 判第三方。
|
||||
const CLICurrentVersion = "2.1.92"
|
||||
|
||||
// FullClaudeCodeMimicryBetas 返回最"像"真实 Claude Code CLI 的完整 beta 列表,
|
||||
// 用于 OAuth 账号伪装成 Claude Code 时使用。
|
||||
// 顺序与真实 CLI 抓包一致。
|
||||
|
||||
98
backend/internal/service/gateway_billing_block.go
Normal file
98
backend/internal/service/gateway_billing_block.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// fingerprintSalt 是计算 cc_version 后缀指纹的盐值。
|
||||
//
|
||||
// 来源:与 Parrot src/transform/cc_mimicry.py 的 FINGERPRINT_SALT 完全一致;
|
||||
// 这是真实 Claude Code CLI 抓包推导出的常量,改动会导致 fp 与 CLI 不一致,
|
||||
// 进一步触发 Anthropic 的第三方检测。
|
||||
const fingerprintSalt = "59cf53e54c78"
|
||||
|
||||
// computeClaudeCodeFingerprint 复刻真实 Claude Code CLI 的 cc_version 指纹算法:
|
||||
//
|
||||
// 1. 取 messages 中第一条 role=user 的纯文本(首块 text)
|
||||
// 2. 取该文本的第 4、7、20 字符(不足以 '0' 补齐)
|
||||
// 3. SHA256(SALT + chars + cc_version) 取 hex 前 3 字符
|
||||
//
|
||||
// 算法来自 Parrot src/transform/cc_mimicry.py:compute_fingerprint,与官方 CLI 字节对齐。
|
||||
// 任何偏差都会导致 cc_version=X.Y.Z.{fp} 在上游侧与真实 CLI 不一致。
|
||||
func computeClaudeCodeFingerprint(body []byte, version string) string {
|
||||
firstText := extractFirstUserText(body)
|
||||
indices := []int{4, 7, 20}
|
||||
chars := make([]byte, 0, 3)
|
||||
for _, i := range indices {
|
||||
if i < len(firstText) {
|
||||
chars = append(chars, firstText[i])
|
||||
} else {
|
||||
chars = append(chars, '0')
|
||||
}
|
||||
}
|
||||
sum := sha256.Sum256([]byte(fingerprintSalt + string(chars) + version))
|
||||
return hex.EncodeToString(sum[:])[:3]
|
||||
}
|
||||
|
||||
// extractFirstUserText 提取 messages 中第一条 user 消息的首段 text 内容。
|
||||
// 兼容 string 和 []block 两种 content 格式。
|
||||
func extractFirstUserText(body []byte) string {
|
||||
messages := gjson.GetBytes(body, "messages")
|
||||
if !messages.IsArray() {
|
||||
return ""
|
||||
}
|
||||
first := ""
|
||||
messages.ForEach(func(_, msg gjson.Result) bool {
|
||||
if msg.Get("role").String() != "user" {
|
||||
return true
|
||||
}
|
||||
content := msg.Get("content")
|
||||
if content.Type == gjson.String {
|
||||
first = content.String()
|
||||
return false
|
||||
}
|
||||
if content.IsArray() {
|
||||
content.ForEach(func(_, block gjson.Result) bool {
|
||||
if block.Get("type").String() == "text" {
|
||||
first = block.Get("text").String()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
return first
|
||||
}
|
||||
|
||||
// buildBillingAttributionBlockJSON 构造 system 数组的 billing attribution block。
|
||||
//
|
||||
// 形态严格对齐真实 Claude Code CLI:
|
||||
//
|
||||
// {"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.92.{fp}; cc_entrypoint=cli; cch=00000;"}
|
||||
//
|
||||
// cch=00000 是签名占位符,由 signBillingHeaderCCH 在 buildUpstreamRequest 阶段
|
||||
// 替换为基于完整 body 的 xxhash64 5 位十六进制摘要。
|
||||
//
|
||||
// 此 block 不带 cache_control(与真实 CLI 一致;cache breakpoint 由后续的
|
||||
// Claude Code prompt block 承担)。
|
||||
func buildBillingAttributionBlockJSON(body []byte, cliVersion string) ([]byte, error) {
|
||||
if cliVersion == "" {
|
||||
return nil, fmt.Errorf("cliVersion required")
|
||||
}
|
||||
fp := computeClaudeCodeFingerprint(body, cliVersion)
|
||||
text := fmt.Sprintf(
|
||||
"x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=cli; cch=00000;",
|
||||
cliVersion, fp,
|
||||
)
|
||||
return json.Marshal(map[string]string{
|
||||
"type": "text",
|
||||
"text": text,
|
||||
})
|
||||
}
|
||||
@@ -3853,17 +3853,20 @@ func rewriteSystemForNonClaudeCode(body []byte, system any) []byte {
|
||||
originalSystemText = strings.Join(parts, "\n\n")
|
||||
}
|
||||
|
||||
// 2. 将 system 替换为 Claude Code 标准提示词(array 格式,与真实 Claude Code 一致)
|
||||
// 真实 Claude Code 始终以 [{type: "text", text: "...", cache_control: {type: "ephemeral"}}] 发送 system。
|
||||
// 使用 string 格式会被 Anthropic 检测为第三方应用。
|
||||
claudeCodeSystemBlock := []map[string]any{
|
||||
{
|
||||
"type": "text",
|
||||
"text": claudeCodeSystemPrompt,
|
||||
"cache_control": map[string]string{"type": "ephemeral", "ttl": claude.DefaultCacheControlTTL},
|
||||
},
|
||||
// 2. 构造 system 数组,对齐真实 Claude Code CLI 的 2-block 形态:
|
||||
// [0] billing attribution block(cc_version={cliVer}.{fp}; cc_entrypoint=cli; cch=00000;)
|
||||
// [1] "You are Claude Code..." prompt block(带 cache_control 作为稳定缓存断点)
|
||||
//
|
||||
// billing block 的 cch=00000 是占位符,会被 buildUpstreamRequest 里的
|
||||
// signBillingHeaderCCH 替换成 xxhash64 签名。缺失 billing block 的系统 payload
|
||||
// 是 Anthropic 判定第三方的关键信号之一(真实 CLI 每个请求都带)。
|
||||
billingBlock, billingErr := buildBillingAttributionBlockJSON(body, claude.CLICurrentVersion)
|
||||
ccPromptBlock, ccErr := marshalAnthropicSystemTextBlock(claudeCodeSystemPrompt, true)
|
||||
if billingErr != nil || ccErr != nil {
|
||||
logger.LegacyPrintf("service.gateway", "Warning: failed to build system blocks (billing=%v, cc=%v)", billingErr, ccErr)
|
||||
return body
|
||||
}
|
||||
out, ok := setJSONValueBytes(body, "system", claudeCodeSystemBlock)
|
||||
out, ok := setJSONRawBytes(body, "system", buildJSONArrayRaw([][]byte{billingBlock, ccPromptBlock}))
|
||||
if !ok {
|
||||
logger.LegacyPrintf("service.gateway", "Warning: failed to set Claude Code system prompt")
|
||||
return body
|
||||
|
||||
Reference in New Issue
Block a user