HKUDS/DeepTutor · critical · ValueError

KeyPool requires at least one non-empty key

Error message

KeyPool requires at least one non-empty key

What it means

KeyPool is the key-rotation helper that cools a provider key after two HTTP 429 responses. Its constructor strips whitespace from every supplied key and refuses to build a pool from an empty result set, because rotation logic has nothing to rotate without at least one usable key. This is a fail-fast configuration guard, not a runtime failure.

Source

Thrown at deeptutor/services/keypool.py:15

"""Thread-safe round-robin API key rotation with rate-limit cooldowns."""

from __future__ import annotations

from threading import Lock
from time import monotonic


class KeyPool:
    """Rotate keys and cool a key after two HTTP 429 responses."""

    def __init__(self, keys: list[str], cooldown_s: int = 60) -> None:
        self._keys = [str(key).strip() for key in keys if str(key).strip()]
        if not self._keys:
            raise ValueError("KeyPool requires at least one non-empty key")
        self._cooldown_s = max(0, cooldown_s)
        self._next_index = 0
        self._strikes = {key: 0 for key in self._keys}
        self._cooldown_until = {key: 0.0 for key in self._keys}
        self._lock = Lock()

    def next(self) -> str:
        """Return the next key, preferring one that is not cooling down.

        When every key is cooling we still return the one that recovers
        soonest instead of refusing to serve. The pool spreads load across
        keys; it is not a circuit breaker. Raising here would convert a
        retryable provider 429 into an unretryable application error for the
        whole cooldown window — and for the common single-key setup that means
        every LLM and embedding call failing for a full minute, which is
        strictly worse than letting the provider's own 429 surface and be
        retried. The caller (``_KeyRotatingCompletions.create``) already marks
        the strike and re-raises the real 429 on its second attempt.

View on GitHub (pinned to 3e82f13042)

Solutions

  1. Check the input before construction: log len of the raw keys list and each key's stripped length.
  2. Fix the source of the key list (env var name, settings file, secret manager path).
  3. Filter blanks explicitly and raise a clearer domain error if nothing remains: keys = [k.strip() for k in raw if k.strip()].
  4. Add a startup settings validation step so the app fails with a readable message before KeyPool is built.

Example fix

// before
pool = KeyPool(os.getenv("PROVIDER_KEYS", "").split(","))

# after
raw = [k for k in os.getenv("PROVIDER_KEYS", "").split(",") if k.strip()]
if not raw:
    raise RuntimeError("PROVIDER_KEYS env var is empty or unset")
pool = KeyPool(raw)
Defensive patterns

Strategy: validation

Validate before calling

raw = os.getenv("PROVIDER_KEYS", "").split(",")
keys = [k.strip() for k in raw if k.strip()]
if not keys:
    raise RuntimeError("PROVIDER_KEYS must contain at least one non-empty key")
pool = KeyPool(keys)

Type guard

def is_valid_key_list(keys: object) -> bool:
    return isinstance(keys, list) and bool(keys) and all(
        isinstance(k, str) and k.strip() for k in keys
    )

Try / catch

try:
    pool = KeyPool(keys)
except ValueError as e:
    if "non-empty key" in str(e):
        raise SystemExit("Configure PROVIDER_KEYS before starting") from e
    raise

Prevention

When it happens

Trigger: Instantiating KeyPool(keys=[]) , with a list of empty/whitespace strings like [' ', ''], or with a list produced by splitting an unset env var (e.g. (os.getenv('KEYS') or '').split(',')) where every element strips to ''.

Common situations: Env var name typo so the keys variable is empty; comma-separated key string with trailing commas producing blank entries; a secrets file loaded but parsed into empty strings in CI/test environments; passing keys before they were read from settings.

Related errors


AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27). Data as JSON: /api/errors/f133f1c05e33f823. Report an issue: GitHub.