From 4e8efa041de3b4c0d4493c17a8fc20e7de05ca52 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:51:01 -0700 Subject: [PATCH] fix(advisor): exclude in-sequence system rows from the advisor sub-call context --- .../messages/interceptors/advisor.py | 7 ++- .../messages/test_advisor_orchestration.py | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py b/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py index 6f732db1548..701211049db 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py @@ -302,6 +302,11 @@ def _build_advisor_context( tool_use blocks are excluded because Anthropic requires tool_use to be immediately followed by tool_result — not the advisor question. + + In-sequence system rows (e.g. Claude Code SessionStart hook output) are + excluded: they are executor-directed, and a trailing one becomes invalid + once the question turn is appended after it (a system row must precede an + assistant message or end the array). """ question: Final = (advisor_use_block.get("input") or {}).get("question") or ( "Please provide guidance on the current task." @@ -313,7 +318,7 @@ def _build_advisor_context( for block in raw_content if isinstance(block, dict) and block.get("type") == "text" ] - result: Final = list(messages) + result: Final = [m for m in messages if m.get("role") != "system"] if executor_text_blocks: result.append({"role": "assistant", "content": executor_text_blocks}) result.append({"role": "user", "content": question}) diff --git a/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py b/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py index 019eb4355c2..da5b5ac3867 100644 --- a/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py +++ b/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py @@ -1294,3 +1294,56 @@ async def test_advisor_sub_call_client_override_bypasses_router(): assert len(advisor_sub_calls) == 1 assert advisor_sub_calls[0]["api_key"] == "client-key" assert advisor_sub_calls[0]["api_base"] == "https://client.example.com" + + +# --------------------------------------------------------------------------- +# 16. In-sequence system rows (e.g. Claude Code SessionStart hook output) are +# excluded from the advisor sub-call context but kept for the executor: a +# trailing system row followed by the appended question turn is rejected +# upstream ("role 'system' must precede an 'assistant' message or end the +# array"). +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_advisor_context_excludes_in_sequence_system_rows(): + from litellm.llms.anthropic.experimental_pass_through.messages.interceptors.advisor import ( + AdvisorOrchestrationHandler, + ) + + messages_with_system_row = [ + *MESSAGES, + {"role": "system", "content": "SessionStart hook output: prefer functional style."}, + ] + + sub_calls = [] + + async def mock_call(model, messages, tools, stream, max_tokens, **kwargs): + sub_calls.append({"messages": messages, "tools": tools}) + if len(sub_calls) == 1: + return _make_advisor_tool_use_response() + if tools is None: + return _make_text_response("Advice.", model="claude-opus-4-6") + return _make_text_response("Final answer.") + + with patch( + "litellm.llms.anthropic.experimental_pass_through.messages.interceptors.advisor._call_messages_handler", + side_effect=mock_call, + ): + h = AdvisorOrchestrationHandler() + await h.handle( + model="openai/gpt-4o-mini", + messages=messages_with_system_row, + tools=[ADVISOR_TOOL], + stream=False, + max_tokens=512, + custom_llm_provider="openai", + ) + + assert len(sub_calls) == 3 + advisor_messages = sub_calls[1]["messages"] + assert sub_calls[1]["tools"] is None + assert [m["role"] for m in advisor_messages if m["role"] == "system"] == [] + assert advisor_messages[-1]["role"] == "user" + executor_roles = [m["role"] for m in sub_calls[0]["messages"]] + assert "system" in executor_roles