From da472ca0d578284738413ad2e915d3ceda31c5fc Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 28 Apr 2026 12:34:24 +0800 Subject: [PATCH] fix: apply assistant-prefill guard to Anthropic models in interactive mode (fixes #416) When interactive mode is enabled (TUI), the guard that prevents sending a trailing assistant message to the Anthropic API was incorrectly skipped. The Anthropic API rejects such requests with "This model does not support assistant message prefill." regardless of whether the caller is in interactive or non-interactive mode. Updated the condition to also apply the guard when `_is_anthropic()` is true, so Anthropic models are always protected while non-Anthropic models that may support assistant prefill continue to be unaffected in interactive mode. Added three unit tests covering all three branches of the new condition. Co-Authored-By: Octopus --- strix/llm/llm.py | 4 ++- tests/llm/test_prepare_messages.py | 50 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/llm/test_prepare_messages.py diff --git a/strix/llm/llm.py b/strix/llm/llm.py index 5e6a01f73..b0b6b22dd 100644 --- a/strix/llm/llm.py +++ b/strix/llm/llm.py @@ -239,7 +239,9 @@ class LLM: conversation_history.extend(compressed) messages.extend(compressed) - if messages[-1].get("role") == "assistant" and not self.config.interactive: + if messages[-1].get("role") == "assistant" and ( + not self.config.interactive or self._is_anthropic() + ): messages.append({"role": "user", "content": "Continue the task."}) if self._is_anthropic() and self.config.enable_prompt_caching: diff --git a/tests/llm/test_prepare_messages.py b/tests/llm/test_prepare_messages.py new file mode 100644 index 000000000..2566f8ee9 --- /dev/null +++ b/tests/llm/test_prepare_messages.py @@ -0,0 +1,50 @@ +"""Tests for LLM._prepare_messages trailing-assistant-message handling.""" +from strix.llm.config import LLMConfig +from strix.llm.llm import LLM + + +def _make_llm(monkeypatch, model_name: str, interactive: bool) -> LLM: + monkeypatch.setenv("STRIX_LLM", model_name) + config = LLMConfig(model_name=model_name, interactive=interactive, enable_prompt_caching=False) + return LLM(config, agent_name=None) + + +def _history_ending_with_assistant() -> list[dict]: + return [ + {"role": "user", "content": "Scan this target."}, + {"role": "assistant", "content": "I found a vulnerability."}, + ] + + +def test_non_interactive_anthropic_adds_user_message(monkeypatch) -> None: + """Non-interactive mode always appends a user message when history ends with assistant.""" + llm = _make_llm(monkeypatch, "claude-sonnet-4-6", interactive=False) + history = _history_ending_with_assistant() + messages = llm._prepare_messages(history) + assert messages[-1]["role"] == "user" + assert messages[-1]["content"] == "Continue the task." + + +def test_interactive_anthropic_adds_user_message(monkeypatch) -> None: + """Interactive mode with Anthropic model must also append a user message. + + Anthropic API rejects messages where the last entry has role 'assistant' + (no assistant prefill support). This should hold regardless of interactive mode. + """ + llm = _make_llm(monkeypatch, "claude-sonnet-4-6", interactive=True) + history = _history_ending_with_assistant() + messages = llm._prepare_messages(history) + assert messages[-1]["role"] == "user" + assert messages[-1]["content"] == "Continue the task." + + +def test_interactive_non_anthropic_does_not_add_user_message(monkeypatch) -> None: + """Interactive mode with a non-Anthropic model keeps the trailing assistant message. + + Non-Anthropic models may support assistant prefill; in interactive mode the + caller (TUI) is responsible for appending the next user message. + """ + llm = _make_llm(monkeypatch, "openai/gpt-5.4", interactive=True) + history = _history_ending_with_assistant() + messages = llm._prepare_messages(history) + assert messages[-1]["role"] == "assistant"