diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index e452dc394..c2e6a881c 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -680,6 +680,17 @@ class LLM(BaseLLM): messages = self._process_message_files(messages) formatted_messages = self._format_messages_for_provider(messages) + # --- 1b) Strip cache_breakpoint flags so non-Anthropic providers + # (Groq, OpenAI-compatible, etc.) do not receive unsupported keys. + # Native providers handle this in BaseLLM._format_messages(); the + # LiteLLM path does not call that method, so we clean up here. + from crewai.llms.cache import CACHE_BREAKPOINT_KEY + + formatted_messages = [ + {k: v for k, v in msg.items() if k != CACHE_BREAKPOINT_KEY} + for msg in formatted_messages + ] + # --- 2) Prepare the parameters for the completion call params = { "model": self.model, diff --git a/lib/crewai/tests/llms/test_prompt_cache.py b/lib/crewai/tests/llms/test_prompt_cache.py index c421c331e..53e80d38f 100644 --- a/lib/crewai/tests/llms/test_prompt_cache.py +++ b/lib/crewai/tests/llms/test_prompt_cache.py @@ -194,3 +194,59 @@ class TestNonAnthropicStripsMarker: formatted = llm._format_messages(messages) for m in formatted: assert CACHE_BREAKPOINT_KEY not in m + + +class TestLiteLLMPathStripsMarker: + """Regression tests for issue #5886: cache_breakpoint must be stripped + on the LiteLLM code path (used by Groq, OpenAI-compatible, etc.) + which does NOT call BaseLLM._format_messages(). + """ + + def test_prepare_completion_params_strips_cache_breakpoint(self) -> None: + """_prepare_completion_params must strip cache_breakpoint from the + messages payload so providers like Groq do not receive unsupported keys. + """ + from crewai.llm import LLM + + llm = LLM(model="groq/llama-3.3-70b-versatile", is_litellm=True) + messages = [ + mark_cache_breakpoint({"role": "system", "content": "You are a researcher"}), + mark_cache_breakpoint({"role": "user", "content": "Write a summary of AI trends"}), + ] + params = llm._prepare_completion_params(messages) + for msg in params["messages"]: + assert CACHE_BREAKPOINT_KEY not in msg, ( + f"cache_breakpoint leaked to LiteLLM params: {msg}" + ) + + def test_prepare_completion_params_preserves_original_markers(self) -> None: + """Stripping must not mutate the caller's messages — executors reuse + their messages list across ReAct loop iterations. + """ + from crewai.llm import LLM + + llm = LLM(model="groq/llama-3.3-70b-versatile", is_litellm=True) + messages = [ + mark_cache_breakpoint({"role": "system", "content": "stable system"}), + mark_cache_breakpoint({"role": "user", "content": "stable user"}), + ] + llm._prepare_completion_params(messages) + # Original messages must still carry the markers + assert messages[0][CACHE_BREAKPOINT_KEY] is True + assert messages[1][CACHE_BREAKPOINT_KEY] is True + + def test_prepare_completion_params_without_markers(self) -> None: + """Messages without cache_breakpoint must pass through unchanged.""" + from crewai.llm import LLM + + llm = LLM(model="groq/llama-3.3-70b-versatile", is_litellm=True) + messages = [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "Hello"}, + ] + params = llm._prepare_completion_params(messages) + assert len(params["messages"]) == 2 + assert params["messages"][0]["content"] == "You are helpful" + assert params["messages"][1]["content"] == "Hello" + for msg in params["messages"]: + assert CACHE_BREAKPOINT_KEY not in msg