- 保留 Windsurf 订制代码 - 上游新增:Affiliate 邀返佣功能、OpenAI compact 支持、Claude Code 完整 mimicry - 解决冲突:handler/wire.go、wire_gen.go、constants.go、gateway_service.go 等
446 lines
17 KiB
Go
446 lines
17 KiB
Go
// Package claude provides constants and helpers for Claude API integration.
|
||
package claude
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
// Claude Code 客户端相关常量
|
||
|
||
// DefaultCLIVersion 是当前模拟的 Claude CLI 版本
|
||
var (
|
||
DefaultCLIVersion = "2.1.104"
|
||
DefaultStainlessLang = "js"
|
||
DefaultStainlessPackageVersion = "0.81.0"
|
||
DefaultStainlessOS = "MacOS"
|
||
DefaultStainlessArch = "arm64"
|
||
DefaultStainlessRuntime = "node"
|
||
DefaultStainlessRuntimeVersion = "v24.3.0"
|
||
DefaultStainlessRetryCount = "0"
|
||
DefaultStainlessTimeout = "600"
|
||
DefaultXApp = "cli"
|
||
DefaultAnthropicVersion = "2023-06-01"
|
||
)
|
||
|
||
// DeviceProfile 表示一组 Claude Code 客户端设备画像默认值。
|
||
type DeviceProfile struct {
|
||
UserAgent string
|
||
StainlessLang string
|
||
StainlessPackageVersion string
|
||
StainlessOS string
|
||
StainlessArch string
|
||
StainlessRuntime string
|
||
StainlessRuntimeVersion string
|
||
StainlessRetryCount string
|
||
StainlessTimeout string
|
||
XApp string
|
||
AnthropicVersion string
|
||
}
|
||
|
||
func trimEnv(key string) string {
|
||
return strings.TrimSpace(os.Getenv(key))
|
||
}
|
||
|
||
func envTruthy(key string) bool {
|
||
switch strings.ToLower(trimEnv(key)) {
|
||
case "1", "true", "yes", "on":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func envExplicitFalse(key string) bool {
|
||
switch strings.ToLower(trimEnv(key)) {
|
||
case "0", "false", "no", "off":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// CurrentEntrypoint returns the Claude Code entrypoint label used by the real CLI.
|
||
// The local bundle defaults to "cli" when CLAUDE_CODE_ENTRYPOINT is not set.
|
||
func CurrentEntrypoint() string {
|
||
if entrypoint := trimEnv("CLAUDE_CODE_ENTRYPOINT"); entrypoint != "" {
|
||
return entrypoint
|
||
}
|
||
return "cli"
|
||
}
|
||
|
||
func currentAgentSDKVersion() string {
|
||
return trimEnv("CLAUDE_AGENT_SDK_VERSION")
|
||
}
|
||
|
||
func currentClientApp() string {
|
||
return trimEnv("CLAUDE_AGENT_SDK_CLIENT_APP")
|
||
}
|
||
|
||
// CurrentWorkload returns the process-scoped workload tag used for cc_workload attribution.
|
||
// Local Claude Code keeps this in-process; sub2api exposes an env fallback so the server can
|
||
// mirror cron/daemon callers when configured.
|
||
func CurrentWorkload() string {
|
||
return trimEnv("CLAUDE_CODE_WORKLOAD")
|
||
}
|
||
|
||
func currentCLIUserAgentDescriptors() []string {
|
||
parts := []string{CurrentEntrypoint()}
|
||
if sdkVersion := currentAgentSDKVersion(); sdkVersion != "" {
|
||
parts = append(parts, "agent-sdk/"+sdkVersion)
|
||
}
|
||
if clientApp := currentClientApp(); clientApp != "" {
|
||
parts = append(parts, "client-app/"+clientApp)
|
||
}
|
||
if workload := CurrentWorkload(); workload != "" {
|
||
parts = append(parts, "workload/"+workload)
|
||
}
|
||
return parts
|
||
}
|
||
|
||
func currentCodeUserAgentDescriptors() []string {
|
||
parts := make([]string, 0, 3)
|
||
if entrypoint := trimEnv("CLAUDE_CODE_ENTRYPOINT"); entrypoint != "" {
|
||
parts = append(parts, entrypoint)
|
||
}
|
||
if sdkVersion := currentAgentSDKVersion(); sdkVersion != "" {
|
||
parts = append(parts, "agent-sdk/"+sdkVersion)
|
||
}
|
||
if clientApp := currentClientApp(); clientApp != "" {
|
||
parts = append(parts, "client-app/"+clientApp)
|
||
}
|
||
return parts
|
||
}
|
||
|
||
func formatUserAgent(cliVersion string) string {
|
||
version := strings.TrimSpace(cliVersion)
|
||
if version == "" {
|
||
version = DefaultCLIVersion
|
||
}
|
||
return fmt.Sprintf("claude-cli/%s (external, %s)", version, strings.Join(currentCLIUserAgentDescriptors(), ", "))
|
||
}
|
||
|
||
func DefaultUserAgent() string {
|
||
return formatUserAgent(DefaultCLIVersion)
|
||
}
|
||
|
||
func DefaultCodeUserAgent() string {
|
||
return "claude-code/" + strings.TrimSpace(DefaultCLIVersion)
|
||
}
|
||
|
||
// DetailedCodeUserAgent mirrors the local JqH() builder, which appends entrypoint / agent-sdk /
|
||
// client-app descriptors when they are present in the process environment.
|
||
func DetailedCodeUserAgent() string {
|
||
version := strings.TrimSpace(DefaultCLIVersion)
|
||
if version == "" {
|
||
version = "unknown"
|
||
}
|
||
parts := currentCodeUserAgentDescriptors()
|
||
if len(parts) == 0 {
|
||
return "claude-code/" + version
|
||
}
|
||
return fmt.Sprintf("claude-code/%s (%s)", version, strings.Join(parts, ", "))
|
||
}
|
||
|
||
// DefaultDeviceProfile 返回当前默认 Claude Code 设备画像。
|
||
func DefaultDeviceProfile() DeviceProfile {
|
||
return DeviceProfile{
|
||
UserAgent: DefaultUserAgent(),
|
||
StainlessLang: DefaultStainlessLang,
|
||
StainlessPackageVersion: DefaultStainlessPackageVersion,
|
||
StainlessOS: DefaultStainlessOS,
|
||
StainlessArch: DefaultStainlessArch,
|
||
StainlessRuntime: DefaultStainlessRuntime,
|
||
StainlessRuntimeVersion: DefaultStainlessRuntimeVersion,
|
||
StainlessRetryCount: DefaultStainlessRetryCount,
|
||
StainlessTimeout: DefaultStainlessTimeout,
|
||
XApp: DefaultXApp,
|
||
AnthropicVersion: DefaultAnthropicVersion,
|
||
}
|
||
}
|
||
|
||
func buildDefaultHeaders(profile DeviceProfile) map[string]string {
|
||
return 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.
|
||
"User-Agent": profile.UserAgent,
|
||
"X-Stainless-Lang": profile.StainlessLang,
|
||
"X-Stainless-Package-Version": profile.StainlessPackageVersion,
|
||
"X-Stainless-OS": profile.StainlessOS,
|
||
"X-Stainless-Arch": profile.StainlessArch,
|
||
"X-Stainless-Runtime": profile.StainlessRuntime,
|
||
"X-Stainless-Runtime-Version": profile.StainlessRuntimeVersion,
|
||
"X-Stainless-Retry-Count": profile.StainlessRetryCount,
|
||
"X-Stainless-Timeout": profile.StainlessTimeout,
|
||
"X-App": profile.XApp,
|
||
"anthropic-version": profile.AnthropicVersion,
|
||
"anthropic-dangerous-direct-browser-access": "true",
|
||
}
|
||
}
|
||
|
||
// DefaultHeadersSnapshot returns a fresh copy of the default Claude Code header skeleton.
|
||
// It re-evaluates env-backed runtime values like CLAUDE_CODE_ENTRYPOINT on each call.
|
||
func DefaultHeadersSnapshot() map[string]string {
|
||
return buildDefaultHeaders(DefaultDeviceProfile())
|
||
}
|
||
|
||
// OptionalAPIHeaders returns the local Claude Code env-driven optional headers that are only
|
||
// attached in remote / SDK / protected modes.
|
||
func OptionalAPIHeaders() map[string]string {
|
||
headers := map[string]string{}
|
||
if containerID := trimEnv("CLAUDE_CODE_CONTAINER_ID"); containerID != "" {
|
||
headers["x-claude-remote-container-id"] = containerID
|
||
}
|
||
if remoteSessionID := trimEnv("CLAUDE_CODE_REMOTE_SESSION_ID"); remoteSessionID != "" {
|
||
headers["x-claude-remote-session-id"] = remoteSessionID
|
||
}
|
||
if clientApp := currentClientApp(); clientApp != "" {
|
||
headers["x-client-app"] = clientApp
|
||
}
|
||
if envTruthy("CLAUDE_CODE_ADDITIONAL_PROTECTION") {
|
||
headers["x-anthropic-additional-protection"] = "true"
|
||
}
|
||
return headers
|
||
}
|
||
|
||
// AttributionHeaderDisabled mirrors the official CLAUDE_CODE_ATTRIBUTION_HEADER=false toggle.
|
||
func AttributionHeaderDisabled() bool {
|
||
return envExplicitFalse("CLAUDE_CODE_ATTRIBUTION_HEADER")
|
||
}
|
||
|
||
// Beta header 常量
|
||
//
|
||
// 这里的常量对齐真实 Claude Code CLI 的最新流量(截至 2026-04)。
|
||
// 选型参考:与 Parrot (src/transform/cc_mimicry.py) 的 BETAS 保持一致,
|
||
// 原因: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 以来的流量)
|
||
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"
|
||
)
|
||
|
||
// DroppedBetas 是转发时需要从 anthropic-beta header 中移除的 beta token 列表。
|
||
// 这些 token 是客户端特有的,不应透传给上游 API。
|
||
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
|
||
|
||
// MessageBetaHeaderNoTools /v1/messages 在无工具时的 beta header(OAuth,不含 context-1m)
|
||
//
|
||
// NOTE: Claude Code OAuth credentials are scoped to Claude Code. When we "mimic"
|
||
// Claude Code for non-Claude-Code clients, we must include the claude-code beta
|
||
// even if the request doesn't use tools, otherwise upstream may reject the
|
||
// request as a non-Claude-Code API request.
|
||
const MessageBetaHeaderNoTools = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||
|
||
// MessageBetaHeaderWithTools /v1/messages 在有工具时的 beta header(OAuth,不含 context-1m)
|
||
const MessageBetaHeaderWithTools = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||
|
||
// CountTokensBetaHeader count_tokens 请求使用的 anthropic-beta header
|
||
const CountTokensBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaTokenCounting + "," + BetaContextManagement
|
||
|
||
// HaikuBetaHeader Haiku 模型使用的 anthropic-beta header(OAuth,不含 claude-code / context-1m)
|
||
const HaikuBetaHeader = BetaOAuth + "," + BetaInterleavedThinking + "," + BetaEffort
|
||
|
||
// APIKeyBetaHeader API-key 账号使用的 anthropic-beta header(不含 oauth / context-1m)
|
||
// 使用 GetAPIKeyBetaHeader(modelID) 获取含 context-1m 的 model-aware 版本。
|
||
const APIKeyBetaHeader = BetaClaudeCode + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + 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-sonnet-4 系列 和 claude-opus-4-6 支持 1M context。
|
||
func ModelSupports1M(modelID string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(modelID))
|
||
return strings.Contains(lower, "claude-sonnet-4") || strings.Contains(lower, "opus-4-6")
|
||
}
|
||
|
||
// GetOAuthBetaHeader 返回 OAuth 账号的 beta header。
|
||
// 仅当模型支持 1M context 时才包含 context-1m-2025-08-07。
|
||
func GetOAuthBetaHeader(modelID string) string {
|
||
if ModelSupports1M(modelID) {
|
||
return BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + BetaContext1M + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
|
||
}
|
||
return DefaultBetaHeader
|
||
}
|
||
|
||
// GetAPIKeyBetaHeader 返回 API-key 账号的 beta header。
|
||
// 仅当模型支持 1M context 时才包含 context-1m-2025-08-07。
|
||
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 APIKeyBetaHeader
|
||
}
|
||
|
||
// DefaultCacheControlTTL 是网关代理为自己生成的 cache_control 块默认使用的 ttl。
|
||
// 真实 Claude Code CLI 当前使用 "1h",但本仓策略是"客户端透传 ttl 优先;
|
||
// 客户端缺省时统一使用 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 抓包一致。
|
||
//
|
||
// 使用建议:
|
||
// - OAuth 账号 + 非 haiku:追加这整份列表,再按需保留 client 带来的 beta。
|
||
// - OAuth 账号 + haiku:Anthropic 对 haiku 不做 third-party 判定,使用 HaikuBetaHeader 即可。
|
||
// - API-key 账号:不要使用本函数,参见 APIKeyBetaHeader。
|
||
func FullClaudeCodeMimicryBetas() []string {
|
||
return []string{
|
||
BetaClaudeCode,
|
||
BetaOAuth,
|
||
BetaInterleavedThinking,
|
||
BetaPromptCachingScope,
|
||
BetaEffort,
|
||
BetaRedactThinking,
|
||
BetaContextManagement,
|
||
BetaExtendedCacheTTL,
|
||
}
|
||
}
|
||
|
||
// 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",
|
||
}
|
||
|
||
// Model 表示一个 Claude 模型
|
||
type Model struct {
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
DisplayName string `json:"display_name"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// DefaultModels Claude Code 客户端支持的默认模型列表
|
||
var DefaultModels = []Model{
|
||
{
|
||
ID: "claude-opus-4-5-20251101",
|
||
Type: "model",
|
||
DisplayName: "Claude Opus 4.5",
|
||
CreatedAt: "2025-11-01T00:00:00Z",
|
||
},
|
||
{
|
||
ID: "claude-opus-4-6",
|
||
Type: "model",
|
||
DisplayName: "Claude Opus 4.6",
|
||
CreatedAt: "2026-02-06T00:00:00Z",
|
||
},
|
||
{
|
||
ID: "claude-opus-4-7",
|
||
Type: "model",
|
||
DisplayName: "Claude Opus 4.7",
|
||
CreatedAt: "2026-04-17T00:00:00Z",
|
||
},
|
||
{
|
||
ID: "claude-sonnet-4-6",
|
||
Type: "model",
|
||
DisplayName: "Claude Sonnet 4.6",
|
||
CreatedAt: "2026-02-18T00:00:00Z",
|
||
},
|
||
{
|
||
ID: "claude-sonnet-4-5-20250929",
|
||
Type: "model",
|
||
DisplayName: "Claude Sonnet 4.5",
|
||
CreatedAt: "2025-09-29T00:00:00Z",
|
||
},
|
||
{
|
||
ID: "claude-haiku-4-5-20251001",
|
||
Type: "model",
|
||
DisplayName: "Claude Haiku 4.5",
|
||
CreatedAt: "2025-10-01T00:00:00Z",
|
||
},
|
||
}
|
||
|
||
// DefaultModelIDs 返回默认模型的 ID 列表
|
||
func DefaultModelIDs() []string {
|
||
ids := make([]string, len(DefaultModels))
|
||
for i, m := range DefaultModels {
|
||
ids[i] = m.ID
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// DefaultTestModel 测试时使用的默认模型
|
||
const DefaultTestModel = "claude-sonnet-4-5-20250929"
|
||
|
||
// ModelIDOverrides Claude OAuth 请求需要的模型 ID 映射
|
||
var ModelIDOverrides = map[string]string{
|
||
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
|
||
"claude-opus-4-5": "claude-opus-4-5-20251101",
|
||
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
||
}
|
||
|
||
// ModelIDReverseOverrides 用于将上游模型 ID 还原为短名
|
||
var ModelIDReverseOverrides = map[string]string{
|
||
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
|
||
"claude-opus-4-5-20251101": "claude-opus-4-5",
|
||
"claude-haiku-4-5-20251001": "claude-haiku-4-5",
|
||
}
|
||
|
||
// NormalizeModelID 根据 Claude OAuth 规则映射模型
|
||
func NormalizeModelID(id string) string {
|
||
if id == "" {
|
||
return id
|
||
}
|
||
if mapped, ok := ModelIDOverrides[id]; ok {
|
||
return mapped
|
||
}
|
||
return id
|
||
}
|
||
|
||
// DenormalizeModelID 将上游模型 ID 转换为短名
|
||
func DenormalizeModelID(id string) string {
|
||
if id == "" {
|
||
return id
|
||
}
|
||
if mapped, ok := ModelIDReverseOverrides[id]; ok {
|
||
return mapped
|
||
}
|
||
return id
|
||
}
|