Skip to content

fix(credential-pool): honor fuzzy throttle windows, stop cooldown bypass, wait for transient recovery - #82375

Open
someaka wants to merge 1 commit into
NousResearch:mainfrom
someaka:fix/custom-pool-402-rotation-final
Open

fix(credential-pool): honor fuzzy throttle windows, stop cooldown bypass, wait for transient recovery#82375
someaka wants to merge 1 commit into
NousResearch:mainfrom
someaka:fix/custom-pool-402-rotation-final

Conversation

@someaka

@someaka someaka commented Aug 9, 2026

Copy link
Copy Markdown

What this fixes

Real-world failure class observed against a 3-key custom-provider pool
(hyper.charm.land):

  1. A transient upstream 429 with body "Please try again in a few minutes."
    benched the funded key for the full 1-hour default TTL — the fuzzy retry
    guidance was never parsed.
  2. Rotation then walked into two genuinely-depleted 402 keys, the pool hit
    "no available entries", and the turn aborted as billing-exhausted — while
    the funded key recovered in minutes.
  3. While the pool had keys benched, runtime resolution silently fell through
    to the config singleton key (custom_providers.api_key), serving keys the
    pool had benched for billing and burning requests on depleted accounts —
    the source of the "random" 402s.
  4. Benches on config-seeded (borrowed) entries were additionally wiped on
    every load_pool(), because _upsert_entry treated secret re-hydration
    as a key rotation — so billing benches on the config key did not survive
    process restarts.

Changes

  1. Cooldown sizing (agent/credential_pool.py):
    _extract_retry_delay_seconds now parses fuzzy retry guidance
    ("try again in a few minutes" → 180 s, "in N minutes" → N×60 capped at
    600 s, "in a moment/shortly" → 60 s). The parsed window rides
    error_context → reset_at → _exhausted_until, so the bench matches the
    provider's own guidance. Message-derived windows carry provenance
    (MESSAGE_DERIVED_RESET_TAG) and are dropped for billing-classified
    failures (preserving the HTTP 402 (payment required) incorrectly retried as transient error — causes runaway token spend #31273 money-burn protection) while
    provider-supplied structured reset_at fields/headers are kept.
  2. Cooldown bypass (hermes_cli/runtime_provider.py):
    _try_resolve_from_custom_pool now reports pool exhaustion state and all
    three fallthrough call sites filter candidates through
    _pick_custom_api_key — billing-benched keys are never served. An
    all-benched pool raises AuthError(code="insufficient_credits"), which
    the gateway already routes to the fallback chain / billing-exhaustion UX.
  3. Recoverable-exhaustion wait (agent/agent_runtime_helpers.py): when
    rotation reaches "no available entries" and the earliest benched entry is
    transient (not billing) and recovers within 600 s, the recovery path waits
    (interruptible, with activity heartbeat) and retries instead of aborting.
    Pure-billing exhaustion still aborts immediately. Auxiliary background
    calls are intentionally exempt (documented at _recover_provider_pool).
  4. Fingerprint-aware rotation detection
    (agent/credential_pool.py::_upsert_entry): re-hydrating the same secret
    no longer clears persisted benches; a genuinely new key in config still
    does.

Relationship to existing work

Tests

  • New: tests/agent/test_credential_pool_402_flapping.py — 36 cases covering
    fuzzy parsing, billing guards (incl. the full extractor → pool runtime
    seam), bench persistence across reload from disk, the resolution bench
    filter, wait-assessment semantics (incl. mixed transient/billing pools),
    and single-key/no-pool legacy behavior.
  • Regression: 182 passed across the credential-pool and runtime-resolution
    suites. One pre-existing unrelated failure
    (test_qwen_oauth_auto_fallthrough_on_auth_failure) fails identically on
    the parent commit — qwen-oauth auto-resolution fallthrough, untouched by
    this diff.
  • Perf: hot path unchanged (select() ≈ baseline); the exhaustion probe adds
    cost only in the all-benched state; config cache intact.
  • Robustness: suite ×2 flake-free; 8-thread rotation storm ×3 (12,000 ops,
    zero torn writes); scripted flapping simulation 16/16; auth.json integrity
    39/39.

Scope note

The borrowed-entry persistence model is unchanged: _seed_custom_pool still
creates the config:<name> entry from custom_providers.api_key, and
borrowed entries persist without secrets by design (fail-closed disk
boundary). The fingerprint fix makes that boundary cooldown-correct; changing
the persistence model itself is a separate (security-sensitive) change.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard area/billing Account usage, credit usage, billing (cross-cutting) labels Aug 9, 2026
…ass, wait for transient recovery

Real-world failure class (hyper.charm.land, 3-key pool): a transient 429
'Please try again in a few minutes.' benched the funded key for the full
1-hour default TTL, rotation walked into two genuinely-depleted 402 keys,
the pool hit 'no available entries' and the turn aborted as billing-
exhausted — while the funded key recovered in minutes. Meanwhile runtime
resolution silently fell through to the config singleton key, serving keys
the pool had benched for billing and burning requests on depleted accounts.

Four fixes:

1. Cooldown sizing (credential_pool.py): _extract_retry_delay_seconds now
   parses fuzzy retry guidance ('try again in a few minutes' -> 180s,
   'in N minutes' -> N*60 capped at 10min, 'in a moment/shortly' -> 60s).
   The parsed window rides error_context -> reset_at -> _exhausted_until,
   so the bench matches the provider's own guidance. Provenance-tagged
   message-derived windows are dropped for billing-classified failures
   (the NousResearch#31273 money-burn protection) while provider-supplied structured
   reset_at fields/headers are kept. extract_api_error_context shares the
   same parser and tags all message-parsed windows.

2. Cooldown bypass (runtime_provider.py + model_switch.py):
   _try_resolve_from_custom_pool now reports pool exhaustion (benched keys
   + billing flags + reset times) and all three fallthrough call sites
   filter candidates through _pick_custom_api_key — billing-benched keys
   are never served, transient bypasses are logged, and all-benched pools
   raise AuthError(code=insufficient_credits) which the gateway already
   routes to the fallback chain / billing-exhaustion UX. Two broad
   except-Exception blocks in model_switch.py that swallowed this
   AuthError and fell back to the raw config key (defeating the filter)
   now re-raise it.

3. Recoverable-exhaustion wait (agent_runtime_helpers.py): when rotation
   reaches 'no available entries' and the earliest benched entry is
   transient (not billing) recovering within 600s, the recovery path waits
   (interruptible, gateway-touch heartbeat) and retries instead of aborting.
   Pure-billing exhaustion still aborts immediately. Auxiliary background
   calls are intentionally exempt (documented at _recover_provider_pool).

4. Fingerprint-aware rotation detection (credential_pool.py):
   _upsert_entry compares secret fingerprints instead of raw strings, so
   re-hydrating the same borrowed credential (config/env keys stripped on
   disk) does not clear persisted benches on every load_pool(). The
   incoming fingerprint is carried forward onto borrowed entries even on
   the legacy-upgrade path (no stored fingerprint) so subsequent loads
   compare correctly. A _exhausted_candidate_unlocked helper DRYs the
   shared iteration logic in next_available_at / recoverable_wait_seconds
   / benched_runtime_keys.

Tests: tests/agent/test_credential_pool_402_flapping.py (38 cases) covering
fuzzy parsing, billing guards incl. the full extractor->pool runtime seam,
bench persistence across reload (same-secret survives, new-secret clears,
legacy fingerprint upgrade), resolution bench filter + structured
exhaustion, wait assessment semantics incl. the mixed transient/billing
pool, and adapter compatibility. Existing credential pool + runtime
resolution + model-switch suites pass unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing Account usage, credit usage, billing (cross-cutting) comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants