ai 0.4.0

Simple to use LLM library for Rust with streaming, tool calling, OAuth helpers, and a lightweight agent loop
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::types::AssistantMessage;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticErrorInfo {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stack: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<Value>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssistantMessageDiagnostic {
    #[serde(rename = "type")]
    pub diagnostic_type: String,
    pub timestamp: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<DiagnosticErrorInfo>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<Value>,
}

pub fn format_thrown_value(value: impl ToString) -> String {
    value.to_string()
}

pub fn extract_diagnostic_error(error: &(dyn std::error::Error + 'static)) -> DiagnosticErrorInfo {
    DiagnosticErrorInfo {
        name: Some(std::any::type_name_of_val(error).to_string()),
        message: error.to_string(),
        stack: None,
        code: None,
    }
}

#[cfg(test)]
fn diagnostic_error_from_message(message: impl Into<String>) -> DiagnosticErrorInfo {
    DiagnosticErrorInfo {
        name: Some("ThrownValue".to_string()),
        message: message.into(),
        stack: None,
        code: None,
    }
}

pub fn create_assistant_message_diagnostic(
    diagnostic_type: impl Into<String>,
    error: DiagnosticErrorInfo,
    details: Option<Value>,
) -> AssistantMessageDiagnostic {
    AssistantMessageDiagnostic {
        diagnostic_type: diagnostic_type.into(),
        timestamp: crate::utils::time::now_millis(),
        error: Some(error),
        details,
    }
}

pub fn append_assistant_message_diagnostic(
    message: &mut AssistantMessage,
    diagnostic: AssistantMessageDiagnostic,
) {
    if let Ok(value) = serde_json::to_value(diagnostic) {
        message.diagnostics.push(value);
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::types::{Model, ModelCost};

    use super::*;

    #[test]
    fn creates_and_appends_diagnostic() {
        let diagnostic = create_assistant_message_diagnostic(
            "provider-retry",
            diagnostic_error_from_message("temporary failure"),
            Some(json!({ "attempt": 1 })),
        );
        assert_eq!(diagnostic.diagnostic_type, "provider-retry");
        assert_eq!(
            diagnostic
                .error
                .as_ref()
                .map(|error| error.message.as_str()),
            Some("temporary failure")
        );

        let model = Model {
            id: "test".to_string(),
            name: "test".to_string(),
            api: "test-api".to_string(),
            provider: "test-provider".to_string(),
            base_url: "http://localhost".to_string(),
            cost: ModelCost::default(),
            ..Model::default()
        };
        let mut message = crate::types::AssistantMessage::empty_for(&model);
        append_assistant_message_diagnostic(&mut message, diagnostic);

        assert_eq!(message.diagnostics.len(), 1);
        assert_eq!(message.diagnostics[0]["type"], "provider-retry");
        assert_eq!(message.diagnostics[0]["details"]["attempt"], 1);
    }
}