diff --git a/config.example.yaml b/config.example.yaml index 40bb87210a..291ae38135 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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). diff --git a/internal/auth/claude/anthropic_auth.go b/internal/auth/claude/anthropic_auth.go index 2853e418e6..21504b53b7 100644 --- a/internal/auth/claude/anthropic_auth.go +++ b/internal/auth/claude/anthropic_auth.go @@ -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), } } diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 7d0ddcf2d2..4a5e8272f2 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -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 diff --git a/internal/runtime/executor/proxy_helpers.go b/internal/runtime/executor/proxy_helpers.go index ab0f626acc..0e8d2cca7a 100644 --- a/internal/runtime/executor/proxy_helpers.go +++ b/internal/runtime/executor/proxy_helpers.go @@ -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 @@ -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 != "" { diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index ae5b745c98..abb220a605 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -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 {