Skip to content

feat: add adaptive Codex quota cooldown#4400

Closed
936220035 wants to merge 5 commits into
router-for-me:mainfrom
936220035:codex/cpa-adaptive-cooldown-upstream-20260717
Closed

feat: add adaptive Codex quota cooldown#4400
936220035 wants to merge 5 commits into
router-for-me:mainfrom
936220035:codex/cpa-adaptive-cooldown-upstream-20260717

Conversation

@936220035

Copy link
Copy Markdown

Summary

  • classify Codex HTTP 429 responses into transient throttling and explicit usage-limit exhaustion
  • use bounded adaptive backoff for transient throttling, while preserving provider reset times for usage limits
  • add non-consuming WHAM usage probes with shared quota-bucket deduplication and strict recovery checks
  • persist the optional next probe time and migrate legacy cooldown state without breaking old files
  • keep the adaptive policy disabled by default
  • include prerequisite Go 1.26 race/vet fixes for async test cleanup, watcher synchronization, and immutable returned stream headers

Safety

  • no active model requests are introduced by the probe path
  • failed, incomplete, or ambiguous usage responses never unlock credentials
  • returned stream headers are frozen before the API returns
  • the Seoul-specific three-hour compatibility patch is not included as an upstream behavior

Verification

  • Windows: focused affected packages x20, full go test -count=1 ./..., and server build
  • NAS Linux Go 1.26.1 with vendored dependencies and GOMAXPROCS=2:
    • full go vet ./...
    • five critical adaptive cooldown race tests x100 each
    • full go test -race -count=1 -p 2 ./...
    • linux/amd64 CGO build
  • NAS source archive SHA-256: d0e328e020d1cbe6b4b508919293fc4f75ed2e9158d45b12d52e8d239af1ba28
  • NAS binary SHA-256: f70e47ea2e11158d2033ee4893a1aa4494dd3e5022a3503e93c5a7d909938cea

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces adaptive Codex cooldowns and non-consuming quota probes that periodically query the ChatGPT WHAM usage endpoint to check rate limits. It also includes resource cleanup improvements in stream handlers. Key feedback highlights a critical spin lock risk in the quota probe loop upon context cancellation, a potential nil pointer dereference when handling stream retry results, and potential deadlocks in test channel communications.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +103 to +110
func (m *Manager) runCodexQuotaProbeLoop(ctx context.Context, wake <-chan struct{}) {
for {
now := time.Now()
due, next := m.collectCodexQuotaProbeBuckets(now)
if len(due) > 0 {
m.probeDueCodexQuotaBuckets(ctx, due)
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If ctx is canceled while there are due buckets, probeDueCodexQuotaBuckets will return immediately without probing or updating the buckets' NextProbeAt. The loop will then continue and immediately find the same buckets due, calling probeDueCodexQuotaBuckets again in an infinite busy loop, spinning the CPU at 100%. Adding a non-blocking check for ctx.Done() at the start of the loop prevents this spin lock.

func (m *Manager) runCodexQuotaProbeLoop(ctx context.Context, wake <-chan struct{}) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}
		now := time.Now()
		due, next := m.collectCodexQuotaProbeBuckets(now)
		if len(due) > 0 {
			m.probeDueCodexQuotaBuckets(ctx, due)
			continue
		}

Comment on lines +1309 to +1314
retryResult, retryErr := h.AuthManager.ExecuteStream(ctx, providers, req, opts)
if retryErr != nil {
pendingChunks[0].Err = enrichAuthSelectionError(retryErr, providers, normalizedModel)
break
}
rawStreamHeaders = cloneHeader(retryResult.Headers)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If retryErr is nil but retryResult is also nil, dereferencing retryResult.Headers will cause a nil pointer dereference panic. Adding a nil check for retryResult ensures robust error handling.

Suggested change
retryResult, retryErr := h.AuthManager.ExecuteStream(ctx, providers, req, opts)
if retryErr != nil {
pendingChunks[0].Err = enrichAuthSelectionError(retryErr, providers, normalizedModel)
break
}
rawStreamHeaders = cloneHeader(retryResult.Headers)
retryResult, retryErr := h.AuthManager.ExecuteStream(ctx, providers, req, opts)
if retryErr != nil {
pendingChunks[0].Err = enrichAuthSelectionError(retryErr, providers, normalizedModel)
break
}
if retryResult == nil {
pendingChunks[0].Err = &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: fmt.Errorf("empty stream result")}
break
}
rawStreamHeaders = cloneHeader(retryResult.Headers)

Comment on lines 1528 to 1534
func (s *stubStore) PersistConfig(context.Context) error {
atomic.AddInt32(&s.cfgPersisted, 1)
if s.cfgDone != nil {
s.cfgDone <- struct{}{}
}
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If PersistConfig is called multiple times concurrently or sequentially in tests, sending to s.cfgDone can block indefinitely if the channel buffer is full and no receiver is active. Using a non-blocking select send prevents potential deadlocks in tests.

func (s *stubStore) PersistConfig(context.Context) error {
	atomic.AddInt32(&s.cfgPersisted, 1)
	if s.cfgDone != nil {
		select {
		case s.cfgDone <- struct{}{}:
		default:
		}
	}
	return nil
}

Comment on lines 1535 to 1545
func (s *stubStore) PersistAuthFiles(_ context.Context, message string, paths ...string) error {
atomic.AddInt32(&s.authPersisted, 1)
s.mu.Lock()
s.lastAuthMessage = message
s.lastAuthPaths = paths
s.lastAuthPaths = append([]string(nil), paths...)
s.mu.Unlock()
if s.authDone != nil {
s.authDone <- struct{}{}
}
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly to PersistConfig, sending to s.authDone can block indefinitely if called multiple times. Using a non-blocking select send ensures the test stub remains robust and deadlock-free.

func (s *stubStore) PersistAuthFiles(_ context.Context, message string, paths ...string) error {
	atomic.AddInt32(&s.authPersisted, 1)
	s.mu.Lock()
	s.lastAuthMessage = message
	s.lastAuthPaths = append([]string(nil), paths...)
	s.mu.Unlock()
	if s.authDone != nil {
		select {
		case s.authDone <- struct{}{}:
		default:
		}
	}
	return nil
}

@936220035

Copy link
Copy Markdown
Author

Closing this duplicate. The first create request succeeded as draft PR #4399 and was automatically retargeted to dev by the repository workflow.

@936220035 936220035 closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant