refactor(claude): deduplicate CLI identity application and tidy helpers

- Extract the credential-identity block shared by the streaming and
  non-streaming Claude paths into applyClaudeCLIIdentity, so the identity
  seed choice (API key versus stable Kimi auth identity) cannot drift
  between the two paths
- Move stripDefaultKimiClaudeCodeAttribution next to the other attribution
  and CCH helpers in claude_signing.go; it is only called from the Claude
  executor paths and never from the Kimi executor itself
- Reattach the addConfigHeadersToAttrs doc comment to its function in the
  watcher synthesizer helpers
This commit is contained in:
sususu
2026-08-18 18:21:33 +08:00
parent aec70dfec4
commit f3e836ce6c
6 changed files with 49 additions and 39 deletions

View File

@@ -171,22 +171,9 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
}
bodyForUpstream = sanitizeClaudeMessagesForClaudeUpstreamWithDebug(ctx, bodyForUpstream, baseModel, helps.APIKeyModelIsCompat(req))
if fp.ApplyCLIIdentity {
// ApplyCLIIdentity and ProfileClaudeCodeCLI are the same predicate, so
// claudeSessionID was already resolved above by ClaudeAgentSessionUUIDForRequest,
// which always returns a UUID. Do not add a second session source here: a
// per-apiKey cached ID would silently break agent-conversation continuity.
identitySeed := apiKey
if isKimiMessagesUpstream(auth, url) {
identitySeed = helps.ClaudeCLIAuthIdentitySeed(auth)
}
var identityAuth *cliproxyauth.Auth
identityAuth, err = helps.PrepareClaudeCLIFingerprintAuth(auth, identitySeed, fp.SynthesizeIdentity)
bodyForUpstream, err = applyClaudeCLIIdentity(bodyForUpstream, auth, apiKey, url, claudeSessionID, fp.SynthesizeIdentity)
if err != nil {
return resp, fmt.Errorf("ensure Claude CLI fingerprint identity: %w", err)
}
bodyForUpstream, _, err = helps.ApplyClaudeCredentialMetadata(bodyForUpstream, identityAuth, claudeSessionID)
if err != nil {
return resp, fmt.Errorf("apply Claude credential metadata: %w", err)
return resp, err
}
}
cchBilling := ""

View File

@@ -163,22 +163,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
}
bodyForUpstream = sanitizeClaudeMessagesForClaudeUpstreamWithDebug(ctx, bodyForUpstream, baseModel, helps.APIKeyModelIsCompat(req))
if fp.ApplyCLIIdentity {
// ApplyCLIIdentity and ProfileClaudeCodeCLI are the same predicate, so
// claudeSessionID was already resolved above by ClaudeAgentSessionUUIDForRequest,
// which always returns a UUID. Do not add a second session source here: a
// per-apiKey cached ID would silently break agent-conversation continuity.
identitySeed := apiKey
if isKimiMessagesUpstream(auth, url) {
identitySeed = helps.ClaudeCLIAuthIdentitySeed(auth)
}
var identityAuth *cliproxyauth.Auth
identityAuth, err = helps.PrepareClaudeCLIFingerprintAuth(auth, identitySeed, fp.SynthesizeIdentity)
bodyForUpstream, err = applyClaudeCLIIdentity(bodyForUpstream, auth, apiKey, url, claudeSessionID, fp.SynthesizeIdentity)
if err != nil {
return nil, fmt.Errorf("ensure Claude CLI fingerprint identity: %w", err)
}
bodyForUpstream, _, err = helps.ApplyClaudeCredentialMetadata(bodyForUpstream, identityAuth, claudeSessionID)
if err != nil {
return nil, fmt.Errorf("apply Claude credential metadata: %w", err)
return nil, err
}
}
cchBilling := ""

View File

@@ -1,11 +1,13 @@
package executor
import (
"fmt"
"strings"
"sync"
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)
@@ -109,3 +111,31 @@ func resolveClaudeFingerprintPolicy(cfg *config.Config, auth *cliproxyauth.Auth,
OAuthCancellation: authIsOAuth,
}
}
// applyClaudeCLIIdentity applies the Claude Code CLI credential identity to the
// upstream Messages body. It is the single implementation behind both the
// streaming and the non-streaming request paths; keep it that way.
//
// ApplyCLIIdentity and ProfileClaudeCodeCLI are the same predicate, so
// sessionID has already been resolved by ClaudeAgentSessionUUIDForRequest,
// which always returns a UUID. Do not add a second session source here: a
// per-apiKey cached ID would silently break agent-conversation continuity.
//
// API keys seed the synthesized identity from the key itself; delegated
// providers such as Kimi seed from the stable auth identity, so an access-token
// rotation does not rotate the device fingerprint.
func applyClaudeCLIIdentity(body []byte, auth *cliproxyauth.Auth, apiKey, upstreamURL, sessionID string, synthesize bool) ([]byte, error) {
identitySeed := apiKey
if isKimiMessagesUpstream(auth, upstreamURL) {
identitySeed = helps.ClaudeCLIAuthIdentitySeed(auth)
}
identityAuth, errIdentity := helps.PrepareClaudeCLIFingerprintAuth(auth, identitySeed, synthesize)
if errIdentity != nil {
return nil, fmt.Errorf("ensure Claude CLI fingerprint identity: %w", errIdentity)
}
updated, _, errApply := helps.ApplyClaudeCredentialMetadata(body, identityAuth, sessionID)
if errApply != nil {
return nil, fmt.Errorf("apply Claude credential metadata: %w", errApply)
}
return updated, nil
}

View File

@@ -10,6 +10,7 @@ import (
xxHash64 "github.com/pierrec/xxHash/xxHash64"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -160,6 +161,18 @@ func isKimiMessagesUpstream(auth *cliproxyauth.Auth, endpoint string) bool {
return isKimiAPIEndpoint(endpoint)
}
// stripDefaultKimiClaudeCodeAttribution removes the Claude Code billing/CCH
// attribution block from a Kimi Messages body when the caller did not opt into
// the full CLI profile. Kimi treats the block as prompt text, so forwarding it
// unchanged would leak CPA's attribution into the model's context. Other system
// content is preserved.
func stripDefaultKimiClaudeCodeAttribution(auth *cliproxyauth.Auth, endpoint string, cliFingerprint bool, body []byte) []byte {
if cliFingerprint || !isKimiMessagesUpstream(auth, endpoint) {
return body
}
return util.StripClaudeCodeAttributionSystem(body)
}
// claudeCCHSigningEnabled applies CPA's CCH policy.
//
// Native gate, identical in Claude Code 2.1.220 through 2.1.234:

View File

@@ -50,13 +50,6 @@ func NewKimiExecutor(cfg *config.Config) *KimiExecutor {
// Identifier returns the executor identifier.
func (e *KimiExecutor) Identifier() string { return "kimi" }
func stripDefaultKimiClaudeCodeAttribution(auth *cliproxyauth.Auth, endpoint string, cliFingerprint bool, body []byte) []byte {
if cliFingerprint || !isKimiMessagesUpstream(auth, endpoint) {
return body
}
return util.StripClaudeCodeAttributionSystem(body)
}
// RequestToFormat reports the upstream request format used after auth selection.
func (e *KimiExecutor) RequestToFormat(_ cliproxyexecutor.Request, opts cliproxyexecutor.Options) sdktranslator.Format {
if opts.SourceFormat == sdktranslator.FormatClaude {

View File

@@ -120,8 +120,6 @@ func addRequestScopedErrorsToMetadata(rules []config.RequestScopedErrorRule, met
metadata["request_scoped_errors"] = rules
}
// addConfigHeadersToAttrs adds header configuration to auth attributes.
// Headers are prefixed with "header:" in the attributes map.
func fingerprintProfileFromMetadata(metadata map[string]any) string {
if metadata == nil {
return ""
@@ -152,6 +150,8 @@ func applyFingerprintProfileAttribute(auth *coreauth.Auth, metadata map[string]a
auth.Attributes["fingerprint_profile"] = profile
}
// addConfigHeadersToAttrs adds header configuration to auth attributes.
// Headers are prefixed with "header:" in the attributes map.
func addConfigHeadersToAttrs(headers map[string]string, attrs map[string]string) {
if len(headers) == 0 || attrs == nil {
return