Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
from cecli.linter import Linter
from cecli.llm import litellm
from cecli.mcp import LocalServer
from cecli.models import RETRY_TIMEOUT
from cecli.reasoning_tags import (
REASONING_TAG,
format_reasoning_content,
Expand Down Expand Up @@ -2829,24 +2828,14 @@ async def format_in_executor():
except EmptyResponseError:
self.io.tool_warning(self.empty_llm_tool_warning())

retry_on_empty = False
retries_config = self.get_active_model().retries
if isinstance(retries_config, str):
try:
retries_config = json.loads(retries_config)
except json.JSONDecodeError:
self.io.tool_warning(
f"Could not parse retries config: {retries_config}"
)
retries_config = {}
if isinstance(retries_config, dict):
retry_on_empty = retries_config.get("retry_on_empty", False)
retry_config = models._parse_retry_config(self.get_active_model().retries)
retry_on_empty = retry_config["retry_on_empty"]

if not retry_on_empty:
break

retry_delay *= 2
if retry_delay > RETRY_TIMEOUT:
retry_delay *= retry_config["retry_backoff_factor"]
if retry_delay > retry_config["retry_timeout"]:
self.io.tool_error("Retry timeout exceeded on empty response.")
break

Expand All @@ -2866,10 +2855,15 @@ async def format_in_executor():
exhausted = True
break

retry_config = models._parse_retry_config(self.get_active_model().retries)

should_retry = ex_info.retry
if ex_info.name == "ServiceUnavailableError":
should_retry = should_retry or retry_config["retry_on_unavailable"]

if should_retry:
retry_delay *= 2
if retry_delay > RETRY_TIMEOUT:
retry_delay *= retry_config["retry_backoff_factor"]
if retry_delay > retry_config["retry_timeout"]:
should_retry = False

if not should_retry:
Expand Down
67 changes: 50 additions & 17 deletions cecli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1452,21 +1452,10 @@ async def send_completion(
litellm_ex = LiteLLMExceptions()
retry_delay = 0.125

if self.retries:
retry_config = dict()
try:
retry_config = json.loads(self.retries)
except (json.JSONDecodeError, TypeError, ValueError):
retry_config = dict()
pass

self.retry_on_unavailable = bool(
nested.getter(retry_config, "retry-on-unavailable", True)
)
self.retry_backoff_factor = float(
nested.getter(retry_config, "retry-backoff-factor", 1.5)
)
self.retry_timeout = float(nested.getter(retry_config, "retry-timeout", 30))
retry_config = _parse_retry_config(self.retries)
self.retry_on_unavailable = retry_config["retry_on_unavailable"]
self.retry_backoff_factor = retry_config["retry_backoff_factor"]
self.retry_timeout = retry_config["retry_timeout"]

while True:
try:
Expand Down Expand Up @@ -1557,6 +1546,11 @@ async def simple_send_with_retries(
temperature = None
tools = None

retry_config = _parse_retry_config(self.retries)
retry_backoff_factor = retry_config["retry_backoff_factor"]
retry_timeout = retry_config["retry_timeout"]
retry_on_unavailable = retry_config["retry_on_unavailable"]

if self.verbose:
dump(messages)

Expand Down Expand Up @@ -1607,14 +1601,17 @@ async def simple_send_with_retries(
if ex_info.description:
print(ex_info.description)
should_retry = ex_info.retry
if ex_info.name == "ServiceUnavailableError":
should_retry = should_retry or retry_on_unavailable

custom_retry_delay = self._extract_retry_delay(err)
if custom_retry_delay is not None:
retry_delay = custom_retry_delay
should_retry = True
elif should_retry:
retry_delay *= 2
retry_delay *= retry_backoff_factor

if retry_delay > RETRY_TIMEOUT:
if retry_delay > retry_timeout:
should_retry = False

if not should_retry:
Expand Down Expand Up @@ -1792,6 +1789,42 @@ def _configured_provider(self) -> str:
return provider


def _parse_retry_config(retries_input):
"""
Parse and normalize retry configuration from a JSON string or dict.
Returns a unified dict with defaults:
retry_timeout: 30
retry_backoff_factor: 1.5
retry_on_unavailable: True
retry_on_empty: False
"""
config = dict()
if isinstance(retries_input, str):
try:
config = json.loads(retries_input)
except (json.JSONDecodeError, TypeError, ValueError):
config = dict()
elif isinstance(retries_input, dict):
config = retries_input.copy()

# Helper to get either hyphenated or underscored key
def _get(key, default):
val = config.get(key)
if val is not None:
return val
val = config.get(key.replace("_", "-"))
if val is not None:
return val
return default

return {
"retry_timeout": float(_get("retry_timeout", 30)),
"retry_backoff_factor": float(_get("retry_backoff_factor", 1.5)),
"retry_on_unavailable": bool(_get("retry_on_unavailable", True)),
"retry_on_empty": bool(_get("retry_on_empty", False)),
}


def register_models(model_settings_fnames):
files_loaded = []
for model_settings_fname in model_settings_fnames:
Expand Down
Loading
Loading