use adk_core::{AdkError, ErrorCategory, ErrorComponent};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartDisposition {
Converted,
Downgraded {
to: &'static str,
},
Omitted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartOutcome {
pub kind: &'static str,
pub mime_type: Option<String>,
pub disposition: PartDisposition,
pub detail: String,
}
#[derive(Debug, Clone)]
pub struct ConversionReport {
provider: &'static str,
outcomes: Vec<PartOutcome>,
}
impl ConversionReport {
pub fn new(provider: &'static str) -> Self {
Self { provider, outcomes: Vec::new() }
}
pub fn provider(&self) -> &'static str {
self.provider
}
pub fn converted(&mut self, kind: &'static str) {
self.outcomes.push(PartOutcome {
kind,
mime_type: None,
disposition: PartDisposition::Converted,
detail: String::new(),
});
}
pub fn downgraded(
&mut self,
kind: &'static str,
mime_type: Option<&str>,
to: &'static str,
detail: impl Into<String>,
) {
let detail = detail.into();
tracing::warn!(
provider = self.provider,
part.kind = kind,
part.mime_type = mime_type.unwrap_or("none"),
part.sent_as = to,
reason = %detail,
"content part downgraded for provider"
);
self.outcomes.push(PartOutcome {
kind,
mime_type: mime_type.map(str::to_string),
disposition: PartDisposition::Downgraded { to },
detail,
});
}
pub fn omitted(
&mut self,
kind: &'static str,
mime_type: Option<&str>,
detail: impl Into<String>,
) {
let detail = detail.into();
tracing::warn!(
provider = self.provider,
part.kind = kind,
part.mime_type = mime_type.unwrap_or("none"),
reason = %detail,
"content part omitted for provider"
);
self.outcomes.push(PartOutcome {
kind,
mime_type: mime_type.map(str::to_string),
disposition: PartDisposition::Omitted,
detail,
});
}
pub fn outcomes(&self) -> &[PartOutcome] {
&self.outcomes
}
pub fn omitted_parts(&self) -> impl Iterator<Item = &PartOutcome> {
self.outcomes.iter().filter(|outcome| outcome.disposition == PartDisposition::Omitted)
}
pub fn downgraded_parts(&self) -> impl Iterator<Item = &PartOutcome> {
self.outcomes
.iter()
.filter(|outcome| matches!(outcome.disposition, PartDisposition::Downgraded { .. }))
}
pub fn has_omissions(&self) -> bool {
self.omitted_parts().next().is_some()
}
pub fn has_losses(&self) -> bool {
self.has_omissions() || self.downgraded_parts().next().is_some()
}
pub fn into_error(self) -> Option<AdkError> {
if !self.has_omissions() {
return None;
}
let omitted = self
.omitted_parts()
.map(|outcome| {
let mime = outcome.mime_type.as_deref().unwrap_or("no mime type");
format!("{} ({}): {}", outcome.kind, mime, outcome.detail)
})
.collect::<Vec<_>>()
.join("; ");
Some(AdkError::new(
ErrorComponent::Model,
ErrorCategory::Unsupported,
"model.content.parts_omitted",
format!(
"{} cannot carry every supplied content part, so the request would reach the \
model incomplete: {omitted}. Remove the part, convert it to a supported type, \
or choose a provider that accepts it.",
self.provider
),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_report_separates_omissions_from_downgrades() {
let mut report = ConversionReport::new("bedrock");
report.converted("Text");
report.downgraded("FileData", Some("image/png"), "text", "only S3 URIs are native");
report.omitted("InlineData", Some("audio/wav"), "no block accepts audio");
assert_eq!(report.outcomes().len(), 3);
assert_eq!(report.omitted_parts().count(), 1);
assert_eq!(report.downgraded_parts().count(), 1);
assert!(report.has_omissions());
assert!(report.has_losses());
}
#[test]
fn downgrades_alone_are_not_an_error() {
let mut report = ConversionReport::new("gemini");
report.downgraded("FileData", Some("application/pdf"), "text", "rendered as a reference");
assert!(!report.has_omissions());
assert!(report.has_losses());
assert!(report.into_error().is_none(), "a downgrade still reaches the model");
}
#[test]
fn an_omission_names_the_part_in_the_error() {
let mut report = ConversionReport::new("bedrock");
report.omitted("ServerToolCall", None, "Gemini-specific part");
let error = report.into_error().expect("omission must error");
let message = error.to_string();
assert!(message.contains("ServerToolCall"), "{message}");
assert!(message.contains("bedrock"), "{message}");
}
}