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
29 changes: 29 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,19 @@ def __init__(
_agent_section = {}
self._tool_use_enforcement = _agent_section.get("tool_use_enforcement", "auto")

# Suppress fallback/rate-limit status messages from gateway channel.
# When true, fallback and rate-limit lifecycle messages are still printed
# to CLI (via _vprint) but NOT forwarded to status_callback (gateway chat).
# Other status messages (errors, warnings, tool failures) remain visible.
# Config key: logging.suppress_fallback_messages (default: false)
_logging_cfg = _agent_cfg.get("logging", {})
if not isinstance(_logging_cfg, dict):
_logging_cfg = {}
self._suppress_fallback_status = _logging_cfg.get("suppress_fallback_messages", False)
if not isinstance(self._suppress_fallback_status, bool):
from utils import is_truthy_value
self._suppress_fallback_status = is_truthy_value(self._suppress_fallback_status, default=False)

# App-level API retry count (wraps each model API call). Default 3,
# overridable via agent.api_max_retries in config.yaml. See #11616.
try:
Expand Down Expand Up @@ -2920,11 +2933,27 @@ def _emit_status(self, message: str) -> None:

This helper never raises — exceptions are swallowed so it cannot
interrupt the retry/fallback logic.

When logging.suppress_fallback_messages is enabled in config.yaml,
fallback and rate-limit messages are still printed to the CLI log but
NOT forwarded to the gateway status callback (keeping chat UI clean).
"""
try:
self._vprint(f"{self.log_prefix}{message}", force=True)
except Exception:
pass
# Suppress fallback/rate-limit status messages from gateway channel
# when logging.suppress_fallback_messages is enabled.
_is_fallback_status = (
"Rate limited" in message
or "switching to fallback" in message
or "trying fallback" in message
or "Non-retryable error" in message
or "Max retries" in message
or ("Waiting" in message and "attempt" in message)
)
if self._suppress_fallback_status and _is_fallback_status:
return
if self.status_callback:
try:
self.status_callback("lifecycle", message)
Expand Down
109 changes: 109 additions & 0 deletions tests/run_agent/test_suppress_fallback_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Tests for logging.suppress_fallback_messages config option.

When enabled, fallback/rate-limit status messages should be printed to CLI
(via _vprint) but NOT forwarded to status_callback (gateway channel).
"""
import pytest
from unittest.mock import MagicMock, patch


def _make_tool_defs(*names):
"""Build minimal tool definition list accepted by AIAgent.__init__."""
return [
{
"type": "function",
"function": {
"name": n,
"description": f"{n} tool",
"parameters": {"type": "object", "properties": {}},
},
}
for n in names
]


class TestSuppressFallbackStatus:
"""Verify _emit_status respects logging.suppress_fallback_messages."""

@pytest.fixture
def agent(self):
from run_agent import AIAgent
with (
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
):
agent = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent.client = MagicMock()
return agent

def test_emit_status_normal_forwards_to_callback(self, agent):
"""Non-fallback messages are forwarded to status_callback normally."""
agent._suppress_fallback_status = False
callback_msgs = []
agent.status_callback = lambda etype, msg: callback_msgs.append((etype, msg))

agent._emit_status("Something happened")

assert len(callback_msgs) == 1
assert callback_msgs[0] == ("lifecycle", "Something happened")

def test_emit_status_fallback_suppressed_from_callback(self, agent):
"""Fallback messages are NOT forwarded when suppress flag is enabled."""
agent._suppress_fallback_status = True
callback_msgs = []
agent.status_callback = lambda etype, msg: callback_msgs.append((etype, msg))

agent._emit_status("⚠️ Rate limited — switching to fallback provider...")
agent._emit_status("⚠️ Empty/malformed response — switching to fallback...")
agent._emit_status("⚠️ Max retries (3) for invalid responses — trying fallback...")
agent._emit_status("⚠️ Non-retryable error (HTTP 403) — trying fallback...")
agent._emit_status("⚠️ Max retries (3) exhausted — trying fallback...")
agent._emit_status("⏱️ Rate limited. Waiting 5.0s (attempt 2/3)...")

assert len(callback_msgs) == 0, f"Expected 0 callback messages, got: {callback_msgs}"

def test_emit_status_non_fallback_still_forwarded_when_suppressed(self, agent):
"""Non-fallback messages are still forwarded even when suppress is enabled."""
agent._suppress_fallback_status = True
callback_msgs = []
agent.status_callback = lambda etype, msg: callback_msgs.append((etype, msg))

agent._emit_status("❌ API failed after 3 retries — connection timeout")
agent._emit_status("⚠️ Something unrelated")

assert len(callback_msgs) == 2

def test_emit_status_fallback_forwarded_when_not_suppressed(self, agent):
"""Fallback messages ARE forwarded when suppress flag is disabled (default)."""
agent._suppress_fallback_status = False
callback_msgs = []
agent.status_callback = lambda etype, msg: callback_msgs.append((etype, msg))

agent._emit_status("⚠️ Rate limited — switching to fallback provider...")

assert len(callback_msgs) == 1

def test_emit_status_rate_limited_final_error_suppressed(self, agent):
"""Final failure message (❌ Rate limited after...) is suppressed too."""
agent._suppress_fallback_status = True
callback_msgs = []
agent.status_callback = lambda etype, msg: callback_msgs.append((etype, msg))

agent._emit_status("❌ Rate limited after 3 retries — provider exhausted")

assert len(callback_msgs) == 0

def test_emit_status_no_callback_no_crash(self, agent):
"""No crash when status_callback is None (CLI mode)."""
agent._suppress_fallback_status = True
agent.status_callback = None

# Should not raise
agent._emit_status("⚠️ Rate limited — switching to fallback provider...")
Loading