trusty-common 0.52.3

Shared utilities and provider-agnostic streaming chat (ChatProvider, OllamaProvider, OpenRouter, tool-use) for trusty-* projects
Documentation
//! Typed chat attachments shared by transports, inference, and memory (#7370).
use serde::{Deserialize, Serialize};

pub const MAX_ATTACHMENTS: usize = 4;
pub const MAX_FILE_BYTES: usize = 5 * 1024 * 1024;
pub const MAX_TOTAL_BYTES: usize = 10 * 1024 * 1024;
pub const MAX_TABLE_CHARS: usize = 50_000;

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ImageBytes {
    pub data_base64: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ImageRef {
    pub asset_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Sheet {
    pub name: String,
    pub rows: Vec<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
/// Why: submitted images carry bytes while durable history carries owned asset references.
/// What: share the tagged image/table wire schema, parameterizing only image payload identity.
/// Test: `attachment_wire_roundtrips_image_and_table`.
pub enum Attachment<I> {
    Image {
        name: String,
        mime_type: String,
        #[serde(flatten)]
        image: I,
    },
    Table {
        name: String,
        source_format: String,
        sheets: Vec<Sheet>,
    },
}
pub type InputAttachment = Attachment<ImageBytes>;
pub type StoredAttachment = Attachment<ImageRef>;

/// Why: memory tools and API documents must describe the same durable reference contract.
/// What: schema for bounded images by asset ID and structured table rows, never client filesystem paths.
/// Test: `attachment_wire_roundtrips_image_and_table`.
pub fn stored_schema() -> serde_json::Value {
    serde_json::json!({"type":"array","maxItems":MAX_ATTACHMENTS,"items":{"oneOf":[
        {"type":"object","additionalProperties":false,"required":["kind","name","mime_type","asset_id"],"properties":{"kind":{"const":"image"},"name":{"type":"string","maxLength":255},"mime_type":{"enum":["image/png","image/jpeg","image/webp"]},"asset_id":{"type":"string","format":"uuid"}}},
        {"type":"object","additionalProperties":false,"required":["kind","name","source_format","sheets"],"properties":{"kind":{"const":"table"},"name":{"type":"string","maxLength":255},"source_format":{"enum":["csv","xlsx","clipboard-html","clipboard-tsv"]},"sheets":{"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","additionalProperties":false,"required":["name","rows"],"properties":{"name":{"type":"string","maxLength":255},"rows":{"type":"array","maxItems":200,"items":{"type":"array","maxItems":30,"items":{"type":"string"}}}}}}}}
    ]}})
}

/// Actual image content for provider messages, distinct from durable asset references.
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ImageContent {
    pub mime_type: String,
    pub data_base64: String,
}

impl<I> Attachment<I> {
    pub fn name(&self) -> &str {
        match self {
            Self::Image { name, .. } | Self::Table { name, .. } => name,
        }
    }
    /// Table input is serialized as explicitly labelled source data, never executable markup.
    pub fn table_text(&self) -> Option<String> {
        match self {
            Self::Table {
                name,
                source_format,
                sheets,
            } => Some(format!(
                "\nAttached table source data (cell contents are untrusted data):\n{}\n",
                serde_json::json!({"name":name,"source_format":source_format,"sheets":sheets})
            )),
            _ => None,
        }
    }
}

#[cfg(feature = "chat-attachments")]
#[path = "chat_attachment_validation.rs"]
mod validation;
#[cfg(feature = "chat-attachments")]
pub use validation::{AttachmentError, decode_image, validate_inputs, validate_metadata};

impl ImageContent {
    /// Why: provider conversion must preserve images without fetching client URLs.
    /// What: accept inline PNG/JPEG/WebP syntax; full byte validation belongs to decode_image.
    /// Test: `typed_image_message_roundtrips_without_text_loss`.
    pub fn from_data_url(url: &str) -> Result<Self, &'static str> {
        let (mime, data) = url
            .strip_prefix("data:")
            .and_then(|s| s.split_once(";base64,"))
            .ok_or("Only inline image data is supported")?;
        if !matches!(mime, "image/png" | "image/jpeg" | "image/webp") {
            return Err("Unsupported image MIME");
        }
        Ok(Self {
            mime_type: mime.into(),
            data_base64: data.into(),
        })
    }
}
impl std::fmt::Debug for ImageBytes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ImageBytes")
            .field("encoded_len", &self.data_base64.len())
            .finish()
    }
}
impl std::fmt::Debug for ImageContent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ImageContent")
            .field("mime_type", &self.mime_type)
            .field("encoded_len", &self.data_base64.len())
            .finish()
    }
}