Skip to content

Commit 6b2b671

Browse files
ryanhoangtopenhands-agentenystxingyaoww
authored
Remote empty text content for tool calls & support kimi-k2-thinking (#1093)
Co-authored-by: openhands <[email protected]> Co-authored-by: enyst <[email protected]> Co-authored-by: Xingyao Wang <[email protected]>
1 parent f3c0c19 commit 6b2b671

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

.github/workflows/integration-runner.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ jobs:
6767
run-suffix: deepseek_run
6868
llm-config:
6969
model: litellm_proxy/deepseek/deepseek-chat
70+
- name: Kimi K2 Thinking
71+
run-suffix: kimi_k2_run
72+
llm-config:
73+
model: litellm_proxy/moonshot/kimi-k2-thinking
7074
steps:
7175
- name: Checkout repository
7276
uses: actions/checkout@v5

openhands-sdk/openhands/sdk/llm/message.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ def to_chat_dict(self) -> dict[str, Any]:
269269
# Assistant function_call(s)
270270
if self.role == "assistant" and self.tool_calls:
271271
message_dict["tool_calls"] = [tc.to_chat_dict() for tc in self.tool_calls]
272+
self._remove_content_if_empty(message_dict)
272273

273274
# Tool result (observation) threading
274275
if self.role == "tool" and self.tool_call_id is not None:
@@ -331,6 +332,52 @@ def _list_serializer(self) -> dict[str, Any]:
331332
# tool call keys are added in to_chat_dict to centralize behavior
332333
return message_dict
333334

335+
def _remove_content_if_empty(self, message_dict: dict[str, Any]) -> None:
336+
"""Remove empty text content entries from assistant tool-call messages.
337+
338+
Mutates the provided message_dict in-place:
339+
- If content is a string of only whitespace, drop the 'content' key
340+
- If content is a list, remove any text items with empty text; if the list
341+
becomes empty, drop the 'content' key
342+
"""
343+
if "content" not in message_dict:
344+
return
345+
346+
content = message_dict["content"]
347+
348+
if isinstance(content, str):
349+
if content.strip() == "":
350+
message_dict.pop("content", None)
351+
return
352+
353+
if isinstance(content, list):
354+
normalized: list[Any] = []
355+
for item in content:
356+
if not isinstance(item, dict):
357+
normalized.append(item)
358+
continue
359+
360+
if item.get("type") == "text":
361+
text_value = item.get("text", "")
362+
if isinstance(text_value, str):
363+
if text_value.strip() == "":
364+
continue
365+
else:
366+
raise ValueError(
367+
f"Text content item has non-string text value: "
368+
f"{text_value!r}"
369+
)
370+
371+
normalized.append(item)
372+
373+
if normalized:
374+
message_dict["content"] = normalized
375+
else:
376+
message_dict.pop("content", None)
377+
return
378+
379+
# Any other content shape is left as-is
380+
334381
def to_responses_value(self, *, vision_enabled: bool) -> str | list[dict[str, Any]]:
335382
"""Return serialized form.
336383

tests/sdk/llm/test_message.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,45 @@ def test_message_with_tool_calls():
142142
assert result["tool_calls"][0]["function"]["arguments"] == '{"arg": "value"}'
143143

144144

145+
def test_message_tool_calls_drop_empty_string_content():
146+
"""Assistant tool calls with no text should not include empty content strings."""
147+
from openhands.sdk.llm.message import Message, MessageToolCall
148+
149+
tool_call = MessageToolCall(
150+
id="call_empty",
151+
name="test_function",
152+
arguments="{}",
153+
origin="completion",
154+
)
155+
156+
message = Message(role="assistant", content=[], tool_calls=[tool_call])
157+
158+
result = message.to_chat_dict()
159+
assert "content" not in result
160+
161+
162+
def test_message_tool_calls_strip_blank_list_content():
163+
"""List-serialized tool call messages should drop blank text content blocks."""
164+
from openhands.sdk.llm.message import Message, MessageToolCall, TextContent
165+
166+
tool_call = MessageToolCall(
167+
id="call_blank_list",
168+
name="test_function",
169+
arguments="{}",
170+
origin="completion",
171+
)
172+
173+
message = Message(
174+
role="assistant",
175+
content=[TextContent(text="")],
176+
tool_calls=[tool_call],
177+
function_calling_enabled=True,
178+
)
179+
180+
result = message.to_chat_dict()
181+
assert "content" not in result
182+
183+
145184
def test_message_from_llm_chat_message_function_role_error():
146185
"""Test Message.from_llm_chat_message with function role raises error."""
147186
from litellm.types.utils import Message as LiteLLMMessage

0 commit comments

Comments
 (0)