mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-16 13:24:55 +08:00
- Updated Antigravity Credits fallback to handle KV store unavailability as a service error. - Enhanced signature caching mechanisms with request-time KV access and sliding expiration. - Added and improved tests for KV client interactions, including error handling and expiration behaviors. - Introduced `CacheSignatureBestEffort` for non-critical signature caching and clarified function flows with required context. - Ensured consistent error reporting for missing or unavailable KV stores in various scenarios. - Replaced direct `homekv` calls with injectable KV client interfaces for `antigravity` and `codex_reasoning_replay` modules. - Improved error reporting and handling for KV operations, including `KVGet`, `KVSet`, `KVDel`, and `KVExpire`. - Introduced dedicated fake KV clients for expanded and granular test coverage. - Added new unit tests to validate KV client behaviors and error scenarios, ensuring robustness and sliding expiration functionality.
577 lines
18 KiB
Go
577 lines
18 KiB
Go
package helps
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
)
|
|
|
|
const (
|
|
defaultClaudeFingerprintUserAgent = "claude-cli/2.1.63 (external, cli)"
|
|
defaultClaudeFingerprintPackageVersion = "0.74.0"
|
|
defaultClaudeFingerprintRuntimeVersion = "v24.3.0"
|
|
defaultClaudeFingerprintOS = "MacOS"
|
|
defaultClaudeFingerprintArch = "arm64"
|
|
claudeDeviceProfileTTL = 7 * 24 * time.Hour
|
|
claudeDeviceProfileLockTTL = 5 * time.Second
|
|
claudeDeviceProfileCleanupPeriod = time.Hour
|
|
)
|
|
|
|
var (
|
|
claudeCLIVersionPattern = regexp.MustCompile(`^claude-cli/(\d+)\.(\d+)\.(\d+)`)
|
|
|
|
claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
|
|
claudeDeviceProfileCacheMu sync.RWMutex
|
|
claudeDeviceProfileCacheCleanupOnce sync.Once
|
|
|
|
ClaudeDeviceProfileBeforeCandidateStore func(ClaudeDeviceProfile)
|
|
)
|
|
|
|
type claudeDeviceProfileKVClient interface {
|
|
KVGet(ctx context.Context, key string) ([]byte, bool, error)
|
|
KVSet(ctx context.Context, key string, value []byte, opts homekv.KVSetOptions) (bool, error)
|
|
KVSetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
|
|
KVExpire(ctx context.Context, key string, ttl time.Duration) (bool, error)
|
|
}
|
|
|
|
var currentClaudeDeviceProfileKVClient = func() (claudeDeviceProfileKVClient, bool, error) {
|
|
return homekv.CurrentKVClient()
|
|
}
|
|
|
|
type claudeCLIVersion struct {
|
|
major int
|
|
minor int
|
|
patch int
|
|
}
|
|
|
|
func (v claudeCLIVersion) Compare(other claudeCLIVersion) int {
|
|
switch {
|
|
case v.major != other.major:
|
|
if v.major > other.major {
|
|
return 1
|
|
}
|
|
return -1
|
|
case v.minor != other.minor:
|
|
if v.minor > other.minor {
|
|
return 1
|
|
}
|
|
return -1
|
|
case v.patch != other.patch:
|
|
if v.patch > other.patch {
|
|
return 1
|
|
}
|
|
return -1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
type ClaudeDeviceProfile struct {
|
|
UserAgent string
|
|
PackageVersion string
|
|
RuntimeVersion string
|
|
OS string
|
|
Arch string
|
|
version claudeCLIVersion
|
|
hasVersion bool
|
|
}
|
|
|
|
type claudeDeviceProfileCacheEntry struct {
|
|
profile ClaudeDeviceProfile
|
|
expire time.Time
|
|
}
|
|
|
|
type claudeDeviceProfileKVValue struct {
|
|
UserAgent string `json:"user_agent"`
|
|
PackageVersion string `json:"package_version"`
|
|
RuntimeVersion string `json:"runtime_version"`
|
|
OS string `json:"os"`
|
|
Arch string `json:"arch"`
|
|
}
|
|
|
|
func ClaudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool {
|
|
if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil {
|
|
return false
|
|
}
|
|
return *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile
|
|
}
|
|
|
|
func ResetClaudeDeviceProfileCache() {
|
|
claudeDeviceProfileCacheMu.Lock()
|
|
claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
|
|
claudeDeviceProfileCacheMu.Unlock()
|
|
}
|
|
|
|
func MapStainlessOS() string {
|
|
return mapStainlessOS()
|
|
}
|
|
|
|
func MapStainlessArch() string {
|
|
return mapStainlessArch()
|
|
}
|
|
|
|
func defaultClaudeDeviceProfile(cfg *config.Config) ClaudeDeviceProfile {
|
|
hdrDefault := func(cfgVal, fallback string) string {
|
|
if strings.TrimSpace(cfgVal) != "" {
|
|
return strings.TrimSpace(cfgVal)
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
var hd config.ClaudeHeaderDefaults
|
|
if cfg != nil {
|
|
hd = cfg.ClaudeHeaderDefaults
|
|
}
|
|
|
|
profile := ClaudeDeviceProfile{
|
|
UserAgent: hdrDefault(hd.UserAgent, defaultClaudeFingerprintUserAgent),
|
|
PackageVersion: hdrDefault(hd.PackageVersion, defaultClaudeFingerprintPackageVersion),
|
|
RuntimeVersion: hdrDefault(hd.RuntimeVersion, defaultClaudeFingerprintRuntimeVersion),
|
|
OS: hdrDefault(hd.OS, defaultClaudeFingerprintOS),
|
|
Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch),
|
|
}
|
|
if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok {
|
|
profile.version = version
|
|
profile.hasVersion = true
|
|
}
|
|
return profile
|
|
}
|
|
|
|
// mapStainlessOS maps runtime.GOOS to Stainless SDK OS names.
|
|
func mapStainlessOS() string {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return "MacOS"
|
|
case "windows":
|
|
return "Windows"
|
|
case "linux":
|
|
return "Linux"
|
|
case "freebsd":
|
|
return "FreeBSD"
|
|
default:
|
|
return "Other::" + runtime.GOOS
|
|
}
|
|
}
|
|
|
|
// mapStainlessArch maps runtime.GOARCH to Stainless SDK architecture names.
|
|
func mapStainlessArch() string {
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
return "x64"
|
|
case "arm64":
|
|
return "arm64"
|
|
case "386":
|
|
return "x86"
|
|
default:
|
|
return "other::" + runtime.GOARCH
|
|
}
|
|
}
|
|
|
|
func parseClaudeCLIVersion(userAgent string) (claudeCLIVersion, bool) {
|
|
matches := claudeCLIVersionPattern.FindStringSubmatch(strings.TrimSpace(userAgent))
|
|
if len(matches) != 4 {
|
|
return claudeCLIVersion{}, false
|
|
}
|
|
major, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
return claudeCLIVersion{}, false
|
|
}
|
|
minor, err := strconv.Atoi(matches[2])
|
|
if err != nil {
|
|
return claudeCLIVersion{}, false
|
|
}
|
|
patch, err := strconv.Atoi(matches[3])
|
|
if err != nil {
|
|
return claudeCLIVersion{}, false
|
|
}
|
|
return claudeCLIVersion{major: major, minor: minor, patch: patch}, true
|
|
}
|
|
|
|
func shouldUpgradeClaudeDeviceProfile(candidate, current ClaudeDeviceProfile) bool {
|
|
if candidate.UserAgent == "" || !candidate.hasVersion {
|
|
return false
|
|
}
|
|
if current.UserAgent == "" || !current.hasVersion {
|
|
return true
|
|
}
|
|
return candidate.version.Compare(current.version) > 0
|
|
}
|
|
|
|
func pinClaudeDeviceProfilePlatform(profile, baseline ClaudeDeviceProfile) ClaudeDeviceProfile {
|
|
profile.OS = baseline.OS
|
|
profile.Arch = baseline.Arch
|
|
return profile
|
|
}
|
|
|
|
// normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current
|
|
// baseline platform and enforces the baseline software fingerprint as a floor.
|
|
func normalizeClaudeDeviceProfile(profile, baseline ClaudeDeviceProfile) ClaudeDeviceProfile {
|
|
profile = pinClaudeDeviceProfilePlatform(profile, baseline)
|
|
if profile.UserAgent == "" || !profile.hasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) {
|
|
profile.UserAgent = baseline.UserAgent
|
|
profile.PackageVersion = baseline.PackageVersion
|
|
profile.RuntimeVersion = baseline.RuntimeVersion
|
|
profile.version = baseline.version
|
|
profile.hasVersion = baseline.hasVersion
|
|
}
|
|
return profile
|
|
}
|
|
|
|
func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (ClaudeDeviceProfile, bool) {
|
|
if headers == nil {
|
|
return ClaudeDeviceProfile{}, false
|
|
}
|
|
|
|
userAgent := strings.TrimSpace(headers.Get("User-Agent"))
|
|
version, ok := parseClaudeCLIVersion(userAgent)
|
|
if !ok {
|
|
return ClaudeDeviceProfile{}, false
|
|
}
|
|
|
|
baseline := defaultClaudeDeviceProfile(cfg)
|
|
profile := ClaudeDeviceProfile{
|
|
UserAgent: userAgent,
|
|
PackageVersion: firstNonEmptyHeader(headers, "X-Stainless-Package-Version", baseline.PackageVersion),
|
|
RuntimeVersion: firstNonEmptyHeader(headers, "X-Stainless-Runtime-Version", baseline.RuntimeVersion),
|
|
OS: firstNonEmptyHeader(headers, "X-Stainless-Os", baseline.OS),
|
|
Arch: firstNonEmptyHeader(headers, "X-Stainless-Arch", baseline.Arch),
|
|
version: version,
|
|
hasVersion: true,
|
|
}
|
|
return profile, true
|
|
}
|
|
|
|
func firstNonEmptyHeader(headers http.Header, name, fallback string) string {
|
|
if headers == nil {
|
|
return fallback
|
|
}
|
|
if value := strings.TrimSpace(headers.Get(name)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func claudeDeviceProfileScopeKey(auth *cliproxyauth.Auth, apiKey string) string {
|
|
switch {
|
|
case auth != nil && strings.TrimSpace(auth.ID) != "":
|
|
return "auth:" + strings.TrimSpace(auth.ID)
|
|
case strings.TrimSpace(apiKey) != "":
|
|
return "api_key:" + strings.TrimSpace(apiKey)
|
|
default:
|
|
return "global"
|
|
}
|
|
}
|
|
|
|
func claudeDeviceProfileCacheKey(auth *cliproxyauth.Auth, apiKey string) string {
|
|
sum := sha256.Sum256([]byte(claudeDeviceProfileScopeKey(auth, apiKey)))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func claudeDeviceProfileKVKey(auth *cliproxyauth.Auth, apiKey string) string {
|
|
return "cpa:claude:device-profile:" + homekv.HashKeyPart(claudeDeviceProfileScopeKey(auth, apiKey))
|
|
}
|
|
|
|
func claudeDeviceProfileLockKVKey(auth *cliproxyauth.Auth, apiKey string) string {
|
|
return "cpa:claude:device-profile-lock:" + homekv.HashKeyPart(claudeDeviceProfileScopeKey(auth, apiKey))
|
|
}
|
|
|
|
func startClaudeDeviceProfileCacheCleanup() {
|
|
go func() {
|
|
ticker := time.NewTicker(claudeDeviceProfileCleanupPeriod)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
purgeExpiredClaudeDeviceProfiles()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func purgeExpiredClaudeDeviceProfiles() {
|
|
now := time.Now()
|
|
claudeDeviceProfileCacheMu.Lock()
|
|
for key, entry := range claudeDeviceProfileCache {
|
|
if !entry.expire.After(now) {
|
|
delete(claudeDeviceProfileCache, key)
|
|
}
|
|
}
|
|
claudeDeviceProfileCacheMu.Unlock()
|
|
}
|
|
|
|
func ResolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) ClaudeDeviceProfile {
|
|
profile, errProfile := ResolveClaudeDeviceProfileRequired(context.Background(), auth, apiKey, headers, cfg)
|
|
if errProfile != nil {
|
|
return defaultClaudeDeviceProfile(cfg)
|
|
}
|
|
return profile
|
|
}
|
|
|
|
// ResolveClaudeDeviceProfileRequired resolves a stable Claude Code device profile for request-time paths.
|
|
func ResolveClaudeDeviceProfileRequired(ctx context.Context, auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) (ClaudeDeviceProfile, error) {
|
|
client, homeMode, errClient := currentClaudeDeviceProfileKVClient()
|
|
if homeMode {
|
|
if errClient != nil {
|
|
return ClaudeDeviceProfile{}, errClient
|
|
}
|
|
return resolveClaudeDeviceProfileHome(ctx, client, auth, apiKey, headers, cfg)
|
|
}
|
|
return resolveClaudeDeviceProfileLocal(auth, apiKey, headers, cfg), nil
|
|
}
|
|
|
|
func resolveClaudeDeviceProfileLocal(auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) ClaudeDeviceProfile {
|
|
claudeDeviceProfileCacheCleanupOnce.Do(startClaudeDeviceProfileCacheCleanup)
|
|
|
|
cacheKey := claudeDeviceProfileCacheKey(auth, apiKey)
|
|
now := time.Now()
|
|
baseline := defaultClaudeDeviceProfile(cfg)
|
|
candidate, hasCandidate := extractClaudeDeviceProfile(headers, cfg)
|
|
if hasCandidate {
|
|
candidate = pinClaudeDeviceProfilePlatform(candidate, baseline)
|
|
}
|
|
if hasCandidate && !shouldUpgradeClaudeDeviceProfile(candidate, baseline) {
|
|
hasCandidate = false
|
|
}
|
|
|
|
claudeDeviceProfileCacheMu.RLock()
|
|
entry, hasCached := claudeDeviceProfileCache[cacheKey]
|
|
cachedValid := hasCached && entry.expire.After(now) && entry.profile.UserAgent != ""
|
|
claudeDeviceProfileCacheMu.RUnlock()
|
|
|
|
if hasCandidate {
|
|
if ClaudeDeviceProfileBeforeCandidateStore != nil {
|
|
ClaudeDeviceProfileBeforeCandidateStore(candidate)
|
|
}
|
|
|
|
claudeDeviceProfileCacheMu.Lock()
|
|
entry, hasCached = claudeDeviceProfileCache[cacheKey]
|
|
cachedValid = hasCached && entry.expire.After(now) && entry.profile.UserAgent != ""
|
|
if cachedValid {
|
|
entry.profile = normalizeClaudeDeviceProfile(entry.profile, baseline)
|
|
}
|
|
if cachedValid && !shouldUpgradeClaudeDeviceProfile(candidate, entry.profile) {
|
|
entry.expire = now.Add(claudeDeviceProfileTTL)
|
|
claudeDeviceProfileCache[cacheKey] = entry
|
|
claudeDeviceProfileCacheMu.Unlock()
|
|
return entry.profile
|
|
}
|
|
|
|
claudeDeviceProfileCache[cacheKey] = claudeDeviceProfileCacheEntry{
|
|
profile: candidate,
|
|
expire: now.Add(claudeDeviceProfileTTL),
|
|
}
|
|
claudeDeviceProfileCacheMu.Unlock()
|
|
return candidate
|
|
}
|
|
|
|
if cachedValid {
|
|
claudeDeviceProfileCacheMu.Lock()
|
|
entry = claudeDeviceProfileCache[cacheKey]
|
|
if entry.expire.After(now) && entry.profile.UserAgent != "" {
|
|
entry.profile = normalizeClaudeDeviceProfile(entry.profile, baseline)
|
|
entry.expire = now.Add(claudeDeviceProfileTTL)
|
|
claudeDeviceProfileCache[cacheKey] = entry
|
|
claudeDeviceProfileCacheMu.Unlock()
|
|
return entry.profile
|
|
}
|
|
claudeDeviceProfileCacheMu.Unlock()
|
|
}
|
|
|
|
return baseline
|
|
}
|
|
|
|
func resolveClaudeDeviceProfileHome(ctx context.Context, client claudeDeviceProfileKVClient, auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) (ClaudeDeviceProfile, error) {
|
|
baseline := defaultClaudeDeviceProfile(cfg)
|
|
candidate, hasCandidate := extractClaudeDeviceProfile(headers, cfg)
|
|
if hasCandidate {
|
|
candidate = pinClaudeDeviceProfilePlatform(candidate, baseline)
|
|
}
|
|
if hasCandidate && !shouldUpgradeClaudeDeviceProfile(candidate, baseline) {
|
|
hasCandidate = false
|
|
}
|
|
|
|
valueKey := claudeDeviceProfileKVKey(auth, apiKey)
|
|
if !hasCandidate {
|
|
return readClaudeDeviceProfileFromHome(ctx, client, valueKey, baseline)
|
|
}
|
|
|
|
lockKey := claudeDeviceProfileLockKVKey(auth, apiKey)
|
|
gotLock, errLock := client.KVSetNX(ctx, lockKey, []byte("1"), claudeDeviceProfileLockTTL)
|
|
if errLock != nil {
|
|
return ClaudeDeviceProfile{}, errLock
|
|
}
|
|
if ClaudeDeviceProfileBeforeCandidateStore != nil {
|
|
ClaudeDeviceProfileBeforeCandidateStore(candidate)
|
|
}
|
|
|
|
cached, found, errRead := readClaudeDeviceProfileValueFromHome(ctx, client, valueKey, baseline)
|
|
if errRead != nil {
|
|
return ClaudeDeviceProfile{}, errRead
|
|
}
|
|
if found && !shouldUpgradeClaudeDeviceProfile(candidate, cached) {
|
|
if _, errExpire := client.KVExpire(ctx, valueKey, claudeDeviceProfileTTL); errExpire != nil {
|
|
return ClaudeDeviceProfile{}, errExpire
|
|
}
|
|
return cached, nil
|
|
}
|
|
if !gotLock {
|
|
if found {
|
|
return cached, nil
|
|
}
|
|
return ClaudeDeviceProfile{}, fmt.Errorf("home kv device profile lock not acquired and profile missing")
|
|
}
|
|
|
|
if errWrite := writeClaudeDeviceProfileToHome(ctx, client, valueKey, candidate); errWrite != nil {
|
|
return ClaudeDeviceProfile{}, errWrite
|
|
}
|
|
return candidate, nil
|
|
}
|
|
|
|
func readClaudeDeviceProfileFromHome(ctx context.Context, client claudeDeviceProfileKVClient, key string, baseline ClaudeDeviceProfile) (ClaudeDeviceProfile, error) {
|
|
profile, found, errRead := readClaudeDeviceProfileValueFromHome(ctx, client, key, baseline)
|
|
if errRead != nil {
|
|
return ClaudeDeviceProfile{}, errRead
|
|
}
|
|
if !found {
|
|
return baseline, nil
|
|
}
|
|
if _, errExpire := client.KVExpire(ctx, key, claudeDeviceProfileTTL); errExpire != nil {
|
|
return ClaudeDeviceProfile{}, errExpire
|
|
}
|
|
return profile, nil
|
|
}
|
|
|
|
func readClaudeDeviceProfileValueFromHome(ctx context.Context, client claudeDeviceProfileKVClient, key string, baseline ClaudeDeviceProfile) (ClaudeDeviceProfile, bool, error) {
|
|
raw, found, errGet := client.KVGet(ctx, key)
|
|
if errGet != nil || !found {
|
|
return ClaudeDeviceProfile{}, false, errGet
|
|
}
|
|
var value claudeDeviceProfileKVValue
|
|
if errUnmarshal := json.Unmarshal(raw, &value); errUnmarshal != nil {
|
|
return ClaudeDeviceProfile{}, false, errUnmarshal
|
|
}
|
|
profile := value.ToProfile()
|
|
if strings.TrimSpace(profile.UserAgent) == "" {
|
|
return ClaudeDeviceProfile{}, false, nil
|
|
}
|
|
return normalizeClaudeDeviceProfile(profile, baseline), true, nil
|
|
}
|
|
|
|
func writeClaudeDeviceProfileToHome(ctx context.Context, client claudeDeviceProfileKVClient, key string, profile ClaudeDeviceProfile) error {
|
|
raw, errMarshal := json.Marshal(claudeDeviceProfileKVValueFromProfile(profile))
|
|
if errMarshal != nil {
|
|
return errMarshal
|
|
}
|
|
written, errSet := client.KVSet(ctx, key, raw, homekv.KVSetOptions{EX: claudeDeviceProfileTTL})
|
|
if errSet != nil {
|
|
return errSet
|
|
}
|
|
if !written {
|
|
return fmt.Errorf("home kv device profile write skipped")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func claudeDeviceProfileKVValueFromProfile(profile ClaudeDeviceProfile) claudeDeviceProfileKVValue {
|
|
return claudeDeviceProfileKVValue{
|
|
UserAgent: profile.UserAgent,
|
|
PackageVersion: profile.PackageVersion,
|
|
RuntimeVersion: profile.RuntimeVersion,
|
|
OS: profile.OS,
|
|
Arch: profile.Arch,
|
|
}
|
|
}
|
|
|
|
func (value claudeDeviceProfileKVValue) ToProfile() ClaudeDeviceProfile {
|
|
profile := ClaudeDeviceProfile{
|
|
UserAgent: strings.TrimSpace(value.UserAgent),
|
|
PackageVersion: strings.TrimSpace(value.PackageVersion),
|
|
RuntimeVersion: strings.TrimSpace(value.RuntimeVersion),
|
|
OS: strings.TrimSpace(value.OS),
|
|
Arch: strings.TrimSpace(value.Arch),
|
|
}
|
|
if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok {
|
|
profile.version = version
|
|
profile.hasVersion = true
|
|
}
|
|
return profile
|
|
}
|
|
|
|
func ApplyClaudeDeviceProfileHeaders(r *http.Request, profile ClaudeDeviceProfile) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
for _, headerName := range []string{
|
|
"User-Agent",
|
|
"X-Stainless-Package-Version",
|
|
"X-Stainless-Runtime-Version",
|
|
"X-Stainless-Os",
|
|
"X-Stainless-Arch",
|
|
} {
|
|
r.Header.Del(headerName)
|
|
}
|
|
r.Header.Set("User-Agent", profile.UserAgent)
|
|
r.Header.Set("X-Stainless-Package-Version", profile.PackageVersion)
|
|
r.Header.Set("X-Stainless-Runtime-Version", profile.RuntimeVersion)
|
|
r.Header.Set("X-Stainless-Os", profile.OS)
|
|
r.Header.Set("X-Stainless-Arch", profile.Arch)
|
|
}
|
|
|
|
// DefaultClaudeVersion returns the version string (e.g. "2.1.63") from the
|
|
// current baseline device profile. It extracts the version from the User-Agent.
|
|
func DefaultClaudeVersion(cfg *config.Config) string {
|
|
profile := defaultClaudeDeviceProfile(cfg)
|
|
if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok {
|
|
return strconv.Itoa(version.major) + "." + strconv.Itoa(version.minor) + "." + strconv.Itoa(version.patch)
|
|
}
|
|
return "2.1.63"
|
|
}
|
|
|
|
func ApplyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg *config.Config) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
profile := defaultClaudeDeviceProfile(cfg)
|
|
miscEnsure := func(name, fallback string) {
|
|
if strings.TrimSpace(r.Header.Get(name)) != "" {
|
|
return
|
|
}
|
|
if strings.TrimSpace(ginHeaders.Get(name)) != "" {
|
|
r.Header.Set(name, strings.TrimSpace(ginHeaders.Get(name)))
|
|
return
|
|
}
|
|
r.Header.Set(name, fallback)
|
|
}
|
|
|
|
miscEnsure("X-Stainless-Runtime-Version", profile.RuntimeVersion)
|
|
miscEnsure("X-Stainless-Package-Version", profile.PackageVersion)
|
|
miscEnsure("X-Stainless-Os", mapStainlessOS())
|
|
miscEnsure("X-Stainless-Arch", mapStainlessArch())
|
|
|
|
// Legacy mode preserves per-auth custom header overrides. By the time we get
|
|
// here, ApplyCustomHeadersFromAttrs has already populated r.Header.
|
|
if strings.TrimSpace(r.Header.Get("User-Agent")) != "" {
|
|
return
|
|
}
|
|
|
|
clientUA := ""
|
|
if ginHeaders != nil {
|
|
clientUA = strings.TrimSpace(ginHeaders.Get("User-Agent"))
|
|
}
|
|
if isClaudeCodeClient(clientUA) {
|
|
r.Header.Set("User-Agent", clientUA)
|
|
return
|
|
}
|
|
r.Header.Set("User-Agent", profile.UserAgent)
|
|
}
|