@@ -3,17 +3,23 @@ package ccm
33import (
44 "bytes"
55 "context"
6+ stdTLS "crypto/tls"
67 "encoding/json"
78 "io"
9+ "net"
810 "net/http"
911 "strconv"
1012 "strings"
1113 "sync"
1214 "time"
1315
16+ "github.com/sagernet/sing-box/adapter"
17+ "github.com/sagernet/sing-box/common/dialer"
1418 "github.com/sagernet/sing-box/log"
1519 "github.com/sagernet/sing-box/option"
1620 E "github.com/sagernet/sing/common/exceptions"
21+ M "github.com/sagernet/sing/common/metadata"
22+ "github.com/sagernet/sing/common/ntp"
1723)
1824
1925const defaultPollInterval = 60 * time .Second
@@ -44,7 +50,29 @@ type defaultCredential struct {
4450 logger log.ContextLogger
4551}
4652
47- func newDefaultCredential (tag string , options option.CCMDefaultCredentialOptions , httpClient * http.Client , logger log.ContextLogger ) * defaultCredential {
53+ func newDefaultCredential (ctx context.Context , tag string , options option.CCMDefaultCredentialOptions , logger log.ContextLogger ) (* defaultCredential , error ) {
54+ credentialDialer , err := dialer .NewWithOptions (dialer.Options {
55+ Context : ctx ,
56+ Options : option.DialerOptions {
57+ Detour : options .Detour ,
58+ },
59+ RemoteIsDomain : true ,
60+ })
61+ if err != nil {
62+ return nil , E .Cause (err , "create dialer for credential " , tag )
63+ }
64+ httpClient := & http.Client {
65+ Transport : & http.Transport {
66+ ForceAttemptHTTP2 : true ,
67+ TLSClientConfig : & stdTLS.Config {
68+ RootCAs : adapter .RootPoolFromContext (ctx ),
69+ Time : ntp .TimeFuncFromContext (ctx ),
70+ },
71+ DialContext : func (ctx context.Context , network , addr string ) (net.Conn , error ) {
72+ return credentialDialer .DialContext (ctx , network , M .ParseSocksaddr (addr ))
73+ },
74+ },
75+ }
4876 credential := & defaultCredential {
4977 tag : tag ,
5078 credentialPath : options .CredentialPath ,
@@ -61,7 +89,7 @@ func newDefaultCredential(tag string, options option.CCMDefaultCredentialOptions
6189 logger : logger ,
6290 }
6391 }
64- return credential
92+ return credential , nil
6593}
6694
6795func (c * defaultCredential ) start () error {
@@ -118,6 +146,17 @@ func (c *defaultCredential) getAccessToken() (string, error) {
118146 return newCredentials .AccessToken , nil
119147}
120148
149+ func parseResetTimestamp (value string ) (time.Time , error ) {
150+ if value == "" {
151+ return time.Time {}, nil
152+ }
153+ unixEpoch , err := strconv .ParseInt (value , 10 , 64 )
154+ if err == nil {
155+ return time .Unix (unixEpoch , 0 ), nil
156+ }
157+ return time .Parse (time .RFC3339Nano , value )
158+ }
159+
121160func (c * defaultCredential ) updateStateFromHeaders (headers http.Header ) {
122161 c .stateMutex .Lock ()
123162 defer c .stateMutex .Unlock ()
@@ -129,9 +168,9 @@ func (c *defaultCredential) updateStateFromHeaders(headers http.Header) {
129168 }
130169 }
131170 if resetAt := headers .Get ("anthropic-ratelimit-unified-5h-reset" ); resetAt != "" {
132- value , err := strconv . ParseInt (resetAt , 10 , 64 )
171+ value , err := parseResetTimestamp (resetAt )
133172 if err == nil {
134- c .state .fiveHourReset = time . Unix ( value , 0 )
173+ c .state .fiveHourReset = value
135174 }
136175 }
137176 if utilization := headers .Get ("anthropic-ratelimit-unified-7d-utilization" ); utilization != "" {
@@ -141,9 +180,9 @@ func (c *defaultCredential) updateStateFromHeaders(headers http.Header) {
141180 }
142181 }
143182 if resetAt := headers .Get ("anthropic-ratelimit-unified-7d-reset" ); resetAt != "" {
144- value , err := strconv . ParseInt (resetAt , 10 , 64 )
183+ value , err := parseResetTimestamp (resetAt )
145184 if err == nil {
146- c .state .weeklyReset = time . Unix ( value , 0 )
185+ c .state .weeklyReset = value
147186 }
148187 }
149188 c .state .lastUpdated = time .Now ()
@@ -244,6 +283,7 @@ func (c *defaultCredential) pollUsage(ctx context.Context) {
244283 }
245284 request .Header .Set ("Authorization" , "Bearer " + accessToken )
246285 request .Header .Set ("Content-Type" , "application/json" )
286+ request .Header .Set ("anthropic-beta" , anthropicBetaOAuthValue )
247287
248288 httpClient := & http.Client {
249289 Transport : c .httpClient .Transport ,
@@ -265,11 +305,11 @@ func (c *defaultCredential) pollUsage(ctx context.Context) {
265305 var usageResponse struct {
266306 FiveHour struct {
267307 Utilization float64 `json:"utilization"`
268- ResetsAt int64 `json:"resets_at"`
308+ ResetsAt string `json:"resets_at"`
269309 } `json:"five_hour"`
270310 SevenDay struct {
271311 Utilization float64 `json:"utilization"`
272- ResetsAt int64 `json:"resets_at"`
312+ ResetsAt string `json:"resets_at"`
273313 } `json:"seven_day"`
274314 }
275315 err = json .NewDecoder (response .Body ).Decode (& usageResponse )
@@ -281,12 +321,14 @@ func (c *defaultCredential) pollUsage(ctx context.Context) {
281321 c .stateMutex .Lock ()
282322 defer c .stateMutex .Unlock ()
283323 c .state .fiveHourUtilization = usageResponse .FiveHour .Utilization * 100
284- if usageResponse .FiveHour .ResetsAt > 0 {
285- c .state .fiveHourReset = time .Unix (usageResponse .FiveHour .ResetsAt , 0 )
324+ fiveHourReset , err := parseResetTimestamp (usageResponse .FiveHour .ResetsAt )
325+ if err == nil && ! fiveHourReset .IsZero () {
326+ c .state .fiveHourReset = fiveHourReset
286327 }
287328 c .state .weeklyUtilization = usageResponse .SevenDay .Utilization * 100
288- if usageResponse .SevenDay .ResetsAt > 0 {
289- c .state .weeklyReset = time .Unix (usageResponse .SevenDay .ResetsAt , 0 )
329+ weeklyReset , err := parseResetTimestamp (usageResponse .SevenDay .ResetsAt )
330+ if err == nil && ! weeklyReset .IsZero () {
331+ c .state .weeklyReset = weeklyReset
290332 }
291333 if c .state .hardRateLimited && time .Now ().After (c .state .rateLimitResetAt ) {
292334 c .state .hardRateLimited = false
@@ -548,8 +590,8 @@ func extractCCMSessionID(bodyBytes []byte) string {
548590}
549591
550592func buildCredentialProviders (
593+ ctx context.Context ,
551594 options option.CCMServiceOptions ,
552- httpClient * http.Client ,
553595 logger log.ContextLogger ,
554596) (map [string ]credentialProvider , []* defaultCredential , error ) {
555597 defaultCredentials := make (map [string ]* defaultCredential )
@@ -559,7 +601,10 @@ func buildCredentialProviders(
559601 for _ , credOpt := range options .Credentials {
560602 switch credOpt .Type {
561603 case "default" :
562- credential := newDefaultCredential (credOpt .Tag , credOpt .DefaultOptions , httpClient , logger )
604+ credential , err := newDefaultCredential (ctx , credOpt .Tag , credOpt .DefaultOptions , logger )
605+ if err != nil {
606+ return nil , nil , err
607+ }
563608 defaultCredentials [credOpt .Tag ] = credential
564609 allDefaults = append (allDefaults , credential )
565610 providers [credOpt .Tag ] = & singleCredentialProvider {credential : credential }
@@ -645,13 +690,17 @@ func validateCCMOptions(options option.CCMServiceOptions) error {
645690 hasCredentials := len (options .Credentials ) > 0
646691 hasLegacyPath := options .CredentialPath != ""
647692 hasLegacyUsages := options .UsagesPath != ""
693+ hasLegacyDetour := options .Detour != ""
648694
649695 if hasCredentials && hasLegacyPath {
650696 return E .New ("credential_path and credentials are mutually exclusive" )
651697 }
652698 if hasCredentials && hasLegacyUsages {
653699 return E .New ("usages_path and credentials are mutually exclusive; use usages_path on individual credentials" )
654700 }
701+ if hasCredentials && hasLegacyDetour {
702+ return E .New ("detour and credentials are mutually exclusive; use detour on individual credentials" )
703+ }
655704
656705 if hasCredentials {
657706 tags := make (map [string ]bool )
@@ -685,7 +734,6 @@ func validateCCMOptions(options option.CCMServiceOptions) error {
685734
686735// retryRequestWithBody re-sends a buffered request body using a different credential.
687736func retryRequestWithBody (
688- httpClient * http.Client ,
689737 originalRequest * http.Request ,
690738 bodyBytes []byte ,
691739 credential * defaultCredential ,
@@ -726,7 +774,7 @@ func retryRequestWithBody(
726774 }
727775 retryRequest .Header .Set ("Authorization" , "Bearer " + accessToken )
728776
729- return httpClient .Do (retryRequest )
777+ return credential . httpClient .Do (retryRequest )
730778}
731779
732780// credentialForUser finds the credential provider for a user.
0 commit comments