headroomlabs-ai/headroom · error · ValueError

retry_max_attempts must be >= 1 when retry_enabled=True

Error message

retry_max_attempts must be >= 1 when retry_enabled=True

What it means

The dataclass ProxyConfig validates in __post_init__ that retries cannot be enabled with an attempt count below 1. retry_enabled=True with retry_max_attempts=0 or negative is contradictory, so startup fails fast. This protects code that would otherwise retry zero times or loop incorrectly.

Source

Thrown at headroom/proxy/models.py:520

    # Number of built-in uvicorn worker processes sharing this listen socket.
    # Kept at the end to avoid shifting existing positional constructor fields.
    # Process-local runtime hot reload is unsafe above one worker because only
    # the worker receiving the admin request would observe the update.
    worker_processes: int = 1

    def __post_init__(self, smart_routing: bool | None = None) -> None:
        if self.rollout is None:
            self.rollout = resolve_rollout()
        # ``read_maturation`` remains a concrete, already-resolved runtime
        # setting for programmatic/config-file callers.  The CLI composition
        # root derives it from this same snapshot before constructing the
        # config; rewriting it here would resolve policy a second time and
        # break explicit non-CLI configuration.
        if self.worker_processes < 1:
            raise ValueError("worker_processes must be >= 1")
        if self.retry_enabled and self.retry_max_attempts < 1:
            raise ValueError("retry_max_attempts must be >= 1 when retry_enabled=True")
        # A 0 (or negative) requests-per-minute limit divides by zero in the
        # token-bucket wait computation (rate_limit_policy.consume_from_bucket),
        # 500-ing every request. The CLI already guards this with IntRange(min=1);
        # fail fast here too so the JSON/programmatic config paths can't produce a
        # limiter that crashes at request time. Only matters when limiting is on.
        if self.rate_limit_enabled and self.rate_limit_requests_per_minute < 1:
            raise ValueError(
                "rate_limit_requests_per_minute must be >= 1 when rate_limit_enabled=True"
            )

    @property
    def provider_api_overrides(self) -> ProviderApiOverrides:
        """Return provider API URL overrides as a dedicated provider config object."""
        return ProviderApiOverrides(
            anthropic=self.anthropic_api_url,
            openai=self.openai_api_url,
            gemini=self.gemini_api_url,
            cloudcode=self.cloudcode_api_url,

View on GitHub (pinned to 322425c43b)

Solutions

  1. Set retry_max_attempts to 1 or higher when retry_enabled is True.
  2. If retries are unwanted, set retry_enabled=False instead of zeroing attempts.
  3. Validate config files in CI by instantiating ProxyConfig before deploy.

Example fix

# before
ProxyConfig(retry_enabled=True, retry_max_attempts=0)

# after
ProxyConfig(retry_enabled=True, retry_max_attempts=3)
# or
ProxyConfig(retry_enabled=False)
Defensive patterns

Strategy: validation

Validate before calling

def check_retry(cfg):
    if cfg.get("retry_enabled", False) and cfg.get("retry_max_attempts", 0) < 1:
        raise SystemExit("Set retry_max_attempts >= 1 or disable retry_enabled")

Type guard

def valid_retry(cfg) -> bool:
    return not cfg.retry_enabled or cfg.retry_max_attempts >= 1

Try / catch

try:
    ProxyConfig(**raw)
except ValueError as e:
    fail_config(e)  # surface before serve

Prevention

When it happens

Trigger: Constructing ProxyConfig(retry_enabled=True, retry_max_attempts=0) or loading JSON/programmatic config where retry_max_attempts is absent and defaults to 0 while retry_enabled defaults to True; also YAML/env overrides setting attempts to 0.

Common situations: Copy-pasting a config template with retries on but attempts unset; migrating configs after a field rename; intentionally 'disabling' attempts while leaving retry_enabled true.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/45dcc0e97605997b. Report an issue: GitHub.