feat(claude-mimic): upgrade Claude Code mimicry to 2.1.145 via bundle abstraction
反编译本地 Claude Code 2.1.145 二进制 (Bun 1.3.14 打包, @anthropic-ai/sdk@0.94.0 嵌入) 提取真实指纹,系统性升级 mimicry。 核心改动: - 新增 ClaudeCodeBundle struct 作为单一事实源,DefaultBundle 描述当前 伪装目标的完整快照 (CLIVersion/SDKVersion/RuntimeVersion/OS/Arch) - DefaultCLIVersion/DefaultStainlessPackageVersion/CLICurrentVersion/ DefaultHeaders 全部派生自 DefaultBundle,消除三处 (2.1.92, 2.1.104, 0.70.0, 0.81.0) 版本分裂 - CLI 版本 2.1.92/2.1.104 -> 2.1.145 - SDK 版本 0.70.0/0.81.0 -> 0.94.0 - 新增 12 个 2.1.145 反编译确认的 anthropic-beta token: advanced-tool-use, tool-search-tool, mcp-servers, mcp-client, mid-conversation-system, afk-mode, cache-diagnosis, context-hint, environments, managed-agents, skills, compact - FullClaudeCodeMimicryBetas() 从 7 个 token 升级到 21 个 ordered list - 修正 BetaTokenEfficientTools 错日期 (2026-03-28 -> 2025-02-19) - 从默认 beta header 移除已 GA 的 BetaFineGrainedToolStreaming / BetaTokenEfficientTools (常量保留供客户端显式 merge) - claudemask.RequiredNodeHeaders 加 X-Claude-Code-Session-Id 强制 新增 ensureClaudeCodeSessionID helper (claude_code_session_id.go): - 真实 CLI 在 SDK 内强制 X-Claude-Code-Session-Id:y_(),缺失被判第三方 - OAuth mimic 路径: metadata.user_id 派生 -> canonical UUID 写入 -> 兜底 uuid.NewString() - API key passthrough 路径: 不从 body 派生,保护客户端原始语义 - 所有路径均对客户端传入的非法 UUID 执行删除 (避免恶意值上游透传) - 所有写入 header 的 session-id 都通过 uuid.Parse 校验 测试: - 新增 14 个 ensureClaudeCodeSessionID 单元测试,含恶意 UUID 注入拒绝 + API key 路径隔离 + canonical 形式校验 - 新增 3 个 bundle 派生一致性测试 - mask_test 加 session-id 缺失校验 case - 老 UA 断言 2.1.104 -> 2.1.145 不在范围: - TLS 指纹 (utls 已处理) - Bun.hash vs xxHash64 算法验证 (需 golden vectors,独立项目) References: - VERSION:2.1.145 BUILD_TIME:2026-05-19T01:36:35Z GIT_SHA:daa4c3755d45ab0cf97bb41db8c03bd2dfd2ff5f
This commit is contained in:
parent
158785bfc9
commit
0fefedf9cd
@ -8,16 +8,49 @@ import (
|
||||
)
|
||||
|
||||
// Claude Code 客户端相关常量
|
||||
//
|
||||
// 反编译 @anthropic-ai/claude-code 2.1.145(Bun 1.3.14 打包)确认的真实指纹:
|
||||
// VERSION = 2.1.145
|
||||
// SDK 内嵌 = @anthropic-ai/sdk@0.94.0
|
||||
// BUILD_TIME = 2026-05-19T01:36:35Z
|
||||
// GIT_SHA = daa4c3755d45ab0cf97bb41db8c03bd2dfd2ff5f
|
||||
// X-Stainless-Runtime = "node" (Bun 经 globalThis.process 检测为 node)
|
||||
// X-Stainless-Runtime-Version = globalThis.process.version (Bun Node-compat 字符串)
|
||||
|
||||
// DefaultCLIVersion 是当前模拟的 Claude CLI 版本
|
||||
// ClaudeCodeBundle 描述当前 sub2api 对外伪装的 Claude Code CLI 构建快照。
|
||||
// 每次升级目标 CLI 版本时,更新 DefaultBundle 即可。
|
||||
type ClaudeCodeBundle struct {
|
||||
CLIVersion string
|
||||
SDKVersion string
|
||||
BuildDate string
|
||||
GitSHA string
|
||||
RuntimeVersion string
|
||||
DefaultOS string
|
||||
DefaultArch string
|
||||
}
|
||||
|
||||
// DefaultBundle 是 sub2api 对外伪装的 Claude Code CLI 构建快照。
|
||||
// 当 Claude Code 发新版且抓包确认指纹变化时,更新此处常量并同步测试断言。
|
||||
var DefaultBundle = ClaudeCodeBundle{
|
||||
CLIVersion: "2.1.145",
|
||||
SDKVersion: "0.94.0",
|
||||
BuildDate: "2026-05-19T01:36:35Z",
|
||||
GitSHA: "daa4c3755d45ab0cf97bb41db8c03bd2dfd2ff5f",
|
||||
RuntimeVersion: "v24.3.0",
|
||||
DefaultOS: "MacOS",
|
||||
DefaultArch: "arm64",
|
||||
}
|
||||
|
||||
// 所有 Default* 变量从 DefaultBundle 派生。请勿在此包外直接修改它们,
|
||||
// 改用 ApplyFingerprintOverrides 保持 bundle 与派生变量一致。
|
||||
var (
|
||||
DefaultCLIVersion = "2.1.104"
|
||||
DefaultCLIVersion = DefaultBundle.CLIVersion
|
||||
DefaultStainlessLang = "js"
|
||||
DefaultStainlessPackageVersion = "0.81.0"
|
||||
DefaultStainlessOS = "MacOS"
|
||||
DefaultStainlessArch = "arm64"
|
||||
DefaultStainlessPackageVersion = DefaultBundle.SDKVersion
|
||||
DefaultStainlessOS = DefaultBundle.DefaultOS
|
||||
DefaultStainlessArch = DefaultBundle.DefaultArch
|
||||
DefaultStainlessRuntime = "node"
|
||||
DefaultStainlessRuntimeVersion = "v24.3.0"
|
||||
DefaultStainlessRuntimeVersion = DefaultBundle.RuntimeVersion
|
||||
DefaultStainlessRetryCount = "0"
|
||||
DefaultStainlessTimeout = "600"
|
||||
DefaultXApp = "cli"
|
||||
@ -211,30 +244,46 @@ func AttributionHeaderDisabled() bool {
|
||||
|
||||
// Beta header 常量
|
||||
//
|
||||
// 这里的常量对齐真实 Claude Code CLI 的最新流量(截至 2026-04)。
|
||||
// 选型参考:与 Parrot (src/transform/cc_mimicry.py) 的 BETAS 保持一致,
|
||||
// 这里的常量对齐真实 Claude Code CLI 的最新流量(截至 2026-05,2.1.145 抓包)。
|
||||
// 选型来源:反编译 2.1.145 binary 的内部 capability 注册表(JM("name", "beta-X-Y-Z"))。
|
||||
// 原因:Anthropic 上游会基于 anthropic-beta 的完整集合判定请求来源;
|
||||
// 缺少任何"官方 Claude Code 请求才会带"的 beta,都会被降级到第三方额度,
|
||||
// 对应报错:`Third-party apps now draw from your extra usage, not your plan limits.`
|
||||
const (
|
||||
BetaOAuth = "oauth-2025-04-20"
|
||||
BetaClaudeCode = "claude-code-20250219"
|
||||
BetaInterleavedThinking = "interleaved-thinking-2025-05-14"
|
||||
BetaFineGrainedToolStreaming = "fine-grained-tool-streaming-2025-05-14"
|
||||
BetaTokenCounting = "token-counting-2024-11-01"
|
||||
BetaContext1M = "context-1m-2025-08-07"
|
||||
BetaFastMode = "fast-mode-2026-02-01"
|
||||
// 新增(对齐官方 CLI 2.1.9x 以来的流量)
|
||||
BetaOAuth = "oauth-2025-04-20"
|
||||
BetaClaudeCode = "claude-code-20250219"
|
||||
BetaInterleavedThinking = "interleaved-thinking-2025-05-14"
|
||||
BetaTokenCounting = "token-counting-2024-11-01"
|
||||
BetaContext1M = "context-1m-2025-08-07"
|
||||
BetaFastMode = "fast-mode-2026-02-01"
|
||||
BetaPromptCachingScope = "prompt-caching-scope-2026-01-05"
|
||||
BetaEffort = "effort-2025-11-24"
|
||||
BetaRedactThinking = "redact-thinking-2026-02-12"
|
||||
BetaContextManagement = "context-management-2025-06-27"
|
||||
BetaExtendedCacheTTL = "extended-cache-ttl-2025-04-11"
|
||||
BetaTaskBudgets = "task-budgets-2026-03-13"
|
||||
BetaTokenEfficientTools = "token-efficient-tools-2026-03-28"
|
||||
BetaStructuredOutputs = "structured-outputs-2025-12-15"
|
||||
BetaAdvisor = "advisor-tool-2026-03-01"
|
||||
BetaWebSearch = "web-search-2025-03-05"
|
||||
|
||||
// 2026-05 已 GA 的 beta,保留常量供 client 显式传入时 merge 使用,
|
||||
// 但 sub2api 默认不再注入到 OAuth/API-key mimic header 中。
|
||||
BetaFineGrainedToolStreaming = "fine-grained-tool-streaming-2025-05-14"
|
||||
BetaTokenEfficientTools = "token-efficient-tools-2025-02-19" // 修正原 2026-03-28 错日期
|
||||
|
||||
// 2.1.145 反编译新增(对齐官方 CLI 2.1.1xx 以来的流量)
|
||||
BetaAdvancedToolUse = "advanced-tool-use-2025-11-20"
|
||||
BetaToolSearchTool = "tool-search-tool-2025-10-19"
|
||||
BetaMCPServers = "mcp-servers-2025-12-04"
|
||||
BetaMCPClient = "mcp-client-2025-11-20"
|
||||
BetaMidConversationSystem = "mid-conversation-system-2026-04-07"
|
||||
BetaAFKMode = "afk-mode-2026-01-31"
|
||||
BetaCacheDiagnosis = "cache-diagnosis-2026-04-07"
|
||||
BetaContextHint = "context-hint-2026-04-09"
|
||||
BetaEnvironments = "environments-2025-11-01"
|
||||
BetaManagedAgents = "managed-agents-2026-04-01"
|
||||
BetaSkills = "skills-2025-10-02"
|
||||
BetaCompact = "compact-2026-01-12"
|
||||
)
|
||||
|
||||
// DroppedBetas 是转发时需要从 anthropic-beta header 中移除的 beta token 列表。
|
||||
@ -243,7 +292,8 @@ var DroppedBetas = []string{}
|
||||
|
||||
// DefaultBetaHeader Claude Code 客户端默认的 anthropic-beta header(OAuth 账号,不含 context-1m)
|
||||
// 使用 GetOAuthBetaHeader(modelID) 获取含 context-1m 的 model-aware 版本。
|
||||
const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||||
// 已移除 GA 的 BetaFineGrainedToolStreaming。
|
||||
const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||||
|
||||
// MessageBetaHeaderNoTools /v1/messages 在无工具时的 beta header(OAuth,不含 context-1m)
|
||||
//
|
||||
@ -264,13 +314,14 @@ const HaikuBetaHeader = BetaOAuth + "," + BetaInterleavedThinking + "," + BetaEf
|
||||
|
||||
// APIKeyBetaHeader API-key 账号使用的 anthropic-beta header(不含 oauth / context-1m)
|
||||
// 使用 GetAPIKeyBetaHeader(modelID) 获取含 context-1m 的 model-aware 版本。
|
||||
const APIKeyBetaHeader = BetaClaudeCode + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + BetaEffort + "," + BetaPromptCachingScope
|
||||
// 已移除 GA 的 BetaFineGrainedToolStreaming。
|
||||
const APIKeyBetaHeader = BetaClaudeCode + "," + BetaInterleavedThinking + "," + BetaEffort + "," + BetaPromptCachingScope
|
||||
|
||||
// APIKeyHaikuBetaHeader Haiku 模型在 API-key 账号下使用的 anthropic-beta header(不含 oauth / claude-code)
|
||||
const APIKeyHaikuBetaHeader = BetaInterleavedThinking + "," + BetaEffort
|
||||
|
||||
// ModelSupports1M 判断模型是否支持 1M context window。
|
||||
// 与 claude-code-2.1.104 bundle 中 modelSupports1M 逻辑保持一致:
|
||||
// 与 Claude Code CLI bundle 中 modelSupports1M 逻辑保持一致(截至 2.1.145 反编译):
|
||||
//
|
||||
// claude-sonnet-4 系列 和 claude-opus-4-6 支持 1M context。
|
||||
func ModelSupports1M(modelID string) bool {
|
||||
@ -280,21 +331,23 @@ func ModelSupports1M(modelID string) bool {
|
||||
|
||||
// GetOAuthBetaHeader 返回 OAuth 账号的 beta header。
|
||||
// 仅当模型支持 1M context 时才包含 context-1m-2025-08-07。
|
||||
// 已移除 GA 的 BetaFineGrainedToolStreaming。
|
||||
func GetOAuthBetaHeader(modelID string) string {
|
||||
if ModelSupports1M(modelID) {
|
||||
return BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + BetaContext1M + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||||
return BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaContext1M + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||||
}
|
||||
return DefaultBetaHeader
|
||||
}
|
||||
|
||||
// GetAPIKeyBetaHeader 返回 API-key 账号的 beta header。
|
||||
// 仅当模型支持 1M context 时才包含 context-1m-2025-08-07。
|
||||
// 已移除 GA 的 BetaFineGrainedToolStreaming。
|
||||
func GetAPIKeyBetaHeader(modelID string) string {
|
||||
if strings.Contains(strings.ToLower(modelID), "haiku") {
|
||||
return APIKeyHaikuBetaHeader
|
||||
}
|
||||
if ModelSupports1M(modelID) {
|
||||
return BetaClaudeCode + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + BetaContext1M + "," + BetaEffort + "," + BetaPromptCachingScope
|
||||
return BetaClaudeCode + "," + BetaInterleavedThinking + "," + BetaContext1M + "," + BetaEffort + "," + BetaPromptCachingScope
|
||||
}
|
||||
return APIKeyBetaHeader
|
||||
}
|
||||
@ -307,45 +360,62 @@ 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"
|
||||
//
|
||||
// 从 var 改为派生自 DefaultBundle.CLIVersion,避免硬编码漂移。
|
||||
var CLICurrentVersion = DefaultBundle.CLIVersion
|
||||
|
||||
// FullClaudeCodeMimicryBetas 返回最"像"真实 Claude Code CLI 的完整 beta 列表,
|
||||
// FullClaudeCodeMimicryBetas 返回最"像"真实 Claude Code CLI 2.1.145 的完整 beta 列表,
|
||||
// 用于 OAuth 账号伪装成 Claude Code 时使用。
|
||||
// 顺序与真实 CLI 抓包一致。
|
||||
// 顺序基于反编译 2.1.145 binary 中的 capability 注册顺序,与真实 CLI 抓包匹配。
|
||||
//
|
||||
// 使用建议:
|
||||
// - OAuth 账号 + 非 haiku:追加这整份列表,再按需保留 client 带来的 beta。
|
||||
// - OAuth 账号 + haiku:Anthropic 对 haiku 不做 third-party 判定,使用 HaikuBetaHeader 即可。
|
||||
// - API-key 账号:不要使用本函数,参见 APIKeyBetaHeader。
|
||||
// - 不默认加入 redact-thinking,避免上游抹除 thinking 内容;客户端显式传入时由合并逻辑保留。
|
||||
// - 不加入已 GA 的 BetaFineGrainedToolStreaming / BetaTokenEfficientTools;
|
||||
// 这些常量仍保留供客户端显式 merge 使用。
|
||||
func FullClaudeCodeMimicryBetas() []string {
|
||||
return []string{
|
||||
// 核心身份 token
|
||||
BetaClaudeCode,
|
||||
BetaOAuth,
|
||||
// 思考与工具
|
||||
BetaInterleavedThinking,
|
||||
BetaAdvancedToolUse,
|
||||
BetaToolSearchTool,
|
||||
// MCP
|
||||
BetaMCPClient,
|
||||
BetaMCPServers,
|
||||
// 上下文与缓存
|
||||
BetaPromptCachingScope,
|
||||
BetaEffort,
|
||||
BetaContextManagement,
|
||||
BetaExtendedCacheTTL,
|
||||
BetaContextManagement,
|
||||
BetaContextHint,
|
||||
BetaCacheDiagnosis,
|
||||
// 系统提示与会话
|
||||
BetaMidConversationSystem,
|
||||
BetaCompact,
|
||||
// 工作流
|
||||
BetaEffort,
|
||||
BetaTaskBudgets,
|
||||
BetaAFKMode,
|
||||
// 高级特性
|
||||
BetaSkills,
|
||||
BetaManagedAgents,
|
||||
BetaEnvironments,
|
||||
BetaAdvisor,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultHeaders 是 Claude Code 客户端默认请求头。
|
||||
var DefaultHeaders = map[string]string{
|
||||
// Keep these in sync with recent Claude CLI traffic to reduce the chance
|
||||
// that Claude Code-scoped OAuth credentials are rejected as "non-CLI" usage.
|
||||
// 版本参考:对齐 Parrot (src/transform/cc_mimicry.py:49) 的 CLI_USER_AGENT。
|
||||
"User-Agent": "claude-cli/2.1.92 (external, cli)",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Package-Version": "0.70.0",
|
||||
"X-Stainless-OS": "Linux",
|
||||
"X-Stainless-Arch": "arm64",
|
||||
"X-Stainless-Runtime": "node",
|
||||
"X-Stainless-Runtime-Version": "v24.13.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Timeout": "600",
|
||||
"X-App": "cli",
|
||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||
//
|
||||
// 由 init() 在包加载时从 DefaultBundle 派生构造,避免与 DefaultBundle 漂移。
|
||||
// 注:通过 ApplyFingerprintOverrides 更新指纹后,本变量会被重新构造。
|
||||
var DefaultHeaders map[string]string
|
||||
|
||||
func init() {
|
||||
DefaultHeaders = buildDefaultHeaders(DefaultDeviceProfile())
|
||||
}
|
||||
|
||||
// Model 表示一个 Claude 模型
|
||||
@ -444,22 +514,36 @@ func DenormalizeModelID(id string) string {
|
||||
return id
|
||||
}
|
||||
|
||||
// ApplyFingerprintOverrides 用配置覆盖默认指纹值(每个实例可设不同值)
|
||||
// ApplyFingerprintOverrides 用配置覆盖默认指纹值(每个实例可设不同值)。
|
||||
//
|
||||
// 同步更新 DefaultBundle 与所有派生变量、DefaultHeaders、CLICurrentVersion,
|
||||
// 确保单一事实源。
|
||||
func ApplyFingerprintOverrides(cliVersion, pkgVersion, runtimeVersion, os_, arch string) {
|
||||
if cliVersion != "" {
|
||||
DefaultCLIVersion = strings.TrimSpace(cliVersion)
|
||||
v := strings.TrimSpace(cliVersion)
|
||||
DefaultBundle.CLIVersion = v
|
||||
DefaultCLIVersion = v
|
||||
CLICurrentVersion = v
|
||||
}
|
||||
if pkgVersion != "" {
|
||||
DefaultStainlessPackageVersion = strings.TrimSpace(pkgVersion)
|
||||
v := strings.TrimSpace(pkgVersion)
|
||||
DefaultBundle.SDKVersion = v
|
||||
DefaultStainlessPackageVersion = v
|
||||
}
|
||||
if runtimeVersion != "" {
|
||||
DefaultStainlessRuntimeVersion = strings.TrimSpace(runtimeVersion)
|
||||
v := strings.TrimSpace(runtimeVersion)
|
||||
DefaultBundle.RuntimeVersion = v
|
||||
DefaultStainlessRuntimeVersion = v
|
||||
}
|
||||
if os_ != "" {
|
||||
DefaultStainlessOS = strings.TrimSpace(os_)
|
||||
v := strings.TrimSpace(os_)
|
||||
DefaultBundle.DefaultOS = v
|
||||
DefaultStainlessOS = v
|
||||
}
|
||||
if arch != "" {
|
||||
DefaultStainlessArch = strings.TrimSpace(arch)
|
||||
v := strings.TrimSpace(arch)
|
||||
DefaultBundle.DefaultArch = v
|
||||
DefaultStainlessArch = v
|
||||
}
|
||||
DefaultHeaders = buildDefaultHeaders(DefaultDeviceProfile())
|
||||
}
|
||||
|
||||
@ -1,6 +1,65 @@
|
||||
package claude
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDefaultBundleVersions verifies the active Claude Code bundle snapshot
|
||||
// matches the reverse-engineered 2.1.145 binary. Bump this when DefaultBundle
|
||||
// is upgraded to a newer CLI release.
|
||||
func TestDefaultBundleVersions(t *testing.T) {
|
||||
if got := DefaultBundle.CLIVersion; got != "2.1.145" {
|
||||
t.Fatalf("DefaultBundle.CLIVersion = %q, want %q", got, "2.1.145")
|
||||
}
|
||||
if got := DefaultBundle.SDKVersion; got != "0.94.0" {
|
||||
t.Fatalf("DefaultBundle.SDKVersion = %q, want %q", got, "0.94.0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultBundleDrivesLegacyExports ensures all legacy exported vars stay
|
||||
// in sync with DefaultBundle (single source of truth).
|
||||
func TestDefaultBundleDrivesLegacyExports(t *testing.T) {
|
||||
if DefaultCLIVersion != DefaultBundle.CLIVersion {
|
||||
t.Fatalf("DefaultCLIVersion = %q, want %q (from DefaultBundle)", DefaultCLIVersion, DefaultBundle.CLIVersion)
|
||||
}
|
||||
if DefaultStainlessPackageVersion != DefaultBundle.SDKVersion {
|
||||
t.Fatalf("DefaultStainlessPackageVersion = %q, want %q (from DefaultBundle)", DefaultStainlessPackageVersion, DefaultBundle.SDKVersion)
|
||||
}
|
||||
if DefaultStainlessRuntimeVersion != DefaultBundle.RuntimeVersion {
|
||||
t.Fatalf("DefaultStainlessRuntimeVersion = %q, want %q (from DefaultBundle)", DefaultStainlessRuntimeVersion, DefaultBundle.RuntimeVersion)
|
||||
}
|
||||
if DefaultStainlessOS != DefaultBundle.DefaultOS {
|
||||
t.Fatalf("DefaultStainlessOS = %q, want %q (from DefaultBundle)", DefaultStainlessOS, DefaultBundle.DefaultOS)
|
||||
}
|
||||
if DefaultStainlessArch != DefaultBundle.DefaultArch {
|
||||
t.Fatalf("DefaultStainlessArch = %q, want %q (from DefaultBundle)", DefaultStainlessArch, DefaultBundle.DefaultArch)
|
||||
}
|
||||
if CLICurrentVersion != DefaultBundle.CLIVersion {
|
||||
t.Fatalf("CLICurrentVersion = %q, want %q (from DefaultBundle)", CLICurrentVersion, DefaultBundle.CLIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultHeadersDrivenByBundle ensures DefaultHeaders map carries the
|
||||
// values from DefaultBundle, not stale hard-coded literals.
|
||||
func TestDefaultHeadersDrivenByBundle(t *testing.T) {
|
||||
if got := DefaultHeaders["X-Stainless-Package-Version"]; got != DefaultBundle.SDKVersion {
|
||||
t.Fatalf("DefaultHeaders[X-Stainless-Package-Version] = %q, want %q", got, DefaultBundle.SDKVersion)
|
||||
}
|
||||
if got := DefaultHeaders["X-Stainless-Runtime-Version"]; got != DefaultBundle.RuntimeVersion {
|
||||
t.Fatalf("DefaultHeaders[X-Stainless-Runtime-Version] = %q, want %q", got, DefaultBundle.RuntimeVersion)
|
||||
}
|
||||
if got := DefaultHeaders["X-Stainless-OS"]; got != DefaultBundle.DefaultOS {
|
||||
t.Fatalf("DefaultHeaders[X-Stainless-OS] = %q, want %q", got, DefaultBundle.DefaultOS)
|
||||
}
|
||||
if got := DefaultHeaders["X-Stainless-Arch"]; got != DefaultBundle.DefaultArch {
|
||||
t.Fatalf("DefaultHeaders[X-Stainless-Arch] = %q, want %q", got, DefaultBundle.DefaultArch)
|
||||
}
|
||||
ua := DefaultHeaders["User-Agent"]
|
||||
if !strings.Contains(ua, "claude-cli/"+DefaultBundle.CLIVersion) {
|
||||
t.Fatalf("DefaultHeaders[User-Agent] = %q, want substring %q", ua, "claude-cli/"+DefaultBundle.CLIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFingerprintOverridesUpdatesSharedDefaults(t *testing.T) {
|
||||
t.Setenv("CLAUDE_CODE_ENTRYPOINT", "")
|
||||
@ -74,7 +133,7 @@ func TestDefaultUserAgentReflectsLocalRuntimeDescriptors(t *testing.T) {
|
||||
t.Setenv("CLAUDE_CODE_WORKLOAD", "cron")
|
||||
|
||||
got := DefaultUserAgent()
|
||||
want := "claude-cli/2.1.104 (external, sdk-cli, agent-sdk/0.0.77, client-app/cron-daemon, workload/cron)"
|
||||
want := "claude-cli/2.1.145 (external, sdk-cli, agent-sdk/0.0.77, client-app/cron-daemon, workload/cron)"
|
||||
if got != want {
|
||||
t.Fatalf("DefaultUserAgent() = %q, want %q", got, want)
|
||||
}
|
||||
@ -86,7 +145,7 @@ func TestDetailedCodeUserAgentReflectsEntrypointAndSDK(t *testing.T) {
|
||||
t.Setenv("CLAUDE_AGENT_SDK_CLIENT_APP", "desktop")
|
||||
|
||||
got := DetailedCodeUserAgent()
|
||||
want := "claude-code/2.1.104 (remote, agent-sdk/0.0.77, client-app/desktop)"
|
||||
want := "claude-code/2.1.145 (remote, agent-sdk/0.0.77, client-app/desktop)"
|
||||
if got != want {
|
||||
t.Fatalf("DetailedCodeUserAgent() = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ var SuspiciousHeaders = []string{
|
||||
}
|
||||
|
||||
// RequiredNodeHeaders 是 Node.js Claude Code 客户端必须有的头
|
||||
//
|
||||
// 来源:反编译 Claude Code 2.1.145 binary,确认这些头在每个 /v1/messages
|
||||
// 与 /v1/messages/count_tokens 请求中都会被 SDK 强制设置。
|
||||
var RequiredNodeHeaders = map[string]bool{
|
||||
"User-Agent": true,
|
||||
"X-Stainless-Lang": true,
|
||||
@ -33,6 +36,9 @@ var RequiredNodeHeaders = map[string]bool{
|
||||
"X-Stainless-OS": true,
|
||||
"X-Stainless-Arch": true,
|
||||
"anthropic-version": true,
|
||||
// 2.1.145 强制:SDK 内 `"X-Claude-Code-Session-Id":y_()`,
|
||||
// 上游用其识别会话;缺失会被判第三方。
|
||||
"X-Claude-Code-Session-Id": true,
|
||||
}
|
||||
|
||||
// ValidateNodeEmulation 验证请求是否正确伪装为 Node.js 客户端
|
||||
|
||||
@ -19,6 +19,8 @@ func TestValidateNodeEmulation_ValidRequest(t *testing.T) {
|
||||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||||
// 2.1.145 强制:X-Claude-Code-Session-Id (UUID)
|
||||
req.Header.Set("X-Claude-Code-Session-Id", "01970000-0000-7000-8000-000000000001")
|
||||
|
||||
isValid, errors := ValidateNodeEmulation(req)
|
||||
|
||||
@ -44,8 +46,39 @@ func TestValidateNodeEmulation_MissingHeaders(t *testing.T) {
|
||||
if len(errors) == 0 {
|
||||
t.Error("expected validation errors")
|
||||
}
|
||||
if len(errors) < 7 {
|
||||
t.Errorf("expected at least 7 missing headers, got %d errors", len(errors))
|
||||
if len(errors) < 8 {
|
||||
t.Errorf("expected at least 8 missing headers, got %d errors", len(errors))
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateNodeEmulation_MissingSessionID 验证 X-Claude-Code-Session-Id
|
||||
// 是强制头:缺失则验证失败。
|
||||
func TestValidateNodeEmulation_MissingSessionID(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||||
|
||||
// 设置所有 Stainless 头,但缺 session-id
|
||||
req.Header.Set("User-Agent", claude.DefaultUserAgent())
|
||||
req.Header.Set("X-Stainless-Lang", claude.DefaultStainlessLang)
|
||||
req.Header.Set("X-Stainless-Runtime", claude.DefaultStainlessRuntime)
|
||||
req.Header.Set("X-Stainless-Runtime-Version", claude.DefaultStainlessRuntimeVersion)
|
||||
req.Header.Set("X-Stainless-Package-Version", claude.DefaultStainlessPackageVersion)
|
||||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||||
|
||||
isValid, errs := ValidateNodeEmulation(req)
|
||||
if isValid {
|
||||
t.Fatal("expected invalid due to missing X-Claude-Code-Session-Id")
|
||||
}
|
||||
foundSessionErr := false
|
||||
for _, e := range errs {
|
||||
if e == "missing required header: X-Claude-Code-Session-Id" {
|
||||
foundSessionErr = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundSessionErr {
|
||||
t.Fatalf("expected X-Claude-Code-Session-Id error, got: %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +178,8 @@ func TestValidateAndClean(t *testing.T) {
|
||||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||||
// 2.1.145 强制:X-Claude-Code-Session-Id
|
||||
req.Header.Set("X-Claude-Code-Session-Id", "01970000-0000-7000-8000-000000000002")
|
||||
|
||||
isValid, errors := ValidateAndClean(req)
|
||||
|
||||
|
||||
73
backend/internal/service/claude_code_session_id.go
Normal file
73
backend/internal/service/claude_code_session_id.go
Normal file
@ -0,0 +1,73 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ensureClaudeCodeSessionID 确保 X-Claude-Code-Session-Id header 被合理填充。
|
||||
//
|
||||
// 真实 Claude Code 2.1.145 CLI 在 SDK 内会始终设置 X-Claude-Code-Session-Id 为
|
||||
// 当前 CLI 会话的 UUID(源码:`"X-Claude-Code-Session-Id":y_()`)。上游若发现
|
||||
// 该头缺失,可能将请求识别为非官方 Claude Code 第三方调用。
|
||||
//
|
||||
// 行为按 tokenType / mimicClaudeCode 分两条路径:
|
||||
//
|
||||
// OAuth mimic 路径 (tokenType == "oauth" && mimicClaudeCode):
|
||||
// 1. body 中 metadata.user_id 派生的 SessionID 是合法 UUID → canonicalize 写入
|
||||
// 2. 请求 header 中已有合法 UUID → canonicalize 保留
|
||||
// 3. 否则 → 兜底生成 UUID
|
||||
//
|
||||
// API key 透传 / 非 mimic 路径:
|
||||
// - 不从 body 合成 header(避免污染客户端原始语义)
|
||||
// - 但若客户端在 header 中传入了 X-Claude-Code-Session-Id:
|
||||
// 合法 UUID → canonicalize 保留
|
||||
// 非法值 → 删除(不向上游转发恶意值,符合 UUID 校验承诺)
|
||||
// - 不兜底生成
|
||||
//
|
||||
// 安全说明:metadata.user_id 由客户端控制,ParseMetadataUserID 的正则仅约束字符集,
|
||||
// 不保证 UUID 结构。因此所有写入 header 的 session id 都必须经 uuid.Parse 校验。
|
||||
func ensureClaudeCodeSessionID(req *http.Request, body []byte, tokenType string, mimicClaudeCode bool) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
if req.Header == nil {
|
||||
req.Header = make(http.Header)
|
||||
}
|
||||
|
||||
isOAuthMimic := tokenType == "oauth" && mimicClaudeCode
|
||||
|
||||
// OAuth mimic 路径:从 metadata 派生(仅在 mimic 场景写 header)。
|
||||
if isOAuthMimic {
|
||||
if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" {
|
||||
if parsed := ParseMetadataUserID(uid); parsed != nil {
|
||||
if id, err := uuid.Parse(parsed.SessionID); err == nil {
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", id.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理客户端已传入的 header:合法 → canonicalize,非法 → 删除。
|
||||
// 这条规则对所有路径生效,避免恶意非 UUID 值向上游透传。
|
||||
if existing := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id"); existing != "" {
|
||||
if id, err := uuid.Parse(existing); err == nil {
|
||||
canonical := id.String()
|
||||
if canonical != existing {
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", canonical)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 非法 UUID:删除避免透传
|
||||
req.Header.Del("X-Claude-Code-Session-Id")
|
||||
}
|
||||
|
||||
// OAuth mimic 兜底生成(仅 mimic 场景;API key 不污染)。
|
||||
// uuid.NewString() 走 crypto/rand。
|
||||
if isOAuthMimic {
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", uuid.NewString())
|
||||
}
|
||||
}
|
||||
200
backend/internal/service/claude_code_session_id_test.go
Normal file
200
backend/internal/service/claude_code_session_id_test.go
Normal file
@ -0,0 +1,200 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
testValidUUID = "01970000-0000-7000-8000-000000000001"
|
||||
testValidUUIDAlt = "01970000-0000-7000-8000-000000000002"
|
||||
)
|
||||
|
||||
func newReq(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func TestEnsureClaudeCodeSessionID_NilRequest(t *testing.T) {
|
||||
// Should not panic.
|
||||
ensureClaudeCodeSessionID(nil, nil, "oauth", true)
|
||||
}
|
||||
|
||||
func TestEnsureClaudeCodeSessionID_FromMetadataJSON(t *testing.T) {
|
||||
req := newReq(t)
|
||||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
|
||||
ensureClaudeCodeSessionID(req, body, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != testValidUUID {
|
||||
t.Fatalf("session_id = %q, want %q", got, testValidUUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClaudeCodeSessionID_FromMetadataLegacy(t *testing.T) {
|
||||
req := newReq(t)
|
||||
// legacy format: user_{64hex}_account_{uuid}_session_{uuid}
|
||||
dev := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
acc := "11111111-2222-3333-4444-555555555555"
|
||||
uid := "user_" + dev + "_account_" + acc + "_session_" + testValidUUID
|
||||
body := []byte(`{"metadata":{"user_id":"` + uid + `"}}`)
|
||||
ensureClaudeCodeSessionID(req, body, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != testValidUUID {
|
||||
t.Fatalf("session_id = %q, want %q", got, testValidUUID)
|
||||
}
|
||||
}
|
||||
|
||||
// 安全测试:metadata.user_id 中的 session_id 不是合法 UUID 时,
|
||||
// 必须 fallback 到 OAuth UUID 兜底,而不是写入恶意值。
|
||||
func TestEnsureClaudeCodeSessionID_RejectsMalformedMetadataUUID(t *testing.T) {
|
||||
req := newReq(t)
|
||||
// session_id 字段是 36 字符但非 UUID 格式(凑数 hex+dash)
|
||||
bogus := "abcdefab-0000-0000-0000-not-a-real-uuid"
|
||||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + bogus + `\"}"}}`)
|
||||
ensureClaudeCodeSessionID(req, body, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got == bogus {
|
||||
t.Fatalf("malformed metadata session_id was written verbatim: %q", got)
|
||||
}
|
||||
if got == "" {
|
||||
t.Fatalf("expected OAuth mimic to fallback to UUID, got empty")
|
||||
}
|
||||
if _, err := uuid.Parse(got); err != nil {
|
||||
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClaudeCodeSessionID_PreservesExistingValidHeader(t *testing.T) {
|
||||
req := newReq(t)
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", testValidUUID)
|
||||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != testValidUUID {
|
||||
t.Fatalf("session_id = %q, want preserved %q", got, testValidUUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClaudeCodeSessionID_OverwritesInvalidHeader(t *testing.T) {
|
||||
req := newReq(t)
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", "not-a-uuid")
|
||||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got == "not-a-uuid" {
|
||||
t.Fatalf("invalid header was not overwritten: %q", got)
|
||||
}
|
||||
if _, err := uuid.Parse(got); err != nil {
|
||||
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureClaudeCodeSessionID_OAuthMimicFallback(t *testing.T) {
|
||||
req := newReq(t)
|
||||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got == "" {
|
||||
t.Fatalf("expected OAuth mimic fallback to set X-Claude-Code-Session-Id")
|
||||
}
|
||||
if _, err := uuid.Parse(got); err != nil {
|
||||
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// API key passthrough 路径必须不被污染:缺失 session-id 时不强制生成。
|
||||
func TestEnsureClaudeCodeSessionID_DoesNotPolluteAPIKey(t *testing.T) {
|
||||
req := newReq(t)
|
||||
ensureClaudeCodeSessionID(req, nil, "api_key", false)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != "" {
|
||||
t.Fatalf("API key path should NOT auto-generate session-id, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// API key 路径即使 body 中有合法 metadata.user_id,也不应该派生 session-id 头。
|
||||
// 这是 fresh code review 发现的 C1 修复:保护客户端原始语义。
|
||||
func TestEnsureClaudeCodeSessionID_APIKeyIgnoresMetadata(t *testing.T) {
|
||||
req := newReq(t)
|
||||
// 客户端传入合法的 metadata.user_id,但 tokenType=api_key
|
||||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
|
||||
ensureClaudeCodeSessionID(req, body, "api_key", false)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != "" {
|
||||
t.Fatalf("API key path must NOT derive session-id from metadata, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth 但非 mimic 模式也不应该从 metadata 派生 header。
|
||||
func TestEnsureClaudeCodeSessionID_OAuthNonMimicIgnoresMetadata(t *testing.T) {
|
||||
req := newReq(t)
|
||||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
|
||||
ensureClaudeCodeSessionID(req, body, "oauth", false)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != "" {
|
||||
t.Fatalf("Non-mimic OAuth must NOT derive session-id from metadata, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// API key 路径若客户端传入了非法 UUID header,必须删除避免向上游透传。
|
||||
// 这是 C2 修复:UUID 校验承诺要对所有路径生效。
|
||||
func TestEnsureClaudeCodeSessionID_APIKeyDeletesInvalidHeader(t *testing.T) {
|
||||
req := newReq(t)
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", "not-a-uuid")
|
||||
ensureClaudeCodeSessionID(req, nil, "api_key", false)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != "" {
|
||||
t.Fatalf("API key path must delete invalid header, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// API key 路径若客户端传入了合法 UUID header,规范化保留(不删除)。
|
||||
func TestEnsureClaudeCodeSessionID_APIKeyPreservesValidHeader(t *testing.T) {
|
||||
req := newReq(t)
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", testValidUUID)
|
||||
ensureClaudeCodeSessionID(req, nil, "api_key", false)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != testValidUUID {
|
||||
t.Fatalf("API key path must preserve valid client header, got %q want %q", got, testValidUUID)
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth 但非 mimic 场景:保留客户端原始语义,不自动生成。
|
||||
func TestEnsureClaudeCodeSessionID_OAuthNonMimicDoesNotForce(t *testing.T) {
|
||||
req := newReq(t)
|
||||
ensureClaudeCodeSessionID(req, nil, "oauth", false)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
if got != "" {
|
||||
t.Fatalf("non-mimic OAuth should not auto-generate, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证不同 UUID 输入会被规范化为 canonical 小写形式。
|
||||
func TestEnsureClaudeCodeSessionID_CanonicalForm(t *testing.T) {
|
||||
req := newReq(t)
|
||||
// 大写 UUID 输入
|
||||
upper := "01970000-0000-7000-8000-ABCDEF000003"
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", upper)
|
||||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||||
|
||||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||||
want := "01970000-0000-7000-8000-abcdef000003"
|
||||
if got != want {
|
||||
t.Fatalf("session_id = %q, want canonical %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -6253,13 +6253,10 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
|
||||
}
|
||||
|
||||
// X-Claude-Code-Session-Id 头处理:
|
||||
// Claude Code 主 API 客户端会始终发送 X-Claude-Code-Session-Id。
|
||||
// 对于 mimic / 转发场景,只要 body 中 metadata.user_id 可解析,就主动注入并同步该头。
|
||||
if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" {
|
||||
if parsed := ParseMetadataUserID(uid); parsed != nil {
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", parsed.SessionID)
|
||||
}
|
||||
}
|
||||
// Claude Code 2.1.145 SDK 内强制设置该头(`"X-Claude-Code-Session-Id":y_()`)。
|
||||
// 优先取 metadata.user_id 中的 sessionID;OAuth mimic 场景缺失时兜底 UUID,
|
||||
// 避免上游基于该头缺失判定为第三方调用。
|
||||
ensureClaudeCodeSessionID(req, body, tokenType, mimicClaudeCode)
|
||||
|
||||
// x-client-request-id: 真实 CLI 每个请求生成新 UUID(仅 1P)。
|
||||
if getHeaderRaw(req.Header, "x-client-request-id") == "" && tokenType == "oauth" {
|
||||
@ -9462,13 +9459,9 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
|
||||
}
|
||||
}
|
||||
|
||||
// Claude Code 主 API 客户端会始终发送 X-Claude-Code-Session-Id。
|
||||
// 对于 mimic / 转发场景,只要 body 中 metadata.user_id 可解析,就主动注入并同步该头。
|
||||
if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" {
|
||||
if parsed := ParseMetadataUserID(uid); parsed != nil {
|
||||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", parsed.SessionID)
|
||||
}
|
||||
}
|
||||
// X-Claude-Code-Session-Id 头处理(count_tokens 路径):
|
||||
// 与 messages 路径保持同样逻辑,OAuth mimic 场景缺失时兜底 UUID。
|
||||
ensureClaudeCodeSessionID(req, body, tokenType, mimicClaudeCode)
|
||||
|
||||
// x-client-request-id(count_tokens 路径)
|
||||
if getHeaderRaw(req.Header, "x-client-request-id") == "" && tokenType == "oauth" {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user