fix: handle list content in _serialize_for_summary for multimodal messages

msg.get('content') can return a list of parts for multimodal messages
(containing text, images, etc.). The old code passed this list directly
to redact_sensitive_text(text: str), which raised AttributeError on
list.replace(), causing context compression to fail entirely for any
session with attached images.

Fix: detect list content and extract text parts before redacting.
Image parts are replaced with '[image]' placeholder.
This commit is contained in:
AlexFucuson9 2026-07-07 11:34:40 +07:00 committed by Teknium
parent 1600008ab0
commit 868a2f7d17
2 changed files with 61 additions and 1 deletions

View file

@ -1664,7 +1664,21 @@ class ContextCompressor(ContextEngine):
parts = []
for msg in turns:
role = msg.get("role", "unknown")
content = redact_sensitive_text(msg.get("content") or "")
content = msg.get("content")
if isinstance(content, list):
text_parts: list[str] = []
for part in content:
if isinstance(part, dict):
if part.get("type") == "text":
text_parts.append(part.get("text", ""))
elif part.get("type") in {
"image", "image_url", "input_image",
}:
text_parts.append("[image]")
elif isinstance(part, str):
text_parts.append(part)
content = "\n".join(text_parts)
content = redact_sensitive_text(content or "")
content = _MEDIA_DIRECTIVE_RE.sub("[media attachment]", content)
# Strip inline reasoning blocks (<think>, <reasoning>, etc.) from
# assistant content before it reaches the summarizer. Reasoning

View file

@ -54,3 +54,49 @@ class TestMediaDirectiveStripping:
result = compressor._serialize_for_summary(turns)
assert "MEDIA:" not in result
assert result.count("[media attachment]") == 2
def test_multimodal_list_content_does_not_crash(self, compressor):
"""content as a list (multimodal) must not crash redact_sensitive_text.
Regression: msg.get('content') can return a list of parts for
multimodal messages (images + text). The old code passed this
list directly to redact_sensitive_text(str), which raised
AttributeError on list.replace().
"""
turns = [
{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc123"}},
],
},
]
# Must not raise
result = compressor._serialize_for_summary(turns)
assert "What is in this image?" in result
assert "[image]" in result
def test_multimodal_list_text_parts_extracted(self, compressor):
"""Text parts from multimodal list content are preserved in output."""
turns = [
{
"role": "user",
"content": [
{"type": "text", "text": "first part"},
{"type": "text", "text": "second part"},
],
},
]
result = compressor._serialize_for_summary(turns)
assert "first part" in result
assert "second part" in result
def test_multimodal_list_bare_strings_handled(self, compressor):
"""Bare strings inside a content list are joined."""
turns = [
{"role": "user", "content": ["hello", "world"]},
]
result = compressor._serialize_for_summary(turns)
assert "hello" in result
assert "world" in result