Skip to content

fix(openai): always use string content for tool messages#1878

Merged
notowen333 merged 4 commits intostrands-agents:mainfrom
giulio-leone:fix/openai-tool-message-string
Mar 16, 2026
Merged

fix(openai): always use string content for tool messages#1878
notowen333 merged 4 commits intostrands-agents:mainfrom
giulio-leone:fix/openai-tool-message-string

Conversation

@giulio-leone
Copy link
Copy Markdown
Contributor

Summary

Fixes #1696

format_request_tool_message() sent tool result content as an array of content blocks. While the OpenAI API accepts both formats, many OpenAI-compatible endpoints (Kimi K2.5, vLLM, SGLang, Ollama) only correctly parse string content for tool messages. Array format causes the model to ignore the tool result and hallucinate the answer.

Changes

  • openai.py: Changed format_request_tool_message() to always join text/json content blocks into a single newline-separated string. Image and document blocks are preserved in array format so _split_tool_message_images() can still extract them into a user message.
  • test_openai.py: Updated multi-content test to expect string output. Added a dedicated regression test for the multi-content string join behavior.

Root Cause

format_request_tool_message() produced array-format content:

{"content": [{"text": "72°F", "type": "text"}], "role": "tool", ...}

OpenAI-compatible endpoints expect string-format:

{"content": "72°F", "role": "tool", ...}

Before / After

Before (array → model ignores result, hallucinates):

content: [{"text": "2026-02-15T09:22:35", "type": "text"}]
Output: "The current time is 18:25:31 UTC on April 26, 2025."  ← WRONG

After (string → model uses result correctly):

content: "2026-02-15T09:30:54"
Output: "The current time is 9:30:54 UTC on February 15, 2026."  ← CORRECT

Testing

All 72 OpenAI model tests pass.

OpenAI-compatible endpoints (e.g., Kimi K2.5, vLLM, Ollama) only
correctly parse string-format content for tool role messages. When
content was sent as an array of content blocks, models would fail to
parse the tool result and hallucinate the answer instead.

Changed format_request_tool_message() to always join text/json content
blocks into a single newline-separated string. Image and document
blocks are preserved in array format so _split_tool_message_images()
can still extract them into a separate user message.

Closes #1696
@giulio-leone
Copy link
Copy Markdown
Contributor Author

Friendly ping — ensures OpenAI tool messages always use string content (not dicts/lists), preventing API validation errors when tool results contain structured data.

Copy link
Copy Markdown
Contributor

@notowen333 notowen333 left a comment

Choose a reason for hiding this comment

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

This is in the right direction, but has some logic polish and testing coverage issues.

Comment thread tests/strands/models/test_openai.py
Comment thread src/strands/models/openai.py Outdated
Address review feedback from @notowen333:
- Replace bulk text separation with adjacent-text-only merging to
  preserve the original order of interleaved text/image/document blocks
- Add tests for mixed text+image, adjacent text merging, image-only,
  and text+document mixed content
- Restore accidentally deleted test_split_tool_message_images_with_image
  function definition
Comment thread src/strands/models/openai.py Outdated
Copy link
Copy Markdown
Contributor

@notowen333 notowen333 left a comment

Choose a reason for hiding this comment

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

Just a small logical nit

The merging loop already joins adjacent text blocks with newlines, so
in the all-text case there is always a single merged entry.  Extract
it directly instead of redundantly re-joining.
notowen333
notowen333 previously approved these changes Mar 16, 2026
Copy link
Copy Markdown
Contributor

@notowen333 notowen333 left a comment

Choose a reason for hiding this comment

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

LGTM

@notowen333
Copy link
Copy Markdown
Contributor

/strands review

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/strands/models/openai.py 92.30% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/strands/models/openai.py
As suggested in review — verify that an empty content list produces
an empty string instead of crashing.
@giulio-leone
Copy link
Copy Markdown
Contributor Author

Thanks @notowen333 for the thorough review and approval! All feedback has been addressed:

  • ✅ Adjacent-text-only merging (preserves content order)
  • ✅ Simplified all-text branch (removed redundant join)
  • ✅ Empty content edge case test added

Is there anything else needed for merge?

@notowen333 notowen333 enabled auto-merge (squash) March 16, 2026 18:50
@notowen333
Copy link
Copy Markdown
Contributor

Once the macOS unit tests pass, this will be merged!

@giulio-leone
Copy link
Copy Markdown
Contributor Author

That's great to hear, thanks @notowen333! 🎉 Let me know if anything else is needed on my end.

@notowen333 notowen333 merged commit b66534b into strands-agents:main Mar 16, 2026
30 of 33 checks passed
kpx-dev pushed a commit to kpx-dev/sdk-python that referenced this pull request Mar 31, 2026
…nts#1878)

Co-authored-by: giulio-leone <giulio.leone@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] OpenAIModel tool message content sent as array instead of string breaks OpenAI-compatible endpoints (e.g., Kimi K2.5)

2 participants