diff --git a/graphify/llm.py b/graphify/llm.py index 560bbcb88..0d76f5a4a 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -1274,9 +1274,10 @@ def _call_claude_cli(user_message: str, max_tokens: int = 8192, *, deep_mode: bo **_no_window_kwargs(), ) if proc.returncode != 0: - raise RuntimeError( - f"claude -p exited {proc.returncode}: {proc.stderr.strip()[:500]}" - ) + # The CLI writes usage-limit and auth errors to stdout, not stderr. + # Fall back to the stdout tail when stderr is empty. + detail = proc.stderr.strip()[:500] or proc.stdout.strip()[-500:] + raise RuntimeError(f"claude -p exited {proc.returncode}: {detail}") envelope = _claude_cli_envelope(proc.stdout) @@ -2076,7 +2077,9 @@ def _rec(inp, out) -> None: **_no_window_kwargs(), ) if proc.returncode != 0: - raise RuntimeError(f"claude -p exited {proc.returncode}: {proc.stderr.strip()[:500]}") + # See _call_claude_cli: the CLI writes these errors to stdout. + _detail = proc.stderr.strip()[:500] or proc.stdout.strip()[-500:] + raise RuntimeError(f"claude -p exited {proc.returncode}: {_detail}") envelope = _claude_cli_envelope(proc.stdout) cli_usage = envelope.get("usage") or {} if cli_usage: diff --git a/tests/test_llm_errors.py b/tests/test_llm_errors.py new file mode 100644 index 000000000..2162158a2 --- /dev/null +++ b/tests/test_llm_errors.py @@ -0,0 +1,41 @@ +"""claude-cli error surfacing when the CLI reports failures on stdout.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from graphify import llm as llmmod + + +def _proc(returncode=1, stdout="", stderr=""): + return SimpleNamespace(returncode=returncode, stdout=stdout, stderr=stderr) + + +def test_claude_cli_error_prefers_stderr(monkeypatch): + import shutil as _shutil + import subprocess as _subprocess + + monkeypatch.setattr(_shutil, "which", lambda _: "/usr/bin/claude") + monkeypatch.setattr( + _subprocess, "run", lambda *a, **k: _proc(stderr="real stderr", stdout="noise") + ) + with pytest.raises(RuntimeError, match="real stderr"): + llmmod._call_claude_cli("hi") + + +def test_claude_cli_error_falls_back_to_stdout(monkeypatch): + import shutil as _shutil + import subprocess as _subprocess + + monkeypatch.setattr(_shutil, "which", lambda _: "/usr/bin/claude") + monkeypatch.setattr( + _subprocess, + "run", + lambda *a, **k: _proc( + stdout='{"type":"result","result":"Claude AI usage limit reached|1752200000"}' + ), + ) + with pytest.raises(RuntimeError, match="usage limit reached"): + llmmod._call_claude_cli("hi")