Skip to content

fix: normalize OpenAI tool message content - #701

Open
Kushagra963-lab wants to merge 2 commits into
lastmile-ai:mainfrom
Kushagra963-lab:codex/fix-openai-tool-content-normalization
Open

fix: normalize OpenAI tool message content#701
Kushagra963-lab wants to merge 2 commits into
lastmile-ai:mainfrom
Kushagra963-lab:codex/fix-openai-tool-content-normalization

Conversation

@Kushagra963-lab

@Kushagra963-lab Kushagra963-lab commented Jun 7, 2026

Copy link
Copy Markdown

Fixes #25

Summary:

  • Convert OpenAI tool-call results through the existing tool-result converter so tool message content is a string, not a content-part list.
  • Normalize raw role="tool" messages with content blocks when mixed messages are passed through to OpenAI.
  • Add regression coverage that tool content blocks become strings while user content blocks stay intact for multimodal messages.

Validation:

  • .venv/bin/python -m pytest tests/utils/test_multipart_converter_openai.py -q
  • .venv/bin/python -m py_compile src/mcp_agent/workflows/llm/augmented_llm_openai.py src/mcp_agent/workflows/llm/multipart_converter_openai.py tests/utils/test_multipart_converter_openai.py
  • git diff --check

Note:

  • I also tried tests/workflows/llm/test_augmented_llm_openai.py, but it currently errors at fixture setup because OpenAIAugmentedLLM is still abstract without generate_stream; this PR leaves that unrelated test setup issue untouched.

Summary by CodeRabbit

  • Bug Fixes

    • Tool-call results now accept either a single message or a list and are converted into proper OpenAI-compatible messages.
    • Tool-message content is normalized so tool messages always carry string content (concatenating blocks or JSON-encoding non-string payloads).
  • Tests

    • Added unit tests validating normalization and conversion of mixed tool/user message content.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 101f57fd-3dd6-4163-9ff7-8e8016dd6fb2

📥 Commits

Reviewing files that changed from the base of the PR and between f8f4ee2 and d0762d7.

📒 Files selected for processing (2)
  • src/mcp_agent/workflows/llm/augmented_llm_openai.py
  • tests/utils/test_multipart_converter_openai.py

📝 Walkthrough

Walkthrough

Normalize tool-message content to strings, integrate normalization into OpenAI message conversion and tool-call result handling, and add tests covering list/dict tool-message content normalization and preservation of user content blocks.

Changes

OpenAI Tool Message Content Normalization

Layer / File(s) Summary
Tool message content normalization contract and logic
src/mcp_agent/workflows/llm/multipart_converter_openai.py
Adds json import and _normalize_tool_message_content to ensure role == "tool" messages have string content, extracting text from content-block lists or serializing objects via JSON; integrates this into convert_mixed_messages_to_openai for dict messages and dict list items.
Tool call result conversion integration
src/mcp_agent/workflows/llm/augmented_llm_openai.py
execute_tool_call return type/docstring updated; now uses OpenAIConverter.convert_tool_result_to_openai and handles tuple/list converter outputs, building OpenAI tool message content from the converted payload and appending or extending messages in generate.
Normalization test cases
tests/utils/test_multipart_converter_openai.py
Adds tests validating that tool-message content provided as single/multiple text blocks or dicts is normalized to a string (including JSON round-trip), and that user content-block lists are preserved.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • rholinshead

🐰 I hop through messages, tidy and spry,
Turning blocks to strings so OpenAI won't cry,
I stitch and serialize with a whiskered grin,
Now tool results arrive neat and thin.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: normalizing OpenAI tool message content to strings instead of lists, directly addressing the core issue.
Linked Issues check ✅ Passed The PR addresses all core requirements from issue #25: converting tool message content from lists to strings, ensuring OpenAI API compatibility, and adding appropriate test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing tool message content normalization in the OpenAI converter and related augmented LLM handler, with appropriate test coverage added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/utils/test_multipart_converter_openai.py (1)

363-394: 💤 Low value

Consider expanding test coverage for additional edge cases.

The current tests validate the primary normalization behaviors well. To increase robustness and coverage of the normalization logic, consider adding tests for:

  1. Tool message with multiple text blocks: Validates that multiple text blocks are concatenated correctly during normalization.
  2. Tool message with non-list, non-string content (e.g., a dict or object): Validates the JSON serialization fallback path in _normalize_tool_message_content (lines 376-378 in context snippet 1).

These additions would provide more comprehensive validation of the normalization contract.

🧪 Example test cases for additional coverage

Test for multiple text blocks:

def test_convert_mixed_messages_normalizes_multiple_tool_text_blocks(self):
    tool_message = {
        "role": "tool",
        "tool_call_id": "call_456",
        "content": [
            {"type": "text", "text": "First block"},
            {"type": "text", "text": "Second block"},
        ],
    }

    messages = OpenAIConverter.convert_mixed_messages_to_openai([tool_message])

    assert len(messages) == 1
    assert messages[0]["role"] == "tool"
    assert messages[0]["content"] == "First block Second block"
    assert isinstance(messages[0]["content"], str)

Test for non-list content:

def test_convert_mixed_messages_normalizes_tool_dict_content(self):
    tool_message = {
        "role": "tool",
        "tool_call_id": "call_789",
        "content": {"key": "value", "nested": {"data": 123}},
    }

    messages = OpenAIConverter.convert_mixed_messages_to_openai([tool_message])

    assert len(messages) == 1
    assert messages[0]["role"] == "tool"
    assert isinstance(messages[0]["content"], str)
    # Content should be JSON-serialized
    import json
    assert json.loads(messages[0]["content"]) == {"key": "value", "nested": {"data": 123}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/test_multipart_converter_openai.py` around lines 363 - 394, Add
two unit tests for OpenAIConverter.convert_mixed_messages_to_openai: one that
supplies a tool message with multiple text blocks to verify
_normalize_tool_message_content concatenates them into a single string (e.g.,
"First block Second block") and another that supplies a tool message whose
content is a non-list object/dict to verify the JSON-serialization fallback path
returns a JSON string that round-trips via json.loads; reference
OpenAIConverter.convert_mixed_messages_to_openai and
_normalize_tool_message_content when adding these tests so they validate both
concatenation and serialization behaviors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mcp_agent/workflows/llm/augmented_llm_openai.py`:
- Around line 650-654: The code currently drops the second element returned by
OpenAIConverter.convert_tool_result_to_openai(result, tool_call_id=tool_call_id)
— preserve and propagate any additional_messages returned by that call instead
of discarding them: when converted is a tuple unpack it into (tool_message,
additional_messages) and append or extend additional_messages onto the messages
buffer (same logic where tool_message is used); apply the same fix to the other
occurrence of convert_tool_result_to_openai at the nearby location so non-text
tool outputs keep their follow-up context.

---

Nitpick comments:
In `@tests/utils/test_multipart_converter_openai.py`:
- Around line 363-394: Add two unit tests for
OpenAIConverter.convert_mixed_messages_to_openai: one that supplies a tool
message with multiple text blocks to verify _normalize_tool_message_content
concatenates them into a single string (e.g., "First block Second block") and
another that supplies a tool message whose content is a non-list object/dict to
verify the JSON-serialization fallback path returns a JSON string that
round-trips via json.loads; reference
OpenAIConverter.convert_mixed_messages_to_openai and
_normalize_tool_message_content when adding these tests so they validate both
concatenation and serialization behaviors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3770182b-3ff0-4544-a8cc-06acb1eaf9e6

📥 Commits

Reviewing files that changed from the base of the PR and between f62d849 and f8f4ee2.

📒 Files selected for processing (3)
  • src/mcp_agent/workflows/llm/augmented_llm_openai.py
  • src/mcp_agent/workflows/llm/multipart_converter_openai.py
  • tests/utils/test_multipart_converter_openai.py

Comment thread src/mcp_agent/workflows/llm/augmented_llm_openai.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

content in OpenAI ChatCompletion messages is a list, not string

2 participants