Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
cli-67: retry on empty addition
  • Loading branch information
Your Name
Your Name committed Sep 29, 2026
commit 25505e914e26185a4394ae378486aecd1469db6d
26 changes: 23 additions & 3 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3988,7 +3988,7 @@ async def show_send_output(self, completion):
if (
not len(self.partial_response_content)
and not len(self.partial_response_tool_calls)
and not len(self.partial_response_reasoning_content)
and not _is_meaningful_reasoning(self.partial_response_reasoning_content)
):
self.empty_response = True
return
Expand Down Expand Up @@ -4118,7 +4118,8 @@ async def show_send_output_stream(self, completion):

text += reasoning_content
self.got_reasoning_content = True
received_content = True
if _is_meaningful_reasoning(reasoning_content):
received_content = True

self.token_profiler.on_token()
self.io.update_spinner_suffix(reasoning_content)
Expand Down Expand Up @@ -4194,7 +4195,14 @@ async def show_send_output_stream(self, completion):
self.io.tool_warning("Execution stopped by on message hook")
return

if not received_content and len(self.partial_response_tool_calls) == 0:
# Treat the response as empty when nothing was received, or when the
# only thing received was reasoning made entirely of non-alphanumeric
# characters (e.g. moonshotai/kimi-k3 returning "!!!!").
if (
not received_content
and len(self.partial_response_tool_calls) == 0
and not _is_meaningful_reasoning(self.partial_response_reasoning_content)
):
self.empty_response = True
return

Expand Down Expand Up @@ -5383,3 +5391,15 @@ def _first_usage_tokens(usage: object, paths: list[str], default: int = 0) -> in
if value is not None:
return value
return default


def _is_meaningful_reasoning(text):
"""Return True if reasoning text contains at least one alphanumeric character.

Some providers (e.g. moonshotai/kimi-k3) occasionally return completions
with empty ``content`` and a ``reasoning_content`` made entirely of
punctuation (e.g. ``"!!!!"``). Those responses are effectively empty, so
the empty-response detector only lets reasoning count as response
content when it holds at least one alphanumeric character.
"""
return bool(text) and any(ch.isalnum() for ch in text)
1 change: 1 addition & 0 deletions cecli/website/docs/config/retries.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Cecli can be configured to retry failed API calls. This is useful for handling i
- `retry-timeout`: The timeout in seconds for each retry.
- `retry-backoff-factor`: The backoff factor to use between retries.
- `retry-on-unavailable`: Whether to retry on 503 Service Unavailable errors.
- `retry-on-empty`: Whether to retry when the model returns an empty response. A response is considered empty when it has no content, no tool calls, and no *meaningful* reasoning. Reasoning that contains no alphanumeric characters (for example a `reasoning_content` of `"!!!!"`) does not count as a response, so it is retried like any other empty response.

Example usage in `.cecli.conf.yml`:

Expand Down
6 changes: 3 additions & 3 deletions tests/basic/test_retry_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import pytest

from cecli.llm import litellm
from cecli.models import Model, _parse_retry_config
from cecli.models import Model, parse_retry_config


def test_parse_retry_config_string():
config_str = '{"retry_timeout": 15, "retry-on-empty": true}'
result = _parse_retry_config(config_str)
result = parse_retry_config(config_str)
assert result["retry_timeout"] == 15.0
assert result["retry_on_empty"] is True
# defaults
Expand All @@ -22,7 +22,7 @@ def test_parse_retry_config_dict():
"retry_backoff_factor": 2.0,
"retry-on-unavailable": False,
}
result = _parse_retry_config(config_dict)
result = parse_retry_config(config_dict)
assert result["retry_timeout"] == 10.0
assert result["retry_backoff_factor"] == 2.0
assert result["retry_on_unavailable"] is False
Expand Down