use crate::{
ContentBlock, ContentPart, Message, ProviderData, Subject, ToolResultContent, UserContent,
Warning, WarningKind,
};
pub fn retain_portable_content(messages: &mut [Message]) -> Vec<Warning> {
let mut warnings = Vec::new();
for (index, message) in messages.iter_mut().enumerate() {
let mut warn = |block: Option<usize>, text: String| {
warnings.push(Warning::new(
WarningKind::Dropped,
Subject::Content {
message: index,
block,
},
text,
));
};
if clear(message.provider_data_mut()) {
warn(
None,
"the message's provider data is not portable; it was dropped".into(),
);
}
match message {
Message::User(m) => {
if let UserContent::Parts(parts) = &mut m.content {
retain_parts(parts, &mut warn);
}
}
Message::Tool(m) => {
if let ToolResultContent::Parts(parts) = &mut m.content {
retain_parts(parts, &mut warn);
}
}
Message::Assistant(m) => {
let mut position = 0;
m.content.retain_mut(|block| {
let at = Some(position);
position += 1;
let state = match block {
ContentBlock::Other(_)
| ContentBlock::ProviderToolCall(_)
| ContentBlock::ProviderToolResult(_) => {
warn(at, not_portable(block.type_name()));
return false;
}
ContentBlock::Image(image) if image.source.provider_file_id().is_some() => {
warn(at, provider_file());
return false;
}
ContentBlock::Text(text) => {
let citations = text
.citations
.iter_mut()
.fold(false, |any, c| clear(&mut c.provider_data) | any);
clear(&mut text.provider_data) | citations
}
ContentBlock::Image(image) => clear(&mut image.provider_data),
ContentBlock::Reasoning(reasoning) => clear(&mut reasoning.provider_data),
ContentBlock::Refusal(refusal) => clear(&mut refusal.provider_data),
ContentBlock::ToolCall(call) => clear(&mut call.provider_data),
};
if state {
warn(at, state_dropped());
}
true
});
}
}
}
warnings
}
fn retain_parts(parts: &mut Vec<ContentPart>, warn: &mut impl FnMut(Option<usize>, String)) {
let mut position = 0;
parts.retain_mut(|part| {
let at = Some(position);
position += 1;
let state = match part {
ContentPart::Other(other) => {
warn(at, not_portable(&other.type_name));
return false;
}
ContentPart::Image(image) if image.source.provider_file_id().is_some() => {
warn(at, provider_file());
return false;
}
ContentPart::File(file) if file.source.provider_file_id().is_some() => {
warn(at, provider_file());
return false;
}
ContentPart::Text(text) => {
let citations = text
.citations
.iter_mut()
.fold(false, |any, c| clear(&mut c.provider_data) | any);
clear(&mut text.provider_data) | citations
}
ContentPart::Image(image) => clear(&mut image.provider_data),
ContentPart::File(file) => clear(&mut file.provider_data),
};
if state {
warn(at, state_dropped());
}
true
});
}
fn clear(data: &mut ProviderData) -> bool {
let had = !data.is_empty();
*data = ProviderData::new();
had
}
fn not_portable(type_name: &str) -> String {
format!("`{type_name}` content is not portable; it was dropped")
}
fn provider_file() -> String {
"a file a provider stores is not portable; it was dropped".to_owned()
}
fn state_dropped() -> String {
"provider data is not portable; it was dropped".to_owned()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::{Image, OtherContent, ProviderToolCall, Reasoning, ToolCall};
#[test]
fn assistant_turns_keep_only_portable_blocks() {
let state = || ProviderData::new().with_raw("anthropic", "signature", json!("sig"));
let mut messages = vec![
Message::user("q"),
Message::assistant_blocks(vec![
ContentBlock::Reasoning(Reasoning::new("think").with_provider_data(state())),
ContentBlock::Other(OtherContent::new("compaction", Default::default())),
ContentBlock::ProviderToolCall(
ProviderToolCall::new("srv_1", "web_search", "{}").with_origin("anthropic"),
),
ContentBlock::Image(Image::provider_file("openai", "file-1")),
ContentBlock::ToolCall(ToolCall::new("c1", "f", "{}").with_provider_data(state())),
ContentBlock::text("answer"),
]),
Message::tool_result("c1", "f", "ok"),
];
let warnings = retain_portable_content(&mut messages);
assert_eq!(
messages[1],
Message::assistant_blocks(vec![
ContentBlock::reasoning("think"),
ContentBlock::ToolCall(ToolCall::new("c1", "f", "{}")),
ContentBlock::text("answer"),
])
);
assert_eq!(warnings.len(), 5);
assert!(warnings.iter().all(|w| w.kind == WarningKind::Dropped));
}
}