Skip to content

CLI-66: Add retry-on-forbidden opt-in for 403 PermissionDeniedError - #701

Open
szmania wants to merge 7 commits into
cecli-dev:v1.6.2from
szmania:cli-66-403-forbidden-retry
Open

szmania wants to merge 7 commits into
cecli-dev:v1.6.2from
szmania:cli-66-403-forbidden-retry

Conversation

@szmania

@szmania szmania commented Sep 26, 2026

Copy link
Copy Markdown

Summary

Implements CLI-66: retry-on-forbidden support in the retries configuration, mirroring the existing retry-on-unavailable behavior. cecli can now optionally retry API calls that fail with 403 Forbidden / PermissionDeniedError when the user explicitly opts in.

This branch also carries the CLI-65 retry-config groundwork it builds on (public parse_retry_config, retry config plumbing in models.py/base_coder.py, and the retry config test suite).

Motivation / Problem Statement

Some providers return transient 403s (e.g., temporary permission propagation delays, gateway/edge authorization hiccups, provider-side quarantine flaps). Previously, PermissionDeniedError was hard-coded as non-retryable (retry: False in exceptions.py), so a transient 403 immediately aborted the request. Users asked for an opt-in way to retry these, symmetric with retry-on-unavailable.

Design decision: retry-on-forbidden defaults to False. A 403 usually indicates an auth/permissions problem that retries cannot fix, so this is deliberately opt-in rather than default-on.

What Changed

retry-on-forbidden implementation (CLI-66)

cecli/models.py

  • ModelSettings: new field retry_on_forbidden: bool = False.
  • parse_retry_config(): parses retry_on_forbidden / retry-on-forbidden (both key styles supported, default False) and documents it in the docstring.
  • Model.send_completion(): reads retry-on-forbidden from the retry config into self.retry_on_forbidden, and adds a PermissionDeniedError branch that ORs it into should_retry — same pattern as ServiceUnavailableError → retry_on_unavailable.
  • Model.simple_send_with_retries(): same handling using the local retry_on_forbidden value.

cecli/coders/base_coder.py

  • Coder.send_message() exception handler: adds if ex_info.name == "PermissionDeniedError": should_retry = should_retry or retry_config["retry_on_forbidden"] so chat requests honor the setting with the same backoff/timeout logic as other retriable errors.

CLI-65 groundwork included in this branch

  • parse_retry_config made public and used consistently across models.py and base_coder.py.
  • tests/basic/test_retry_config.py: unit tests for retry config parsing (hyphenated/underscored keys, defaults, dict vs JSON-string inputs) and retry backoff timeout behavior in simple_send_with_retries.

Configuration

# .cecli.conf.yml
retries:
  retry-timeout: 30
  retry-backoff-factor: 1.5
  retry-on-unavailable: true
  retry-on-forbidden: true   # new, opt-in (default: false)
cecli --retries '{"retry-on-forbidden": true}'
# or
export CECLI_RETRIES='{"retry-on-forbidden": true}'

Behavior

  • With retry-on-forbidden: false (default): 403 / PermissionDeniedError fails immediately, exactly as before — no behavior change for existing users.
  • With retry-on-forbidden: true: 403s retry with the configured backoff factor and retry-timeout cap, interruptible like other retries, and honor server-suggested delays from _extract_retry_delay when present.

Testing

  • tests/basic/test_retry_config.py covers config parsing and retry-loop backoff/timeout behavior (test_simple_send_with_retries_honors_timeout).
  • Note on CI: local act runs of .github/workflows/pre-commit.yml fail with "Could not find any stages to run" — an infrastructure/workflow-config issue (no matching jobs), not a code lint violation.

Notes / Follow-ups

  • Documentation update for cecli/website/docs/config/retries.md is tracked in the plan (technical_writer scope) and can follow in a docs pass.
  • Diff also surfaces cecli/sessions.py and requirements pin adjustments relative to v1.6.2; these were introduced by the preceding CLI-65 commits on this branch.

@dwash96

dwash96 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

I think this needs to be otherwise updated with v1.6.2 since it is reverting the parse_retry_config section in models.py

@szmania

szmania commented Oct 1, 2026

Copy link
Copy Markdown
Author

I think this needs to be otherwise updated with v1.6.2 since it is reverting the parse_retry_config section in models.py

Caught this, should be using parse_retry_config() now

This branch has not been deployed

No deployments
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.

2 participants