417 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 常量
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"
BetaRedactThinking = "redact-thinking-2026-02-12"
BetaContextManagement = "context-management-2025-06-27"
BetaPromptCachingScope = "prompt-caching-scope-2026-01-05"
BetaEffort = "effort-2025-11-24"
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 headerOAuth 账号,不含 context-1m
// 使用 GetOAuthBetaHeader(modelID) 获取含 context-1m 的 model-aware 版本。
const DefaultBetaHeader = BetaClaudeCode + "," + BetaOAuth + "," + BetaInterleavedThinking + "," + BetaFineGrainedToolStreaming + "," + BetaRedactThinking + "," + BetaContextManagement + "," + BetaPromptCachingScope + "," + BetaEffort
// MessageBetaHeaderNoTools /v1/messages 在无工具时的 beta headerOAuth不含 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 headerOAuth不含 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 headerOAuth不含 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
}
// DefaultHeaders 是 Claude Code 客户端默认请求头。
var DefaultHeaders = buildDefaultHeaders(DefaultDeviceProfile())
// ApplyFingerprintOverrides 用配置覆盖默认指纹值(每个实例可设不同值)
// cliVersion: Claude CLI 版本(如 "2.1.81"
// pkgVersion: SDK 版本(如 "0.80.0"
// runtimeVersion: Node.js 版本(如 "v24.13.0"
// os_: 操作系统(如 "Linux"
// arch: 架构(如 "arm64"
func ApplyFingerprintOverrides(cliVersion, pkgVersion, runtimeVersion, os_, arch string) {
if cliVersion != "" {
DefaultCLIVersion = strings.TrimSpace(cliVersion)
}
if pkgVersion != "" {
DefaultStainlessPackageVersion = strings.TrimSpace(pkgVersion)
}
if runtimeVersion != "" {
DefaultStainlessRuntimeVersion = strings.TrimSpace(runtimeVersion)
}
if os_ != "" {
DefaultStainlessOS = strings.TrimSpace(os_)
}
if arch != "" {
DefaultStainlessArch = strings.TrimSpace(arch)
}
DefaultHeaders = buildDefaultHeaders(DefaultDeviceProfile())
}
// 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
}