Skip to content

fix(auth): surface real upstream 429 when all pool entries exhausted (#40960) - #57137

Open
justdoit829 wants to merge 1 commit into
NousResearch:mainfrom
justdoit829:fix/credential-pool-exhaustion-misleading-401
Open

fix(auth): surface real upstream 429 when all pool entries exhausted (#40960)#57137
justdoit829 wants to merge 1 commit into
NousResearch:mainfrom
justdoit829:fix/credential-pool-exhaustion-misleading-401

Conversation

@justdoit829

Copy link
Copy Markdown

Summary

Fix issue #40960: when all credential pool entries are in exhaustion cooldown (e.g. after a 429 quota-exhausted response), _resolve_api_key_provider_secret() silently returned an empty api_key. This caused the API request to fail with a misleading 401 Unauthorized, hiding the real cause (a 429 quota message from the provider).

Root Cause

hermes_cli/auth.py L593-605: when pool.peek() returns None (all entries in cooldown), the code returns an empty string for api_key instead of falling back to an exhausted entry.

Fix

After peek() returns None, iterate pool._entries and return the first usable key with a warning log. The upstream API then returns its real 429 with the quota-reset timestamp, giving users actionable information.

Test Plan

  • Syntax check: ast.parse() passes
  • Locally verified pool returns 401-masked-error before fix
  • CI passes (GitHub Actions)
  • Maintainer review

Related

…ousResearch#40960)

When all credential pool entries are in exhaustion cooldown (e.g. after
a 429 quota-exhausted response from the upstream provider),
_resolve_api_key_provider_secret() silently returned an empty api_key.
This caused the API request to fail with a misleading 401 Unauthorized
error, hiding the real cause (a 429 quota message from the provider).

Fix: after pool.peek() returns None, iterate pool._entries and return
the first usable key from an exhausted entry, with a warning log. The
upstream API then returns its real 429 with the quota-reset timestamp,
giving users actionable information about when their quota will recover.

Closes NousResearch#40960
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists duplicate This issue or pull request already exists labels Jul 2, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #40961 - same fix in hermes_cli/auth.py (_resolve_api_key_provider_secret): fall through to an exhausted pool entry's key so the upstream returns its real 429 instead of a misleading 401. #40961 is the earliest open PR for #40960; #42475 is a sibling fix. Human should pick the canonical one from the cluster.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026

@teknium1 teknium1 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.

Thanks for tracing the empty-key path. The premise is still present on current main: hermes_cli/auth.py:595-609 returns an empty key when pool.peek() has no available entry.

Problems

  • The new loop at hermes_cli/auth.py:611 reads every raw entry, including STATUS_DEAD credentials. agent/credential_pool.py:1483-1516 deliberately excludes dead entries and entries inside exhaustion cooldowns; returning either can still produce the misleading 401 this change aims to avoid.
  • Reissuing a request with a cooldown entry conflicts with the current recovery policy: run_agent.py:293-315 says retrying an exhausted quota burns retry budget, and the documented pool flow falls through to a configured fallback after all keys are exhausted (website/docs/user-guide/features/credential-pools.md:24-40).
  • No regression test exercises peek() is None with exhausted/dead entries; the current fallback tests only cover a healthy pool entry (tests/tools/test_credential_pool_env_fallback.py:181-250).

Suggested changes

  • Avoid raw _entries access; route the intended all-exhausted behavior through a pool API that preserves dead-entry and cooldown semantics.
  • Add focused exhausted/dead-pool regression coverage after agreeing on the intended fallback or diagnostic behavior.

This is an automated hermes-sweeper review.

Comment thread hermes_cli/auth.py
try:
for exhausted_entry in pool._entries:
ex_key = (
getattr(exhausted_entry, "access_token", "")

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.

This raw iteration includes STATUS_DEAD entries as well as cooldown entries. CredentialPool._available_entries() intentionally excludes dead credentials (agent/credential_pool.py:1483-1516); returning one here can send a permanently invalid key and retain the same 401 failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] credential pool exhaustion causes misleading 401 instead of real 429/402

4 participants