From c2d1377c84ad523fa7fec7bb6128fb66c2ec2469 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 6 Jun 2026 13:22:43 -0700 Subject: [PATCH 1/3] feat: Add retry on empty LLM response Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/args.py | 6 ++++++ cecli/coders/base_coder.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/cecli/args.py b/cecli/args.py index d51421d9012..b02bf95141d 100644 --- a/cecli/args.py +++ b/cecli/args.py @@ -278,6 +278,12 @@ def get_parser(default_config_files, git_root): help="Specify LLM retry configuration as a JSON string", default=None, ) + group.add_argument( + "--retry-on-empty", + action=argparse.BooleanOptionalAction, + default=False, + help="Enable/disable retrying on empty LLM responses (default: False)", + ) ####### group = parser.add_argument_group("Customization Settings") diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index c28dc866cc6..698b7e0c235 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -91,6 +91,10 @@ class FinishReasonLength(Exception): pass +class EmptyResponseError(Exception): + pass + + def wrap_fence(name): return f"<{name}>", f"" @@ -2399,9 +2403,29 @@ async def format_in_executor(): try: while True: try: + self.empty_response = False async for chunk in self.send(messages, tools=self.get_tool_list()): yield chunk break + except EmptyResponseError: + self.io.tool_warning(self.empty_llm_tool_warning()) + if not (self.args and self.args.retry_on_empty): + break + + retry_delay *= 2 + if retry_delay > RETRY_TIMEOUT: + self.io.tool_error("Retry timeout exceeded on empty response.") + break + + self.io.tool_output(f"Retrying in {retry_delay:.1f} seconds...") + + _res, interrupted_sleep = await coroutines.interruptible( + asyncio.sleep(retry_delay), self.interrupt_event + ) + if interrupted_sleep: + interrupted = True + break + continue except litellm_ex.exceptions_tuple() as err: ex_info = litellm_ex.get_ex_info(err) @@ -3302,6 +3326,9 @@ async def send(self, messages, model=None, functions=None, tools=None): else: await self.show_send_output(completion) + if self.empty_response: + raise EmptyResponseError + response, func_err, content_err = self.consolidate_chunks() if response: @@ -3382,7 +3409,8 @@ async def show_send_output(self, completion): and not len(self.partial_response_tool_calls) and not len(self.partial_response_reasoning_content) ): - self.io.tool_warning(self.empty_llm_tool_warning()) + self.empty_response = True + return self.io.assistant_output(show_resp, pretty=self.show_pretty()) @@ -3539,7 +3567,8 @@ async def show_send_output_stream(self, completion): return if not received_content and len(self.partial_response_tool_calls) == 0: - self.io.tool_warning(self.empty_llm_tool_warning()) + self.empty_response = True + return def consolidate_chunks(self): if self.partial_response_consolidated: From 05bb5337dc48ff2d1aec0cbf51db61eda3484fd8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 7 Jun 2026 14:14:36 -0700 Subject: [PATCH 2/3] refactor: Move --retry-on-empty to retries config block Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/args.py | 11 ++++------- cecli/args_formatter.py | 10 ++++++++++ cecli/coders/base_coder.py | 14 +++++++++++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/cecli/args.py b/cecli/args.py index 7dbb06f9439..4b368f21b1c 100644 --- a/cecli/args.py +++ b/cecli/args.py @@ -276,15 +276,12 @@ def get_parser(default_config_files, git_root): group.add_argument( "--retries", metavar="RETRIES_JSON", - help="Specify LLM retry configuration as a JSON string", + help=( + "Specify LLM retry configuration as a JSON/YAML string (e.g., '{\"retry_on_empty\": " + "true}')" + ), default=None, ) - group.add_argument( - "--retry-on-empty", - action=argparse.BooleanOptionalAction, - default=False, - help="Enable/disable retrying on empty LLM responses (default: False)", - ) ####### group = parser.add_argument_group("Customization Settings") diff --git a/cecli/args_formatter.py b/cecli/args_formatter.py index 01b9bc94094..aaa9463c3b3 100644 --- a/cecli/args_formatter.py +++ b/cecli/args_formatter.py @@ -132,6 +132,16 @@ def _format_action(self, action): break switch = switch.lstrip("-") + if switch == "retries": + parts.append(f"## {action.help}") + parts.append("#retries:") + parts.append("# retry-timeout: 60") + parts.append("# retry-backoff-factor: 2.0") + parts.append("# retry-on-unavailable: true") + parts.append("# retry-on-empty: false") + parts.append("") + return "\n".join(parts) + if isinstance(action, argparse._StoreTrueAction): default = False elif isinstance(action, argparse._StoreConstAction): diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 698b7e0c235..bc07ec16b99 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -2409,7 +2409,19 @@ async def format_in_executor(): break except EmptyResponseError: self.io.tool_warning(self.empty_llm_tool_warning()) - if not (self.args and self.args.retry_on_empty): + + 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) + + if not retry_on_empty: break retry_delay *= 2 From 8655aa2529d842383cd1a6b1c26f50db60877aad Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 7 Jun 2026 14:15:42 -0700 Subject: [PATCH 3/3] chore: Fix linter warnings Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/args.py | 2 +- cecli/coders/base_coder.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cecli/args.py b/cecli/args.py index 4b368f21b1c..83286782492 100644 --- a/cecli/args.py +++ b/cecli/args.py @@ -277,7 +277,7 @@ def get_parser(default_config_files, git_root): "--retries", metavar="RETRIES_JSON", help=( - "Specify LLM retry configuration as a JSON/YAML string (e.g., '{\"retry_on_empty\": " + 'Specify LLM retry configuration as a JSON/YAML string (e.g., \'{"retry_on_empty": ' "true}')" ), default=None, diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index bc07ec16b99..7372cca1277 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -2416,7 +2416,9 @@ async def format_in_executor(): try: retries_config = json.loads(retries_config) except json.JSONDecodeError: - self.io.tool_warning(f"Could not parse retries config: {retries_config}") + 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)