fix(tui): surface backend error as visible text when final_response is empty (#21245)

When the provider rejects a request (e.g. invalid model slug like
'--provider nous --model kimi-k2.6' where the valid slug is
'moonshotai/kimi-k2.6'), run_conversation() returns
{failed: True, error: <detail>, final_response: None}. The TUI gateway
and one-shot CLI mode both dropped the error on the floor and emitted
an empty turn, so the user saw a blank response with no indication
that anything went wrong.

Mirror the interactive CLI's existing pattern (cli.py:9832): when
final_response is empty AND (failed|partial) is set AND error is
populated, surface 'Error: <detail>' as the visible text. Leaves
the None-with-no-error path and the '(empty)' sentinel path
untouched — an empty successful turn still renders empty, and
existing sentinel handlers keep owning their lane.

Reported by @counterposition in PR #20873; taking a minimal fix
rather than the broader structured-failure refactor proposed there.
This commit is contained in:
Teknium 2026-05-07 05:53:19 -07:00 committed by GitHub
parent 8dcdc3cbc2
commit 6e46f99e7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 118 additions and 1 deletions

View file

@ -3137,6 +3137,18 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
if result.get("interrupted")
else "error" if result.get("error") else "complete"
)
# When the backend produced no visible response AND reported a
# real error (e.g. invalid model slug → provider 4xx), surface
# that error as the visible text instead of shipping an empty
# turn to Ink. Mirrors classic CLI behavior at cli.py where
# (failed|partial) + no final_response → "Error: <detail>".
# Leaves the None-with-no-error path untouched: an empty
# successful turn still renders as empty, and the existing
# "(empty)" sentinel handling stays in its own lane.
if (not raw) and result.get("error") and (
result.get("failed") or result.get("partial")
):
raw = f"Error: {result.get('error')}"
lr = result.get("last_reasoning")
if isinstance(lr, str) and lr.strip():
last_reasoning = lr.strip()