-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(claude): add native request cloaking for non-claude-code clients #868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,9 +67,10 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r | |
| // Inject thinking config based on model metadata for thinking variants | ||
| body = e.injectThinkingConfig(model, req.Metadata, body) | ||
|
|
||
| if !strings.HasPrefix(model, "claude-3-5-haiku") { | ||
| body = checkSystemInstructions(body) | ||
| } | ||
| // Apply cloaking (system prompt injection, fake user ID, sensitive word obfuscation) | ||
| // based on client type and configuration | ||
| body = applyCloaking(ctx, e.cfg, auth, body, model) | ||
|
|
||
| body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated) | ||
|
|
||
| // Disable thinking if tool_choice forces tool use (Anthropic API constraint) | ||
|
|
@@ -181,7 +182,11 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A | |
| body, _ = sjson.SetBytes(body, "model", model) | ||
| // Inject thinking config based on model metadata for thinking variants | ||
| body = e.injectThinkingConfig(model, req.Metadata, body) | ||
| body = checkSystemInstructions(body) | ||
|
|
||
| // Apply cloaking (system prompt injection, fake user ID, sensitive word obfuscation) | ||
| // based on client type and configuration | ||
| body = applyCloaking(ctx, e.cfg, auth, body, model) | ||
|
|
||
| body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated) | ||
|
|
||
| // Disable thinking if tool_choice forces tool use (Anthropic API constraint) | ||
|
|
@@ -770,3 +775,164 @@ func checkSystemInstructions(payload []byte) []byte { | |
| } | ||
| return payload | ||
| } | ||
|
|
||
| // getClientUserAgent extracts the client User-Agent from the gin context. | ||
| func getClientUserAgent(ctx context.Context) string { | ||
| if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { | ||
| return ginCtx.GetHeader("User-Agent") | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // getCloakConfigFromAuth extracts cloak configuration from auth attributes. | ||
| // Returns (cloakMode, strictMode, sensitiveWords). | ||
| func getCloakConfigFromAuth(auth *cliproxyauth.Auth) (string, bool, []string) { | ||
| if auth == nil || auth.Attributes == nil { | ||
| return "auto", false, nil | ||
| } | ||
|
|
||
| cloakMode := auth.Attributes["cloak_mode"] | ||
| if cloakMode == "" { | ||
| cloakMode = "auto" | ||
| } | ||
|
|
||
| strictMode := strings.ToLower(auth.Attributes["cloak_strict_mode"]) == "true" | ||
|
|
||
| var sensitiveWords []string | ||
| if wordsStr := auth.Attributes["cloak_sensitive_words"]; wordsStr != "" { | ||
| sensitiveWords = strings.Split(wordsStr, ",") | ||
| for i := range sensitiveWords { | ||
| sensitiveWords[i] = strings.TrimSpace(sensitiveWords[i]) | ||
| } | ||
| } | ||
|
|
||
| return cloakMode, strictMode, sensitiveWords | ||
| } | ||
|
|
||
| // resolveClaudeKeyCloakConfig finds the matching ClaudeKey config and returns its CloakConfig. | ||
| func resolveClaudeKeyCloakConfig(cfg *config.Config, auth *cliproxyauth.Auth) *config.CloakConfig { | ||
| if cfg == nil || auth == nil { | ||
| return nil | ||
| } | ||
|
|
||
| apiKey, baseURL := claudeCreds(auth) | ||
| if apiKey == "" { | ||
| return nil | ||
| } | ||
|
|
||
| for i := range cfg.ClaudeKey { | ||
| entry := &cfg.ClaudeKey[i] | ||
| cfgKey := strings.TrimSpace(entry.APIKey) | ||
| cfgBase := strings.TrimSpace(entry.BaseURL) | ||
|
|
||
| // Match by API key | ||
| if strings.EqualFold(cfgKey, apiKey) { | ||
| // If baseURL is specified, also check it | ||
| if baseURL != "" && cfgBase != "" && !strings.EqualFold(cfgBase, baseURL) { | ||
| continue | ||
| } | ||
| return entry.Cloak | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // injectFakeUserID generates and injects a fake user ID into the request metadata. | ||
| func injectFakeUserID(payload []byte) []byte { | ||
| metadata := gjson.GetBytes(payload, "metadata") | ||
| if !metadata.Exists() { | ||
| payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateFakeUserID()) | ||
| return payload | ||
| } | ||
|
|
||
| existingUserID := gjson.GetBytes(payload, "metadata.user_id").String() | ||
| if existingUserID == "" || !isValidUserID(existingUserID) { | ||
| payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateFakeUserID()) | ||
| } | ||
| return payload | ||
| } | ||
|
|
||
| // checkSystemInstructionsWithMode injects Claude Code system prompt. | ||
| // In strict mode, it replaces all user system messages. | ||
| // In non-strict mode (default), it prepends to existing system messages. | ||
| func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte { | ||
| system := gjson.GetBytes(payload, "system") | ||
| claudeCodeInstructions := `[{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude."}]` | ||
|
|
||
| if strictMode { | ||
| // Strict mode: replace all system messages with Claude Code prompt only | ||
| payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions)) | ||
| return payload | ||
| } | ||
|
|
||
| // Non-strict mode (default): prepend Claude Code prompt to existing system messages | ||
| if system.IsArray() { | ||
| if gjson.GetBytes(payload, "system.0.text").String() != "You are Claude Code, Anthropic's official CLI for Claude." { | ||
| system.ForEach(func(_, part gjson.Result) bool { | ||
| if part.Get("type").String() == "text" { | ||
| claudeCodeInstructions, _ = sjson.SetRaw(claudeCodeInstructions, "-1", part.Raw) | ||
| } | ||
| return true | ||
| }) | ||
| payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions)) | ||
| } | ||
| } else { | ||
| payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions)) | ||
| } | ||
| return payload | ||
| } | ||
|
|
||
| // applyCloaking applies cloaking transformations to the payload based on config and client. | ||
| // Cloaking includes: system prompt injection, fake user ID, and sensitive word obfuscation. | ||
| func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string) []byte { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While |
||
| clientUserAgent := getClientUserAgent(ctx) | ||
|
|
||
| // Get cloak config from ClaudeKey configuration | ||
| cloakCfg := resolveClaudeKeyCloakConfig(cfg, auth) | ||
|
|
||
| // Determine cloak settings | ||
| var cloakMode string | ||
| var strictMode bool | ||
| var sensitiveWords []string | ||
|
|
||
| if cloakCfg != nil { | ||
| cloakMode = cloakCfg.Mode | ||
| strictMode = cloakCfg.StrictMode | ||
| sensitiveWords = cloakCfg.SensitiveWords | ||
| } | ||
|
|
||
| // Fallback to auth attributes if no config found | ||
| if cloakMode == "" { | ||
| attrMode, attrStrict, attrWords := getCloakConfigFromAuth(auth) | ||
| cloakMode = attrMode | ||
| if !strictMode { | ||
| strictMode = attrStrict | ||
| } | ||
| if len(sensitiveWords) == 0 { | ||
| sensitiveWords = attrWords | ||
| } | ||
| } | ||
|
|
||
| // Determine if cloaking should be applied | ||
| if !shouldCloak(cloakMode, clientUserAgent) { | ||
| return payload | ||
| } | ||
|
|
||
| // Skip system instructions for claude-3-5-haiku models | ||
| if !strings.HasPrefix(model, "claude-3-5-haiku") { | ||
| payload = checkSystemInstructionsWithMode(payload, strictMode) | ||
| } | ||
|
|
||
| // Inject fake user ID | ||
| payload = injectFakeUserID(payload) | ||
|
|
||
| // Apply sensitive word obfuscation | ||
| if len(sensitiveWords) > 0 { | ||
| matcher := buildSensitiveWordMatcher(sensitiveWords) | ||
| payload = obfuscateSensitiveWords(payload, matcher) | ||
| } | ||
|
|
||
| return payload | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When prepending the Claude Code system prompt, the current logic only preserves existing system prompt blocks of
type: "text". Any other content block types, such as images, will be dropped from the request. This can lead to data loss and incorrect model behavior. To fix this, all parts of the original system prompt should be preserved, regardless of their type.