Files
CLIProxyAPI/internal/runtime/executor/helps/claude_cli_identity_seed.go
sususu98 f1b0431c77 feat(claude): add fingerprint-profile=claude-code-cli for API keys and delegated providers (#5047)
* feat(config): add fingerprint-profile to Claude keys and auth JSON

- Add FingerprintProfile to ClaudeKey configuration struct and normalizer
- Track fingerprint-profile in config diff
- Map fingerprint-profile / fingerprint_profile to auth attributes in file and config synthesizers
- Support fingerprint-profile in Management API PatchClaudeKey and normalization
- Add Claude billing attribution string manipulation utilities in internal/util
- Document fingerprint-profile options in config.example.yaml

* feat(claude): add fingerprint policy and request-local CLI identity

- Centralize Claude fingerprint policy resolution in claude_fingerprint_policy.go
- Support stable Claude CLI identity synthesis (UUIDv5 account_uuid and SHA-256 device_id)
  seeded from API keys or stable OAuth IDs, keeping access tokens isolated
- Warn on unrecognized fingerprint-profile values

* feat(claude): apply CLI fingerprint to Messages and keep API keys caller-owned

- Wire centralized fingerprint policy into Claude and Kimi executors
- Keep first-party Anthropic API keys and delegated providers caller-owned by default
- Apply Claude Code CLI wire profile (betas, metadata, diagnostics, MCP aliases)
  when fingerprint-profile=claude-code-cli is configured
- Strictly align CCH signing with native Claude Code 2.1.220: only first-party
  api.anthropic.com and Vertex sign dynamic CCH; third-party gateways and Kimi
  receive billing header without cch= to avoid prompt cache busting
- Respect caller-owned count_tokens bodies by default while aligning CLI shape on opt-in
- Fall back to CLIProxyAPI/<version> User-Agent when caller sends no UA in caller-owned mode
- Scope custom operator header overrides accurately in caller-owned mode
- Add comprehensive test coverage for policy resolution, gateway opt-in, Kimi, and token counting
2026-08-18 17:29:26 +08:00

93 lines
3.3 KiB
Go

package helps
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"github.com/google/uuid"
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// Stable identity seeds for fingerprint-profile=claude-code-cli on non-OAuth credentials.
// Real OAuth credentials keep their stored account/device pool; this only fills gaps
// so ApplyClaudeCredentialMetadata can run as the single identity algorithm.
var claudeCLIIdentityNamespace = uuid.MustParse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
func stableClaudeCLIDeviceID(seed string) string {
sum := sha256.Sum256([]byte("cpa-claude-code-cli-device|" + seed))
return hex.EncodeToString(sum[:])
}
func stableClaudeCLIAccountUUID(seed string) string {
return uuid.NewSHA1(claudeCLIIdentityNamespace, []byte("cpa-claude-code-cli-account|"+seed)).String()
}
// ClaudeCLIAuthIdentitySeed returns a stable credential identity that does not
// rotate with delegated-provider access tokens.
func ClaudeCLIAuthIdentitySeed(auth *cliproxyauth.Auth) string {
if auth != nil {
if id := strings.TrimSpace(auth.ID); id != "" {
return "auth-id|" + id
}
if index := strings.TrimSpace(auth.Index); index != "" {
return "auth-index|" + index
}
if fileName := strings.TrimSpace(auth.FileName); fileName != "" {
return "auth-file|" + fileName
}
}
return ""
}
// PrepareClaudeCLIFingerprintAuth returns the auth object that should receive
// ApplyClaudeCredentialMetadata. Synthesized API-key / delegated-provider
// identity is written to a clone so the shared credential metadata map is not
// mutated on the request path.
func PrepareClaudeCLIFingerprintAuth(auth *cliproxyauth.Auth, seed string, synthesizeMissing bool) (*cliproxyauth.Auth, error) {
if auth == nil {
return nil, fmt.Errorf("auth is nil")
}
if !synthesizeMissing {
return auth, nil
}
local := auth.Clone()
if err := EnsureClaudeCLIFingerprintIdentity(local, seed, true); err != nil {
return nil, err
}
return local, nil
}
// EnsureClaudeCLIFingerprintIdentity prepares auth.Metadata so the shared
// ApplyClaudeCredentialMetadata path can run.
//
// When synthesizeMissing is false (real OAuth), this is a no-op: missing account
// or device data must surface as credential errors.
// When synthesizeMissing is true (fingerprint-profile=claude-code-cli on API keys),
// missing account_uuid / device pool are filled with stable values derived from seed.
// Callers that hold a shared Auth must use PrepareClaudeCLIFingerprintAuth instead.
func EnsureClaudeCLIFingerprintIdentity(auth *cliproxyauth.Auth, seed string, synthesizeMissing bool) error {
if auth == nil {
return fmt.Errorf("auth is nil")
}
if !synthesizeMissing {
return nil
}
seed = strings.TrimSpace(seed)
if seed == "" {
seed = "anonymous"
}
if ClaudeCredentialAccountUUID(auth) == "" {
claudeauth.StoreMetadataString(&auth.Metadata, "account_uuid", stableClaudeCLIAccountUUID(seed))
}
if !claudeauth.HasCanonicalDeviceIDPool(claudeauth.ReadDeviceIDPool(&auth.Metadata)) {
claudeauth.StoreDeviceIDPool(&auth.Metadata, []string{stableClaudeCLIDeviceID(seed)})
}
if _, _, errPool := claudeauth.EnsureDeviceIDPoolFor(&auth.Metadata); errPool != nil {
return fmt.Errorf("ensure device pool: %w", errPool)
}
return nil
}