From f49658de15199f63b803c8ed50ceaef7aa172dc7 Mon Sep 17 00:00:00 2001 From: Om Chauhan Date: Sat, 21 Feb 2026 17:06:54 +0530 Subject: [PATCH 1/3] skipping provider-specific messages during summarization --- .../context/llm_context_summarization.py | 18 ++++- tests/test_context_summarization.py | 74 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/pipecat/utils/context/llm_context_summarization.py b/src/pipecat/utils/context/llm_context_summarization.py index 6865a00d9..06551e3bb 100644 --- a/src/pipecat/utils/context/llm_context_summarization.py +++ b/src/pipecat/utils/context/llm_context_summarization.py @@ -15,7 +15,7 @@ from typing import List, Optional from loguru import logger -from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage # Token estimation constants CHARS_PER_TOKEN = 4 # Industry-standard heuristic: 1 token ≈ 4 characters @@ -188,6 +188,9 @@ class LLMContextSummarizationUtil: total = 0 for message in context.messages: + if isinstance(message, LLMSpecificMessage): + continue + # Role and structure overhead total += TOKEN_OVERHEAD_PER_MESSAGE @@ -248,6 +251,9 @@ class LLMContextSummarizationUtil: for i in range(start_idx, len(messages)): msg = messages[i] + if isinstance(msg, LLMSpecificMessage): + continue + role = msg.get("role") # Check for tool calls in assistant messages @@ -298,7 +304,12 @@ class LLMContextSummarizationUtil: # Find first system message index first_system_index = next( - (i for i, msg in enumerate(messages) if msg.get("role") == "system"), -1 + ( + i + for i, msg in enumerate(messages) + if not isinstance(msg, LLMSpecificMessage) and msg.get("role") == "system" + ), + -1, ) # Messages to summarize are between first system and recent messages @@ -356,6 +367,9 @@ class LLMContextSummarizationUtil: transcript_parts = [] for msg in messages: + if isinstance(msg, LLMSpecificMessage): + continue + role = msg.get("role", "unknown") content = msg.get("content", "") diff --git a/tests/test_context_summarization.py b/tests/test_context_summarization.py index 87aaa74d3..36559ed3f 100644 --- a/tests/test_context_summarization.py +++ b/tests/test_context_summarization.py @@ -10,7 +10,7 @@ import unittest from unittest.mock import AsyncMock, MagicMock, patch from pipecat.frames.frames import LLMContextSummaryRequestFrame -from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage from pipecat.services.llm_service import LLMService from pipecat.utils.context.llm_context_summarization import ( LLMContextSummarizationConfig, @@ -602,5 +602,77 @@ class TestSummaryGenerationExceptions(unittest.IsolatedAsyncioTestCase): self.assertEqual(last_index, 1) # Should be the index of the last summarized message +class TestLLMSpecificMessageHandling(unittest.TestCase): + """Tests that LLMSpecificMessage objects are correctly skipped in summarization.""" + + def test_estimate_context_tokens_skips_specific_messages(self): + """Test that estimate_context_tokens skips LLMSpecificMessage objects.""" + context = LLMContext() + context.add_message({"role": "user", "content": "Hello"}) + context.add_message(LLMSpecificMessage(llm="google", message={})) + context.add_message({"role": "assistant", "content": "Hi there"}) + + tokens_with_specific = LLMContextSummarizationUtil.estimate_context_tokens(context) + + context_without = LLMContext() + context_without.add_message({"role": "user", "content": "Hello"}) + context_without.add_message({"role": "assistant", "content": "Hi there"}) + tokens_without = LLMContextSummarizationUtil.estimate_context_tokens(context_without) + + self.assertEqual(tokens_with_specific, tokens_without) + + def test_get_messages_to_summarize_with_specific_messages(self): + """Test that get_messages_to_summarize handles LLMSpecificMessage objects.""" + context = LLMContext() + context.add_message({"role": "system", "content": "System prompt"}) + context.add_message(LLMSpecificMessage(llm="google", message={})) + context.add_message({"role": "user", "content": "Message 1"}) + context.add_message({"role": "assistant", "content": "Response 1"}) + context.add_message(LLMSpecificMessage(llm="google", message={})) + context.add_message({"role": "user", "content": "Message 2"}) + context.add_message({"role": "assistant", "content": "Response 2"}) + + result = LLMContextSummarizationUtil.get_messages_to_summarize(context, 2) + + self.assertGreater(len(result.messages), 0) + self.assertGreater(result.last_summarized_index, 0) + + def test_format_messages_skips_specific_messages(self): + """Test that format_messages_for_summary skips LLMSpecificMessage objects.""" + messages = [ + {"role": "user", "content": "Hello"}, + LLMSpecificMessage(llm="google", message={}), + {"role": "assistant", "content": "Hi there"}, + ] + + transcript = LLMContextSummarizationUtil.format_messages_for_summary(messages) + + self.assertIn("USER: Hello", transcript) + self.assertIn("ASSISTANT: Hi there", transcript) + + def test_function_call_tracking_skips_specific_messages(self): + """Test that _get_function_calls_in_progress_index skips LLMSpecificMessage.""" + messages = [ + {"role": "user", "content": "What time is it?"}, + LLMSpecificMessage(llm="google", message={}), + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": {"name": "get_time", "arguments": "{}"}, + } + ], + }, + LLMSpecificMessage(llm="google", message={}), + {"role": "tool", "tool_call_id": "call_123", "content": '{"time": "10:30 AM"}'}, + ] + + result = LLMContextSummarizationUtil._get_function_calls_in_progress_index(messages, 0) + self.assertEqual(result, -1) + + if __name__ == "__main__": unittest.main() From 9476b5d184751ddfaff76b478711fc22ba835401 Mon Sep 17 00:00:00 2001 From: Om Chauhan Date: Sat, 21 Feb 2026 17:35:08 +0530 Subject: [PATCH 2/3] added changelog --- changelog/3794.fixed.md | 1 + tests/test_context_summarization.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/3794.fixed.md diff --git a/changelog/3794.fixed.md b/changelog/3794.fixed.md new file mode 100644 index 000000000..e2b3c7c00 --- /dev/null +++ b/changelog/3794.fixed.md @@ -0,0 +1 @@ +- Added `LLMSpecificMessage` handling in `LLMContextSummarizationUtil` to skip provider-specific messages during context summarization. diff --git a/tests/test_context_summarization.py b/tests/test_context_summarization.py index 36559ed3f..3bb1246e9 100644 --- a/tests/test_context_summarization.py +++ b/tests/test_context_summarization.py @@ -634,8 +634,8 @@ class TestLLMSpecificMessageHandling(unittest.TestCase): result = LLMContextSummarizationUtil.get_messages_to_summarize(context, 2) - self.assertGreater(len(result.messages), 0) - self.assertGreater(result.last_summarized_index, 0) + self.assertEqual(len(result.messages), 4) + self.assertEqual(result.last_summarized_index, 4) def test_format_messages_skips_specific_messages(self): """Test that format_messages_for_summary skips LLMSpecificMessage objects.""" From aa6d3b38b38793f948c1aa8ac5fb3b1b8d644e5d Mon Sep 17 00:00:00 2001 From: filipi87 Date: Fri, 27 Feb 2026 12:53:25 -0300 Subject: [PATCH 3/3] Add explanatory comments for LLMSpecificMessage guards in context summarization, amd fixed the missing guard in LLMContextSummarizer._apply_summary when searching for the first system message. --- .../aggregators/llm_context_summarizer.py | 16 +++++++++++++--- .../utils/context/llm_context_summarization.py | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pipecat/processors/aggregators/llm_context_summarizer.py b/src/pipecat/processors/aggregators/llm_context_summarizer.py index a1a613ccc..7886fcf12 100644 --- a/src/pipecat/processors/aggregators/llm_context_summarizer.py +++ b/src/pipecat/processors/aggregators/llm_context_summarizer.py @@ -18,7 +18,7 @@ from pipecat.frames.frames import ( LLMContextSummaryResultFrame, LLMFullResponseStartFrame, ) -from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage from pipecat.utils.asyncio.task_manager import BaseTaskManager from pipecat.utils.base_object import BaseObject from pipecat.utils.context.llm_context_summarization import ( @@ -290,8 +290,18 @@ class LLMContextSummarizer(BaseObject): """ messages = self._context.messages - # Find the first system message to preserve - first_system_msg = next((msg for msg in messages if msg.get("role") == "system"), None) + # Find the first system message to preserve. LLMSpecificMessage instances are excluded + # because they are not dict-like and never represent a system message; they hold + # service-specific metadata (e.g. thinking blocks) that is always paired with a + # standard message. + first_system_msg = next( + ( + msg + for msg in messages + if not isinstance(msg, LLMSpecificMessage) and msg.get("role") == "system" + ), + None, + ) # Get recent messages to keep recent_messages = messages[last_summarized_index + 1 :] diff --git a/src/pipecat/utils/context/llm_context_summarization.py b/src/pipecat/utils/context/llm_context_summarization.py index 06551e3bb..537cc91ab 100644 --- a/src/pipecat/utils/context/llm_context_summarization.py +++ b/src/pipecat/utils/context/llm_context_summarization.py @@ -188,6 +188,8 @@ class LLMContextSummarizationUtil: total = 0 for message in context.messages: + # LLMSpecificMessage holds service-specific data (e.g. thinking blocks, + # thought signatures). Skipping them here for now. if isinstance(message, LLMSpecificMessage): continue @@ -251,6 +253,9 @@ class LLMContextSummarizationUtil: for i in range(start_idx, len(messages)): msg = messages[i] + # LLMSpecificMessage instances (e.g. thinking blocks) never carry tool_call or + # tool_call_id fields, so they cannot affect the pending-call tracking. Skipping + # them avoids an AttributeError. if isinstance(msg, LLMSpecificMessage): continue @@ -302,7 +307,10 @@ class LLMContextSummarizationUtil: if len(messages) <= min_messages_to_keep: return LLMMessagesToSummarize(messages=[], last_summarized_index=-1) - # Find first system message index + # Find first system message index. LLMSpecificMessage instances are excluded because + # they are not dict-like and never represent a system message; they hold + # service-specific metadata (e.g. thinking blocks) that is always paired with a + # standard message. first_system_index = next( ( i @@ -367,6 +375,11 @@ class LLMContextSummarizationUtil: transcript_parts = [] for msg in messages: + # LLMSpecificMessage holds service-specific internal data (e.g. Anthropic thinking + # blocks, Gemini thought signatures). This data is not meaningful as plain text for + # a summarization transcript, and the summarizer LLM would not know how to interpret + # it. The conversational content of those turns is already captured by the + # accompanying standard assistant message. if isinstance(msg, LLMSpecificMessage): continue