From d00f7a79605ae5ebd6d710404d2ef80d0b9dd7e0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 10:10:46 +0000 Subject: [PATCH] fix: strip cache_breakpoint from LiteLLM path for non-Anthropic providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LiteLLM-based LLM.call() (used by Groq, OpenAI-compatible, etc.) does not go through BaseLLM._format_messages() which strips the cache_breakpoint flag. This causes providers like Groq to reject messages with 'cache_breakpoint' as an unsupported property. Strip the flag in _prepare_completion_params() so it is removed from the wire payload for all providers using the LiteLLM code path. Fixes #5886 Co-Authored-By: João --- lib/crewai/src/crewai/llm.py | 11 +++++ lib/crewai/tests/llms/test_prompt_cache.py | 56 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) 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