Skip to content

Inconsistent message/part trimming behavior across model providers #2515

@simonferquel-clanker

Description

@simonferquel-clanker

Summary

There is an inconsistency in how user message content is trimmed across providers when converting chat messages to provider API payloads. The desired behavior is:

  • Pruning: Use strings.TrimSpace() to detect and skip purely whitespace messages/parts (i.e. the trim result is only used as a guard).
  • Content: Always send the original, untrimmed content to the API.

Currently, the Anthropic provider (both standard and Beta) deviates from this by sending the trimmed value rather than the original. Additionally, some providers have no guard at all, allowing empty/whitespace-only parts to be forwarded.

Current behavior by provider

Single-part user messages

Provider Empty check Content sent to API
Anthropic (standard) TrimSpace != "" ✂️ Trimmed value
Anthropic (Beta API) TrimSpace != "" ✂️ Trimmed value
OpenAI Chat Completions None for user msgs Original msg.Content
OpenAI Responses API None for user msgs Original msg.Content
DMR / Ollama None for user msgs Original msg.Content
AWS Bedrock TrimSpace guard only Original msg.Content
Google Gemini != "" (no trim) Original msg.Content

Multi-part user messages (per text part)

Provider Guard on text part Content sent to API Whitespace part
Anthropic (standard) TrimSpace != "" ✂️ Trimmed value Dropped
Anthropic (Beta API) TrimSpace != "" ✂️ Trimmed value Dropped
OpenAI Chat / oaistream None Original part.Text Forwarded as-is
OpenAI Responses API None Original part.Text Forwarded as-is
DMR / Ollama None Original part.Text Forwarded as-is
AWS Bedrock TrimSpace guard only Original part.Text Dropped
Google Gemini None Original part.Text Forwarded as-is ⚠️

Issues identified

  1. Anthropic (standard + Beta) — single-part and multi-part text parts: sends the trimmed value (txt) instead of the original msg.Content / part.Text.

    • pkg/model/provider/anthropic/client.go lines 308–311 and 485–488
    • pkg/model/provider/anthropic/beta_converter.go lines 46–53 and 169–174
    • Also affects tool-result content: client.go lines 404–445, beta_converter.go lines 139–162
  2. OpenAI Chat / oaistream / DMR / Responses API — multi-part text parts: no whitespace guard at all, so empty or whitespace-only text parts are forwarded to the API.

    • pkg/model/provider/oaistream/messages.go lines 27–42 (ConvertMultiContent)
    • pkg/model/provider/openai/client.go lines 571–604
  3. Google Gemini — single-part: uses != "" instead of TrimSpace != "", so a whitespace-only message (e.g. " ") passes the guard and is forwarded.

    • pkg/model/provider/gemini/client.go lines 264–267
  4. Google Gemini — multi-part: no guard at all (not even != ""), making it more permissive than its own single-part path.

    • pkg/model/provider/gemini/client.go lines 290–293

Desired behavior (target)

For every provider, in both single-part and multi-part paths:

// Guard: use TrimSpace to decide whether to skip
if strings.TrimSpace(content) == "" {
    continue // or skip
}
// Send: use the original, untrimmed value
api.Send(content)

Concretely:

  • Anthropic: change anthropic.NewTextBlock(txt)anthropic.NewTextBlock(msg.Content) (and equivalent in Beta/tool-result paths), keeping txt only for the emptiness check.
  • OpenAI/oaistream multi-part: add if strings.TrimSpace(part.Text) == "" { continue } guard before appending text parts.
  • OpenAI Responses API multi-part: same guard.
  • Gemini single-part: change msg.Content != ""strings.TrimSpace(msg.Content) != "".
  • Gemini multi-part: add if strings.TrimSpace(part.Text) == "" { continue } guard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions