Skip to content

Commit 6e3f7db

Browse files
QwertyJackclaude
andcommitted
Fix message content field handling for tool calls and multimodal input
This commit fixes two issues with message content processing: 1. **Missing content field in tool call responses**: When assistant messages contain tool_calls, the content field may be missing or None, causing KeyError when checking isinstance(msg['content'], list). 2. **Incomplete multimodal text merging**: Previous implementation only extracted the first text block. Now correctly merges all text blocks with newline separator. Changes: - Add _merge_message_content() module-level function in async_engine.py - Handle 4 cases: missing content, None content, string content, list content - Convert None/missing content to empty string '' (prevents Jinja2 errors) - Use '\n'.join() to merge text blocks (matches vLLM behavior) - Preserve all message fields (e.g., tool_calls, name, etc.) - Add comprehensive unit tests (19 test cases in test_content_merge.py) Implementation based on vLLM's content normalization logic: https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/chat_utils.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 49f6324 commit 6e3f7db

File tree

2 files changed

+410
-5
lines changed

2 files changed

+410
-5
lines changed

lmdeploy/serve/async_engine.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,57 @@
3232
logger = get_logger('lmdeploy')
3333

3434

35+
def _merge_message_content(msg: Dict) -> Dict:
36+
"""Merge multimodal content blocks and ensure content field exists.
37+
38+
This function normalizes message content to match vLLM's behavior:
39+
1. Missing content field -> add content='' (empty string)
40+
2. None content -> convert to content='' (empty string)
41+
3. String content -> return as-is
42+
4. List content (multimodal) -> merge all text blocks with newline separator
43+
44+
Args:
45+
msg: A message dict with 'role' and optionally 'content' field
46+
47+
Returns:
48+
A message dict with 'content' field guaranteed to exist
49+
50+
Note:
51+
This implementation is based on vLLM's content processing logic.
52+
vLLM uses "\n".join() to merge multiple text blocks from multimodal content.
53+
54+
References:
55+
- vLLM content normalization:
56+
https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/chat_utils.py
57+
See _parse_chat_message_content() and _parse_chat_message_content_parts()
58+
- vLLM text merging logic:
59+
text_prompt = "\n".join(texts)
60+
"""
61+
# If content is missing or None, convert to empty string (matches vLLM behavior)
62+
# This prevents Jinja2 template errors when rendering chat templates
63+
if 'content' not in msg or msg['content'] is None:
64+
result = dict(msg)
65+
result['content'] = ''
66+
return result
67+
68+
# If content is already a string, return as-is
69+
if isinstance(msg['content'], str):
70+
return msg
71+
72+
# If content is a list, merge all text blocks into a single string
73+
# This matches vLLM's behavior: text_prompt = "\n".join(texts)
74+
content_parts = []
75+
for block in msg['content']:
76+
if isinstance(block, dict) and block.get('type') == 'text':
77+
content_parts.append(block.get('text', ''))
78+
merged_content = '\n'.join(content_parts)
79+
80+
# Preserve all other fields in the message (e.g., tool_calls)
81+
result = dict(msg)
82+
result['content'] = merged_content
83+
return result
84+
85+
3586
@dataclasses.dataclass
3687
class GenOut:
3788
"""Pack all response information together."""
@@ -609,11 +660,9 @@ async def _get_prompt_input(self,
609660
# Change multimodal data to openai text messages, i.e.,
610661
# [{'role': 'user', 'content': [{'type': 'text', 'text': 'hi'}]}] ->
611662
# [{'role': 'user', 'content': 'hi']
612-
if isinstance(prompt, list) and any(isinstance(msg['content'], list) for msg in prompt):
613-
prompt = [
614-
msg if isinstance(msg['content'], str) else dict(role=msg['role'], content=msg['content'][0]['text'])
615-
for msg in prompt
616-
]
663+
# Also ensure all messages have 'content' field (set to None if missing, e.g., assistant with tool_calls)
664+
if isinstance(prompt, list):
665+
prompt = [_merge_message_content(msg) for msg in prompt]
617666
if do_preprocess:
618667
# use adapter's chat template if possible
619668
chat_template = self.chat_template

0 commit comments

Comments
 (0)