99 "math"
1010 "net"
1111 "net/http"
12+ "path/filepath"
1213 "slices"
1314 "strconv"
1415 "sync"
@@ -29,9 +30,11 @@ type defaultCredential struct {
2930 tag string
3031 serviceContext context.Context
3132 credentialPath string
33+ claudeDirectory string
3234 credentialFilePath string
3335 configDir string
34- statePath string
36+ deviceID string
37+ configLoaded bool
3538 credentials * oauthCredentials
3639 access sync.RWMutex
3740 state credentialState
@@ -109,7 +112,7 @@ func newDefaultCredential(ctx context.Context, tag string, options option.CCMDef
109112 tag : tag ,
110113 serviceContext : ctx ,
111114 credentialPath : options .CredentialPath ,
112- statePath : options .StatePath ,
115+ claudeDirectory : options .ClaudeDirectory ,
113116 cap5h : cap5h ,
114117 capWeekly : capWeekly ,
115118 forwardHTTPClient : httpClient ,
@@ -129,13 +132,18 @@ func newDefaultCredential(ctx context.Context, tag string, options option.CCMDef
129132}
130133
131134func (c * defaultCredential ) start () error {
135+ if c .claudeDirectory != "" {
136+ c .loadClaudeCodeConfig ()
137+ if c .credentialPath == "" {
138+ c .credentialPath = filepath .Join (c .claudeDirectory , ".credentials.json" )
139+ }
140+ }
132141 credentialFilePath , err := resolveCredentialFilePath (c .credentialPath )
133142 if err != nil {
134143 return E .Cause (err , "resolve credential path for " , c .tag )
135144 }
136145 c .credentialFilePath = credentialFilePath
137146 c .configDir = resolveConfigDir (c .credentialPath , credentialFilePath )
138- c .loadPersistedState ()
139147 err = c .ensureCredentialWatcher ()
140148 if err != nil {
141149 c .logger .Debug ("start credential watcher for " , c .tag , ": " , err )
@@ -154,6 +162,28 @@ func (c *defaultCredential) start() error {
154162 return nil
155163}
156164
165+ func (c * defaultCredential ) loadClaudeCodeConfig () {
166+ configFilePath := resolveClaudeConfigFile (c .claudeDirectory )
167+ if configFilePath == "" {
168+ return
169+ }
170+ config , err := readClaudeCodeConfig (configFilePath )
171+ if err != nil {
172+ c .logger .Warn ("read claude code config for " , c .tag , ": " , err )
173+ return
174+ }
175+ c .stateAccess .Lock ()
176+ if config .OAuthAccount != nil && config .OAuthAccount .AccountUUID != "" {
177+ c .state .accountUUID = config .OAuthAccount .AccountUUID
178+ }
179+ c .stateAccess .Unlock ()
180+ if config .UserID != "" {
181+ c .deviceID = config .UserID
182+ }
183+ c .configLoaded = true
184+ c .logger .Debug ("loaded claude code config for " , c .tag , ": account=" , c .state .accountUUID , ", device=" , c .deviceID )
185+ }
186+
157187func (c * defaultCredential ) setStatusSubscriber (subscriber * observable.Subscriber [struct {}]) {
158188 c .statusSubscriber = subscriber
159189}
@@ -697,7 +727,7 @@ func (c *defaultCredential) pollUsage() {
697727 }
698728 c .logger .Debug ("poll usage for " , c .tag , ": 5h=" , c .state .fiveHourUtilization , "%, weekly=" , c .state .weeklyUtilization , "%" , resetSuffix )
699729 }
700- needsProfileFetch := c .state .rateLimitTier == ""
730+ needsProfileFetch := ! c . configLoaded && c .state .rateLimitTier == ""
701731 shouldInterrupt := c .checkTransitionLocked ()
702732 c .stateAccess .Unlock ()
703733 if shouldInterrupt {
@@ -782,7 +812,6 @@ func (c *defaultCredential) fetchProfile(httpClient *http.Client, accessToken st
782812 if shouldEmit {
783813 c .emitStatusUpdate ()
784814 }
785- c .savePersistedState ()
786815 c .logger .Info ("fetched profile for " , c .tag , ": type=" , resolvedAccountType , ", tier=" , rateLimitTier , ", weight=" , ccmPlanWeight (resolvedAccountType , rateLimitTier ))
787816}
788817
@@ -841,7 +870,7 @@ func (c *defaultCredential) buildProxyRequest(ctx context.Context, original *htt
841870 proxyURL := claudeAPIBaseURL + original .URL .RequestURI ()
842871 var body io.Reader
843872 if bodyBytes != nil {
844- bodyBytes = c .injectAccountUUID (bodyBytes )
873+ bodyBytes = c .injectMetadataFields (bodyBytes )
845874 body = bytes .NewReader (bodyBytes )
846875 } else {
847876 body = original .Body
@@ -878,24 +907,20 @@ func (c *defaultCredential) buildProxyRequest(ctx context.Context, original *htt
878907 return proxyRequest , nil
879908}
880909
881- // injectAccountUUID fills in the account_uuid field in metadata.user_id
882- // when the client sends it empty (e.g. using ANTHROPIC_AUTH_TOKEN).
910+ // injectMetadataFields fills in account_uuid and device_id in metadata.user_id
911+ // when the client sends them empty (e.g. using ANTHROPIC_AUTH_TOKEN).
883912//
884913// Claude Code >= 2.1.78 (@anthropic-ai/claude-code) sets metadata as:
885914//
886915// {user_id: JSON.stringify({device_id, account_uuid, session_id})}
887916//
888917// ref: cli.js L66() — metadata constructor
889- //
890- // account_uuid is populated from oauthAccount.accountUuid which comes from
891- // the /api/oauth/profile endpoint (ref: cli.js EX1() → fP6()).
892- // When the client uses ANTHROPIC_AUTH_TOKEN instead of Claude AI OAuth,
893- // account_uuid is empty. We inject it from the fetchProfile result.
894- func (c * defaultCredential ) injectAccountUUID (bodyBytes []byte ) []byte {
918+ func (c * defaultCredential ) injectMetadataFields (bodyBytes []byte ) []byte {
895919 c .stateAccess .RLock ()
896920 accountUUID := c .state .accountUUID
897921 c .stateAccess .RUnlock ()
898- if accountUUID == "" {
922+ deviceID := c .deviceID
923+ if accountUUID == "" && deviceID == "" {
899924 return bodyBytes
900925 }
901926
@@ -931,19 +956,43 @@ func (c *defaultCredential) injectAccountUUID(bodyBytes []byte) []byte {
931956 return bodyBytes
932957 }
933958
934- existingRaw , hasExisting := userIDObject ["account_uuid" ]
935- if hasExisting {
936- var existing string
937- if json .Unmarshal (existingRaw , & existing ) == nil && existing != "" {
938- return bodyBytes
959+ modified := false
960+
961+ if accountUUID != "" {
962+ existingRaw , hasExisting := userIDObject ["account_uuid" ]
963+ needsInject := ! hasExisting
964+ if hasExisting {
965+ var existing string
966+ needsInject = json .Unmarshal (existingRaw , & existing ) != nil || existing == ""
967+ }
968+ if needsInject {
969+ accountUUIDJSON , marshalErr := json .Marshal (accountUUID )
970+ if marshalErr == nil {
971+ userIDObject ["account_uuid" ] = json .RawMessage (accountUUIDJSON )
972+ modified = true
973+ }
939974 }
940975 }
941976
942- accountUUIDJSON , err := json .Marshal (accountUUID )
943- if err != nil {
977+ if deviceID != "" {
978+ existingRaw , hasExisting := userIDObject ["device_id" ]
979+ needsInject := ! hasExisting
980+ if hasExisting {
981+ var existing string
982+ needsInject = json .Unmarshal (existingRaw , & existing ) != nil || existing == ""
983+ }
984+ if needsInject {
985+ deviceIDJSON , marshalErr := json .Marshal (deviceID )
986+ if marshalErr == nil {
987+ userIDObject ["device_id" ] = json .RawMessage (deviceIDJSON )
988+ modified = true
989+ }
990+ }
991+ }
992+
993+ if ! modified {
944994 return bodyBytes
945995 }
946- userIDObject ["account_uuid" ] = json .RawMessage (accountUUIDJSON )
947996
948997 newUserIDBytes , err := json .Marshal (userIDObject )
949998 if err != nil {
0 commit comments