Problem
When an empty text message (for example Message::user("")) is converted through
impl From<Message> for AIMessage, the conversion's Text arm fabricates placeholder
content to avoid sending an empty value to the provider:
- empty user text becomes
"(empty message)"
- empty system text becomes
"You are a helpful assistant."
- any other empty text becomes a single space
" "
The model then treats these fabricated strings as a real user utterance or as a
system directive, which pollutes the conversation context.
Root cause
The defensive "avoid empty content" intent is implemented by injecting a synthetic
non-empty token rather than by returning no content. This is the opposite of the
downstream converters' own empty-content handling: openai/gemini filter out empty
content, and anthropic skips an empty system message - but a non-empty fabricated
string bypasses those filters. The Mixed arm already handles empty text correctly by
producing None, so the Text arm did not need to invent its own behaviour.
Repro
- Convert
Message::user(String::new()) via AIMessage::from(...).
- Observe the resulting
content is Some("(empty message)") instead of None.
Impact
Synthetic instructions and user utterances reach the model for what is actually an
empty message, corrupting the context. The conversion layer is a single chokepoint
that every Message -> AIMessage path flows through.
Problem
When an empty text message (for example
Message::user("")) is converted throughimpl From<Message> for AIMessage, the conversion's Text arm fabricates placeholdercontent to avoid sending an empty value to the provider:
"(empty message)""You are a helpful assistant."" "The model then treats these fabricated strings as a real user utterance or as a
system directive, which pollutes the conversation context.
Root cause
The defensive "avoid empty content" intent is implemented by injecting a synthetic
non-empty token rather than by returning no content. This is the opposite of the
downstream converters' own empty-content handling: openai/gemini filter out empty
content, and anthropic skips an empty system message - but a non-empty fabricated
string bypasses those filters. The Mixed arm already handles empty text correctly by
producing
None, so the Text arm did not need to invent its own behaviour.Repro
Message::user(String::new())viaAIMessage::from(...).contentisSome("(empty message)")instead ofNone.Impact
Synthetic instructions and user utterances reach the model for what is actually an
empty message, corrupting the context. The conversion layer is a single chokepoint
that every Message -> AIMessage path flows through.