providers-kit 0.0.2

Portable enum-dispatch Kit over multiple model provider APIs.
Documentation
use crate::KitError;
use crate::error::{ErrorPhase, ResponseViolation};
use crate::generation::{Citation, FileReference};

use super::super::wire::{OpenAiAnnotation, OpenAiContentItem};

pub(super) fn content_items_to_citations(
    items: &[OpenAiContentItem],
) -> Result<Vec<Citation>, KitError> {
    let mut citations = Vec::new();
    for item in items {
        for annotation in &item.annotations {
            citations.push(annotation_to_citation(annotation)?);
        }
    }
    Ok(citations)
}

fn annotation_to_citation(annotation: &OpenAiAnnotation) -> Result<Citation, KitError> {
    let fields = (
        annotation.title.clone(),
        annotation.text.clone(),
        annotation.start_index,
        annotation.end_index,
    );
    if let Some(url) = annotation.url.clone() {
        Ok(Citation::Url {
            url,
            title: fields.0,
            snippet: fields.1,
            start_index: fields.2,
            end_index: fields.3,
        })
    } else {
        let id = annotation
            .file_id
            .clone()
            .filter(|value| !value.trim().is_empty())
            .ok_or_else(|| {
                KitError::invalid_response(ResponseViolation::MissingField {
                    field: "annotation.reference",
                })
                .at_phase(ErrorPhase::Normalize)
                .with_message(format!(
                    "OpenAI annotation `{}` did not include a URL or file ID",
                    annotation.kind
                ))
                .with_diagnostic(None)
            })?;
        Ok(Citation::File {
            reference: FileReference::Provider {
                id,
                media_type: None,
            },
            title: fields.0,
            snippet: fields.1,
            start_index: fields.2,
            end_index: fields.3,
        })
    }
}