Compare commits

...

1 Commits

Author SHA1 Message Date
Teknium
be53f82674 fix: guard api_kwargs in except handler to prevent UnboundLocalError
When _build_api_kwargs() throws an exception, the except handler in
the retry loop referenced api_kwargs before it was assigned. This
caused an UnboundLocalError that masked the real error, making
debugging impossible for the user.

Two _dump_api_request_debug() calls in the except block (non-retryable
client error path and max-retries-exhausted path) both accessed
api_kwargs without checking if it was assigned.

Fix: initialize api_kwargs = None before the retry loop and guard both
dump calls. Now the real error surfaces instead of the masking
UnboundLocalError.

Reported by Discord user gruman0.
2026-04-10 13:44:32 -07:00
2 changed files with 31 additions and 6 deletions

View File

@@ -7706,6 +7706,7 @@ class AIAgent:
finish_reason = "stop" finish_reason = "stop"
response = None # Guard against UnboundLocalError if all retries fail response = None # Guard against UnboundLocalError if all retries fail
api_kwargs = None # Guard against UnboundLocalError in except handler
while retry_count < max_retries: while retry_count < max_retries:
try: try:
@@ -8740,6 +8741,7 @@ class AIAgent:
if self._try_activate_fallback(): if self._try_activate_fallback():
retry_count = 0 retry_count = 0
continue continue
if api_kwargs is not None:
self._dump_api_request_debug( self._dump_api_request_debug(
api_kwargs, reason="non_retryable_client_error", error=api_error, api_kwargs, reason="non_retryable_client_error", error=api_error,
) )
@@ -8845,6 +8847,7 @@ class AIAgent:
self.log_prefix, max_retries, _final_summary, self.log_prefix, max_retries, _final_summary,
_provider, _model, len(api_messages), f"{approx_tokens:,}", _provider, _model, len(api_messages), f"{approx_tokens:,}",
) )
if api_kwargs is not None:
self._dump_api_request_debug( self._dump_api_request_debug(
api_kwargs, reason="max_retries_exhausted", error=api_error, api_kwargs, reason="max_retries_exhausted", error=api_error,
) )

View File

@@ -2125,6 +2125,28 @@ class TestRetryExhaustion:
assert "error" in result assert "error" in result
assert "rate limited" in result["error"] assert "rate limited" in result["error"]
def test_build_api_kwargs_error_no_unbound_local(self, agent):
"""When _build_api_kwargs raises, except handler must not crash with UnboundLocalError.
Regression: _dump_api_request_debug(api_kwargs, ...) in the except block
referenced api_kwargs before it was assigned when _build_api_kwargs threw.
"""
self._setup_agent(agent)
with (
patch.object(agent, "_build_api_kwargs", side_effect=ValueError("bad messages")),
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
patch("run_agent.time", self._make_fast_time_mock()),
):
result = agent.run_conversation("hello")
# Must surface the real error, not UnboundLocalError
assert result.get("completed") is False
assert result.get("failed") is True
assert "error" in result
assert "UnboundLocalError" not in result.get("error", "")
assert "bad messages" in result["error"]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Flush sentinel leak # Flush sentinel leak