Skip to content

Commit 7b7f243

Browse files
authored
Merge pull request router-for-me#2 from jc01rho/merge-upstream-v6.8.24-16401083871630863435
Merge upstream v6.8.24 and tag v6.8.24-2
2 parents d1c2afc + a5e36a5 commit 7b7f243

File tree

7 files changed

+365
-13
lines changed

7 files changed

+365
-13
lines changed

config.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ nonstream-keepalive-interval: 0
172172
# sensitive-words: # optional: words to obfuscate with zero-width characters
173173
# - "API"
174174
# - "proxy"
175+
# cache-user-id: true # optional: default is false; set true to reuse cached user_id per API key instead of generating a random one each request
175176

176177
# Default headers for Claude API requests. Update when Claude Code releases new versions.
177178
# These are used as fallbacks when the client does not send its own headers.

internal/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ type CloakConfig struct {
330330
// SensitiveWords is a list of words to obfuscate with zero-width characters.
331331
// This can help bypass certain content filters.
332332
SensitiveWords []string `yaml:"sensitive-words,omitempty" json:"sensitive-words,omitempty"`
333+
334+
// CacheUserID controls whether Claude user_id values are cached per API key.
335+
// When false, a fresh random user_id is generated for every request.
336+
CacheUserID *bool `yaml:"cache-user-id,omitempty" json:"cache-user-id,omitempty"`
333337
}
334338

335339
// ClaudeKey represents the configuration for a Claude API key,

internal/registry/model_definitions_static_data.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,21 @@ func GetGeminiModels() []*ModelInfo {
196196
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
197197
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
198198
},
199+
{
200+
ID: "gemini-3.1-pro-preview",
201+
Object: "model",
202+
Created: 1771459200,
203+
OwnedBy: "google",
204+
Type: "gemini",
205+
Name: "models/gemini-3.1-pro-preview",
206+
Version: "3.1",
207+
DisplayName: "Gemini 3.1 Pro Preview",
208+
Description: "Gemini 3.1 Pro Preview",
209+
InputTokenLimit: 1048576,
210+
OutputTokenLimit: 65536,
211+
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
212+
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
213+
},
199214
{
200215
ID: "gemini-3-flash-preview",
201216
Object: "model",
@@ -309,7 +324,7 @@ func GetGeminiVertexModels() []*ModelInfo {
309324
{
310325
ID: "gemini-3.1-pro-preview",
311326
Object: "model",
312-
Created: 1771491385,
327+
Created: 1771459200,
313328
OwnedBy: "google",
314329
Type: "gemini",
315330
Name: "models/gemini-3.1-pro-preview",
@@ -559,6 +574,21 @@ func GetAIStudioModels() []*ModelInfo {
559574
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
560575
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
561576
},
577+
{
578+
ID: "gemini-3.1-pro-preview",
579+
Object: "model",
580+
Created: 1771459200,
581+
OwnedBy: "google",
582+
Type: "gemini",
583+
Name: "models/gemini-3.1-pro-preview",
584+
Version: "3.1",
585+
DisplayName: "Gemini 3.1 Pro Preview",
586+
Description: "Gemini 3.1 Pro Preview",
587+
InputTokenLimit: 1048576,
588+
OutputTokenLimit: 65536,
589+
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
590+
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
591+
},
562592
{
563593
ID: "gemini-3-flash-preview",
564594
Object: "model",

internal/runtime/executor/claude_executor.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
117117

118118
// Apply cloaking (system prompt injection, fake user ID, sensitive word obfuscation)
119119
// based on client type and configuration.
120-
body = applyCloaking(ctx, e.cfg, auth, body, baseModel)
120+
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
121121

122122
requestedModel := payloadRequestedModel(opts, req.Model)
123123
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
@@ -266,7 +266,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
266266

267267
// Apply cloaking (system prompt injection, fake user ID, sensitive word obfuscation)
268268
// based on client type and configuration.
269-
body = applyCloaking(ctx, e.cfg, auth, body, baseModel)
269+
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
270270

271271
requestedModel := payloadRequestedModel(opts, req.Model)
272272
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
@@ -990,10 +990,10 @@ func getClientUserAgent(ctx context.Context) string {
990990
}
991991

992992
// getCloakConfigFromAuth extracts cloak configuration from auth attributes.
993-
// Returns (cloakMode, strictMode, sensitiveWords).
994-
func getCloakConfigFromAuth(auth *cliproxyauth.Auth) (string, bool, []string) {
993+
// Returns (cloakMode, strictMode, sensitiveWords, cacheUserID).
994+
func getCloakConfigFromAuth(auth *cliproxyauth.Auth) (string, bool, []string, bool) {
995995
if auth == nil || auth.Attributes == nil {
996-
return "auto", false, nil
996+
return "auto", false, nil, false
997997
}
998998

999999
cloakMode := auth.Attributes["cloak_mode"]
@@ -1011,7 +1011,9 @@ func getCloakConfigFromAuth(auth *cliproxyauth.Auth) (string, bool, []string) {
10111011
}
10121012
}
10131013

1014-
return cloakMode, strictMode, sensitiveWords
1014+
cacheUserID := strings.EqualFold(strings.TrimSpace(auth.Attributes["cloak_cache_user_id"]), "true")
1015+
1016+
return cloakMode, strictMode, sensitiveWords, cacheUserID
10151017
}
10161018

10171019
// resolveClaudeKeyCloakConfig finds the matching ClaudeKey config and returns its CloakConfig.
@@ -1044,16 +1046,24 @@ func resolveClaudeKeyCloakConfig(cfg *config.Config, auth *cliproxyauth.Auth) *c
10441046
}
10451047

10461048
// injectFakeUserID generates and injects a fake user ID into the request metadata.
1047-
func injectFakeUserID(payload []byte) []byte {
1049+
// When useCache is false, a new user ID is generated for every call.
1050+
func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
1051+
generateID := func() string {
1052+
if useCache {
1053+
return cachedUserID(apiKey)
1054+
}
1055+
return generateFakeUserID()
1056+
}
1057+
10481058
metadata := gjson.GetBytes(payload, "metadata")
10491059
if !metadata.Exists() {
1050-
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateFakeUserID())
1060+
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID())
10511061
return payload
10521062
}
10531063

10541064
existingUserID := gjson.GetBytes(payload, "metadata.user_id").String()
10551065
if existingUserID == "" || !isValidUserID(existingUserID) {
1056-
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateFakeUserID())
1066+
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID())
10571067
}
10581068
return payload
10591069
}
@@ -1090,7 +1100,7 @@ func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte {
10901100

10911101
// applyCloaking applies cloaking transformations to the payload based on config and client.
10921102
// Cloaking includes: system prompt injection, fake user ID, and sensitive word obfuscation.
1093-
func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string) []byte {
1103+
func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string, apiKey string) []byte {
10941104
clientUserAgent := getClientUserAgent(ctx)
10951105

10961106
// Get cloak config from ClaudeKey configuration
@@ -1100,23 +1110,33 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
11001110
var cloakMode string
11011111
var strictMode bool
11021112
var sensitiveWords []string
1113+
var cacheUserID bool
11031114

11041115
if cloakCfg != nil {
11051116
cloakMode = cloakCfg.Mode
11061117
strictMode = cloakCfg.StrictMode
11071118
sensitiveWords = cloakCfg.SensitiveWords
1119+
if cloakCfg.CacheUserID != nil {
1120+
cacheUserID = *cloakCfg.CacheUserID
1121+
}
11081122
}
11091123

11101124
// Fallback to auth attributes if no config found
11111125
if cloakMode == "" {
1112-
attrMode, attrStrict, attrWords := getCloakConfigFromAuth(auth)
1126+
attrMode, attrStrict, attrWords, attrCache := getCloakConfigFromAuth(auth)
11131127
cloakMode = attrMode
11141128
if !strictMode {
11151129
strictMode = attrStrict
11161130
}
11171131
if len(sensitiveWords) == 0 {
11181132
sensitiveWords = attrWords
11191133
}
1134+
if cloakCfg == nil || cloakCfg.CacheUserID == nil {
1135+
cacheUserID = attrCache
1136+
}
1137+
} else if cloakCfg == nil || cloakCfg.CacheUserID == nil {
1138+
_, _, _, attrCache := getCloakConfigFromAuth(auth)
1139+
cacheUserID = attrCache
11201140
}
11211141

11221142
// Determine if cloaking should be applied
@@ -1130,7 +1150,7 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
11301150
}
11311151

11321152
// Inject fake user ID
1133-
payload = injectFakeUserID(payload)
1153+
payload = injectFakeUserID(payload, apiKey, cacheUserID)
11341154

11351155
// Apply sensitive word obfuscation
11361156
if len(sensitiveWords) > 0 {

internal/runtime/executor/claude_executor_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@ package executor
22

33
import (
44
"bytes"
5+
"context"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
59
"testing"
610

11+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
12+
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
13+
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
14+
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
715
"github.com/tidwall/gjson"
16+
"github.com/tidwall/sjson"
817
)
918

1019
func TestApplyClaudeToolPrefix(t *testing.T) {
@@ -199,6 +208,119 @@ func TestApplyClaudeToolPrefix_NestedToolReference(t *testing.T) {
199208
}
200209
}
201210

211+
func TestClaudeExecutor_ReusesUserIDAcrossModelsWhenCacheEnabled(t *testing.T) {
212+
resetUserIDCache()
213+
214+
var userIDs []string
215+
var requestModels []string
216+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
217+
body, _ := io.ReadAll(r.Body)
218+
userID := gjson.GetBytes(body, "metadata.user_id").String()
219+
model := gjson.GetBytes(body, "model").String()
220+
userIDs = append(userIDs, userID)
221+
requestModels = append(requestModels, model)
222+
t.Logf("HTTP Server received request: model=%s, user_id=%s, url=%s", model, userID, r.URL.String())
223+
w.Header().Set("Content-Type", "application/json")
224+
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
225+
}))
226+
defer server.Close()
227+
228+
t.Logf("End-to-end test: Fake HTTP server started at %s", server.URL)
229+
230+
cacheEnabled := true
231+
executor := NewClaudeExecutor(&config.Config{
232+
ClaudeKey: []config.ClaudeKey{
233+
{
234+
APIKey: "key-123",
235+
BaseURL: server.URL,
236+
Cloak: &config.CloakConfig{
237+
CacheUserID: &cacheEnabled,
238+
},
239+
},
240+
},
241+
})
242+
auth := &cliproxyauth.Auth{Attributes: map[string]string{
243+
"api_key": "key-123",
244+
"base_url": server.URL,
245+
}}
246+
247+
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
248+
models := []string{"claude-3-5-sonnet", "claude-3-5-haiku"}
249+
for _, model := range models {
250+
t.Logf("Sending request for model: %s", model)
251+
modelPayload, _ := sjson.SetBytes(payload, "model", model)
252+
if _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
253+
Model: model,
254+
Payload: modelPayload,
255+
}, cliproxyexecutor.Options{
256+
SourceFormat: sdktranslator.FromString("claude"),
257+
}); err != nil {
258+
t.Fatalf("Execute(%s) error: %v", model, err)
259+
}
260+
}
261+
262+
if len(userIDs) != 2 {
263+
t.Fatalf("expected 2 requests, got %d", len(userIDs))
264+
}
265+
if userIDs[0] == "" || userIDs[1] == "" {
266+
t.Fatal("expected user_id to be populated")
267+
}
268+
t.Logf("user_id[0] (model=%s): %s", requestModels[0], userIDs[0])
269+
t.Logf("user_id[1] (model=%s): %s", requestModels[1], userIDs[1])
270+
if userIDs[0] != userIDs[1] {
271+
t.Fatalf("expected user_id to be reused across models, got %q and %q", userIDs[0], userIDs[1])
272+
}
273+
if !isValidUserID(userIDs[0]) {
274+
t.Fatalf("user_id %q is not valid", userIDs[0])
275+
}
276+
t.Logf("✓ End-to-end test passed: Same user_id (%s) was used for both models", userIDs[0])
277+
}
278+
279+
func TestClaudeExecutor_GeneratesNewUserIDByDefault(t *testing.T) {
280+
resetUserIDCache()
281+
282+
var userIDs []string
283+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
284+
body, _ := io.ReadAll(r.Body)
285+
userIDs = append(userIDs, gjson.GetBytes(body, "metadata.user_id").String())
286+
w.Header().Set("Content-Type", "application/json")
287+
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
288+
}))
289+
defer server.Close()
290+
291+
executor := NewClaudeExecutor(&config.Config{})
292+
auth := &cliproxyauth.Auth{Attributes: map[string]string{
293+
"api_key": "key-123",
294+
"base_url": server.URL,
295+
}}
296+
297+
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
298+
299+
for i := 0; i < 2; i++ {
300+
if _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
301+
Model: "claude-3-5-sonnet",
302+
Payload: payload,
303+
}, cliproxyexecutor.Options{
304+
SourceFormat: sdktranslator.FromString("claude"),
305+
}); err != nil {
306+
t.Fatalf("Execute call %d error: %v", i, err)
307+
}
308+
}
309+
310+
if len(userIDs) != 2 {
311+
t.Fatalf("expected 2 requests, got %d", len(userIDs))
312+
}
313+
if userIDs[0] == "" || userIDs[1] == "" {
314+
t.Fatal("expected user_id to be populated")
315+
}
316+
if userIDs[0] == userIDs[1] {
317+
t.Fatalf("expected user_id to change when caching is not enabled, got identical values %q", userIDs[0])
318+
}
319+
if !isValidUserID(userIDs[0]) || !isValidUserID(userIDs[1]) {
320+
t.Fatalf("user_ids should be valid, got %q and %q", userIDs[0], userIDs[1])
321+
}
322+
}
323+
202324
func TestStripClaudeToolPrefixFromResponse_NestedToolReference(t *testing.T) {
203325
input := []byte(`{"content":[{"type":"tool_result","tool_use_id":"toolu_123","content":[{"type":"tool_reference","tool_name":"proxy_mcp__nia__manage_resource"}]}]}`)
204326
out := stripClaudeToolPrefixFromResponse(input, "proxy_")

0 commit comments

Comments
 (0)