From 297eaa3533f6c98a45db4cb5c63fa07c008fd67e Mon Sep 17 00:00:00 2001 From: ygd58 Date: Sat, 25 Apr 2026 10:33:40 +0200 Subject: [PATCH] fix(api_server): emit run.failed when run_conversation returns failed=True MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When run_conversation encounters a non-retryable client error (401, 400, etc.), it returns a dict with failed=True instead of raising. The gateway's _run_and_close only branched on exceptions, so it always emitted run.completed even for failed runs — clients could not distinguish success from failure. Inspect the result dict before emitting: if failed=True, emit run.failed with the error message; otherwise emit run.completed as before. The existing except Exception path is unchanged for genuine programming errors. Fixes #15561 --- gateway/platforms/api_server.py | 48 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index b4d3ccb20c..230859023b 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -2578,21 +2578,39 @@ class APIServerAdapter(BasePlatformAdapter): return r, u result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync) - final_response = result.get("final_response", "") if isinstance(result, dict) else "" - q.put_nowait({ - "event": "run.completed", - "run_id": run_id, - "timestamp": time.time(), - "output": final_response, - "usage": usage, - }) - self._set_run_status( - run_id, - "completed", - output=final_response, - usage=usage, - last_event="run.completed", - ) + # Check for structured failure (non-retryable client errors like + # 401/400 return failed=True instead of raising, so the except + # block below never fires — issue #15561). + if isinstance(result, dict) and result.get("failed"): + error_msg = result.get("error") or "agent run failed" + q.put_nowait({ + "event": "run.failed", + "run_id": run_id, + "timestamp": time.time(), + "error": error_msg, + }) + self._set_run_status( + run_id, + "failed", + error=error_msg, + last_event="run.failed", + ) + else: + final_response = result.get("final_response", "") if isinstance(result, dict) else "" + q.put_nowait({ + "event": "run.completed", + "run_id": run_id, + "timestamp": time.time(), + "output": final_response, + "usage": usage, + }) + self._set_run_status( + run_id, + "completed", + output=final_response, + usage=usage, + last_event="run.completed", + ) except asyncio.CancelledError: self._set_run_status( run_id,