From 93884aae1e81b17914ce8415626ebe7ed8995109 Mon Sep 17 00:00:00 2001 From: vitali87 Date: Sun, 30 Aug 2026 15:10:27 +0100 Subject: [PATCH] fix: only warn about truncation when the stop reason is max_tokens Every non-tool_use stop reason was logged as 'LLM output truncated', mislabelling end_turn/stop_sequence responses that carried a complete tool block. --- pr_split/logs.py | 1 + pr_split/planner/client.py | 5 ++++- tests/test_client.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pr_split/logs.py b/pr_split/logs.py index 3f61cd4..7ae7350 100644 --- a/pr_split/logs.py +++ b/pr_split/logs.py @@ -41,6 +41,7 @@ "LLM output truncated (stop_reason: {stop_reason}), keys in partial output: {keys}" ) LLM_OUTPUT_INCOMPLETE = "LLM output incomplete (status: {status}, reason: {reason})" +LLM_UNEXPECTED_STOP = "LLM stopped with stop_reason={stop_reason} (keys in tool input: {keys})" CHUNK_RETRY = "Chunk {index}/{total} failed (attempt {attempt}), retrying: {error}" INVALID_HUNK_INDEX = ( "Group '{group}': invalid hunk index {index} for {file} (max: {max}), skipping" diff --git a/pr_split/planner/client.py b/pr_split/planner/client.py index ed02b96..738e91d 100644 --- a/pr_split/planner/client.py +++ b/pr_split/planner/client.py @@ -142,9 +142,12 @@ def _call_anthropic(system: str, user: str, *, settings: Settings) -> RawToolOut if isinstance(block, BetaToolUseBlock) and isinstance(block.input, dict): keys = list(block.input.keys()) break - logger.warning(logs.LLM_OUTPUT_TRUNCATED.format(stop_reason=stop_reason, keys=keys)) if stop_reason == "max_tokens": + logger.warning(logs.LLM_OUTPUT_TRUNCATED.format(stop_reason=stop_reason, keys=keys)) raise LLMError(ErrorMsg.LLM_OUTPUT_TRUNCATED(detail=f"stop_reason={stop_reason}")) + # Any other stop reason is not a truncation; if a tool block is + # present the plan is complete, otherwise the loop below reports it. + logger.debug(logs.LLM_UNEXPECTED_STOP.format(stop_reason=stop_reason, keys=keys)) for block in response.content: if isinstance(block, BetaToolUseBlock) and block.name == SPLIT_TOOL_NAME: return RawToolOutput(groups=_extract_raw_output(block.input)) diff --git a/tests/test_client.py b/tests/test_client.py index 36e13be..9d9be1a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1064,3 +1064,22 @@ def test_failed_status_falls_back_to_error_code(self, mock_cls: MagicMock) -> No ) with pytest.raises(LLMError, match="response failed: rate_limit_exceeded"): _call_openai("sys", "usr", settings=_make_settings(Provider.OPENAI)) + + +class TestTruncationWarningScope: + @patch("pr_split.planner.client.logger") + @patch("pr_split.planner.client.anthropic.Anthropic") + def test_end_turn_with_tool_block_does_not_warn_truncated( + self, mock_cls: MagicMock, mock_logger: MagicMock + ) -> None: + from anthropic.types.beta import BetaToolUseBlock + + block = BetaToolUseBlock( + id="tu_1", type="tool_use", name=SPLIT_TOOL_NAME, input={"groups": _SAMPLE_RAW_GROUPS} + ) + mock_cls.return_value.beta.messages.create.return_value = SimpleNamespace( + stop_reason="end_turn", content=[block] + ) + result = _call_anthropic("sys", "usr", settings=_make_settings(Provider.ANTHROPIC)) + assert result["groups"] == _SAMPLE_RAW_GROUPS + assert not any("truncated" in str(c.args[0]) for c in mock_logger.warning.call_args_list)