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
3 changes: 3 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type Config struct {
// CredentialInFlight configures credential observation snapshots.
CredentialInFlight CredentialInFlightConfig `yaml:"credential-in-flight" json:"credential-in-flight"`

// CredentialProber configures optional active credential health probing.
CredentialProber CredentialProberConfig `yaml:"credential-prober" json:"credential-prober"`

// RemoteManagement nests management-related options under 'remote-management'.
RemoteManagement RemoteManagement `yaml:"remote-management" json:"-"`

Expand Down
49 changes: 49 additions & 0 deletions internal/config/prober.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package config

import "time"

const (
defaultCredentialProberInterval = 60 * time.Second
defaultCredentialProberTimeout = 10 * time.Second
defaultCredentialProberMaxConcurrency = 4
defaultCredentialProberRatePerMinute = 60
defaultCredentialProberBackoffBase = 5 * time.Second
defaultCredentialProberBackoffMax = 5 * time.Minute
defaultCredentialProberPath = "/v1/models"
)

// CredentialProberConfig controls optional active health probing for registered credentials.
// When enabled, the conductor periodically issues a lightweight HTTP probe per credential
// and feeds failures into the existing cooldown/suspension machinery.
type CredentialProberConfig struct {
// Enabled turns active credential health probing on. Default false.
Enabled bool `yaml:"enabled" json:"enabled"`
// Interval is the period between probe sweeps. Default 60s.
Interval time.Duration `yaml:"interval" json:"interval"`
// Timeout is the maximum duration a single probe request may take. Default 10s.
Timeout time.Duration `yaml:"timeout" json:"timeout"`
// MaxConcurrency limits the number of in-flight probes. Default 4.
MaxConcurrency int `yaml:"max-concurrency" json:"max-concurrency"`
// RateLimitPerMinute caps the number of probe requests across all credentials per minute. Default 60.
RateLimitPerMinute int `yaml:"rate-limit-per-minute" json:"rate-limit-per-minute"`
// BackoffBase is the initial cooldown applied when a probe fails. Default 5s.
BackoffBase time.Duration `yaml:"backoff-base" json:"backoff-base"`
// BackoffMax is the maximum probe-induced cooldown. Default 5m.
BackoffMax time.Duration `yaml:"backoff-max" json:"backoff-max"`
// DefaultProbePath is the HTTP path appended to the credential base_url for the probe. Default /v1/models.
DefaultProbePath string `yaml:"default-probe-path" json:"default-probe-path"`
}

// DefaultCredentialProberConfig returns the prober default configuration.
func DefaultCredentialProberConfig() CredentialProberConfig {
return CredentialProberConfig{
Enabled: false,
Interval: defaultCredentialProberInterval,
Timeout: defaultCredentialProberTimeout,
MaxConcurrency: defaultCredentialProberMaxConcurrency,
RateLimitPerMinute: defaultCredentialProberRatePerMinute,
BackoffBase: defaultCredentialProberBackoffBase,
BackoffMax: defaultCredentialProberBackoffMax,
DefaultProbePath: defaultCredentialProberPath,
}
}
4 changes: 4 additions & 0 deletions sdk/cliproxy/auth/conductor.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ type Manager struct {
refreshCancel context.CancelFunc
refreshLoop *authAutoRefreshLoop

// Active credential prober state
proberCancel context.CancelFunc
proberLoop *authProberLoop

requestPrepareLocks sync.Map
// refreshLocks serializes credential refresh per auth ID so concurrent
// 401 recoveries and auto-refresh workers do not race the same refresh_token.
Expand Down
21 changes: 15 additions & 6 deletions sdk/cliproxy/auth/conductor_cooldown.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,12 @@ func (m *Manager) SetConfig(cfg *internalconfig.Config) {
return
}
m.configCooldownMu.Lock()
defer m.configCooldownMu.Unlock()
if m.setConfigSnapshotLocked(cfg) {
cleared := m.setConfigSnapshotLocked(cfg)
if cleared {
m.persistCooldownStatesLocked(context.Background())
}
m.configCooldownMu.Unlock()
m.restartProber(cfg)
}

// SetConfigSnapshot updates only in-memory configuration state. It reports whether
Expand All @@ -123,8 +125,10 @@ func (m *Manager) SetConfigSnapshot(cfg *internalconfig.Config) bool {
return false
}
m.configCooldownMu.Lock()
defer m.configCooldownMu.Unlock()
return m.setConfigSnapshotLocked(cfg)
cleared := m.setConfigSnapshotLocked(cfg)
m.configCooldownMu.Unlock()
m.restartProber(cfg)
return cleared
}

func (m *Manager) setConfigSnapshotLocked(cfg *internalconfig.Config) bool {
Expand Down Expand Up @@ -171,26 +175,31 @@ func (m *Manager) ApplyConfigWithCooldownStateStore(ctx context.Context, cfg *in
}

m.configCooldownMu.Lock()
defer m.configCooldownMu.Unlock()
m.mu.RLock()
oldStore := m.cooldownStore
m.mu.RUnlock()
m.setConfigSnapshotLocked(cfg)
if oldStore != nil && !m.persistCooldownStatesToLocked(ctx, oldStore) {
m.configCooldownMu.Unlock()
return false
}
if errContext := ctx.Err(); errContext != nil {
m.configCooldownMu.Unlock()
return false
}
m.mu.Lock()
defer m.mu.Unlock()
if m.cooldownStore != oldStore {
m.mu.Unlock()
m.configCooldownMu.Unlock()
return false
}
if m.pendingCooldownStateStore == oldStore {
m.pendingCooldownStateStore = nil
}
m.cooldownStore = store
m.mu.Unlock()
m.configCooldownMu.Unlock()
m.restartProber(cfg)
return true
}

Expand Down
Loading
Loading