Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,18 @@ error-logs-max-files: 10
# When false, disable in-memory usage statistics aggregation
usage-statistics-enabled: false

# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
# Global proxy URL for all outgoing connections (API requests + OAuth token refresh).
# Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080
#
# Proxy resolution priority (highest to lowest):
# 1. Per-account proxy_url in auth credential JSON file (e.g. claude-user@example.com.json)
# 2. This global proxy-url
# 3. HTTPS_PROXY / HTTP_PROXY environment variables (standard http.Transport only)
#
# IMPORTANT: OAuth token refresh uses a custom TLS transport that does NOT read
# HTTPS_PROXY environment variables. If you rely on env vars for proxy routing,
# you must ALSO set this field or add "proxy_url" to your credential JSON files,
# otherwise token refresh requests will attempt direct connections.
proxy-url: ""

# When true, unprefixed model requests only use credentials without a prefix (except when prefix == model name).
Expand Down
17 changes: 13 additions & 4 deletions internal/auth/claude/anthropic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,25 @@ type ClaudeAuth struct {
// It initializes the HTTP client with a custom TLS transport that uses Firefox
// fingerprint to bypass Cloudflare's TLS fingerprinting on Anthropic domains.
//
// An optional proxyURL may be provided to override the proxy in cfg.SDKConfig.
// This allows callers to pass a pre-resolved proxy URL (e.g. from ResolveProxyURL)
// so that both request and refresh paths share the same proxy priority logic.
//
// Parameters:
// - cfg: The application configuration containing proxy settings
// - proxyURL: Optional pre-resolved proxy URL that overrides cfg.SDKConfig.ProxyURL
//
// Returns:
// - *ClaudeAuth: A new Claude authentication service instance
func NewClaudeAuth(cfg *config.Config) *ClaudeAuth {
// Use custom HTTP client with Firefox TLS fingerprint to bypass
// Cloudflare's bot detection on Anthropic domains
func NewClaudeAuth(cfg *config.Config, proxyURL ...string) *ClaudeAuth {
sdkCfg := &cfg.SDKConfig
if len(proxyURL) > 0 && proxyURL[0] != "" {
cfgCopy := cfg.SDKConfig
cfgCopy.ProxyURL = proxyURL[0]
sdkCfg = &cfgCopy
}
return &ClaudeAuth{
httpClient: NewAnthropicHttpClient(&cfg.SDKConfig),
httpClient: NewAnthropicHttpClient(sdkCfg),
}
}

Expand Down
5 changes: 4 additions & 1 deletion internal/runtime/executor/claude_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,10 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (
if refreshToken == "" {
return auth, nil
}
svc := claudeauth.NewClaudeAuth(e.cfg)
// Use per-account proxy_url for token refresh, matching the priority in
// proxy_helpers.go: auth.ProxyURL > cfg.ProxyURL > env vars.
proxyURL := ResolveProxyURL(e.cfg, auth)
svc := claudeauth.NewClaudeAuth(e.cfg, proxyURL)
td, err := svc.RefreshTokens(ctx, refreshToken)
if err != nil {
return nil, err
Expand Down
29 changes: 19 additions & 10 deletions internal/runtime/executor/proxy_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ import (
"golang.org/x/net/proxy"
)

// ResolveProxyURL returns the effective proxy URL following the standard priority:
// 1. auth.ProxyURL (per-account override)
// 2. cfg.ProxyURL (global config)
// 3. "" (empty — caller decides fallback behavior)
func ResolveProxyURL(cfg *config.Config, auth *cliproxyauth.Auth) string {
if auth != nil {
if u := strings.TrimSpace(auth.ProxyURL); u != "" {
return u
}
}
if cfg != nil {
if u := strings.TrimSpace(cfg.ProxyURL); u != "" {
return u
}
}
return ""
}

// newProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority:
// 1. Use auth.ProxyURL if configured (highest priority)
// 2. Use cfg.ProxyURL if auth proxy is not configured
Expand All @@ -33,16 +51,7 @@ func newProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip
httpClient.Timeout = timeout
}

// Priority 1: Use auth.ProxyURL if configured
var proxyURL string
if auth != nil {
proxyURL = strings.TrimSpace(auth.ProxyURL)
}

// Priority 2: Use cfg.ProxyURL if auth proxy is not configured
if proxyURL == "" && cfg != nil {
proxyURL = strings.TrimSpace(cfg.ProxyURL)
}
proxyURL := ResolveProxyURL(cfg, auth)

// If we have a proxy URL configured, set up the transport
if proxyURL != "" {
Expand Down
6 changes: 5 additions & 1 deletion sdk/cliproxy/auth/conductor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2178,8 +2178,12 @@ func (m *Manager) refreshAuth(ctx context.Context, id string) {
log.Debugf("refresh canceled for %s, %s", auth.Provider, auth.ID)
return
}
log.Debugf("refreshed %s, %s, %v", auth.Provider, auth.ID, err)
now := time.Now()
if err != nil {
log.Warnf("auth refresh failed for %s/%s: %v", auth.Provider, auth.ID, err)
} else {
log.Debugf("auth refresh ok for %s/%s", auth.Provider, auth.ID)
}
if err != nil {
m.mu.Lock()
if current := m.auths[id]; current != nil {
Expand Down