systemprompt-ai 0.2.2

Provider-agnostic LLM integration for systemprompt.io AI governance — Anthropic, OpenAI, Gemini, and local models unified behind one governed pipeline with cost tracking and audit.
Documentation
use crate::models::ai::{AiContentPart, AiMessage, MessageRole};
use crate::models::providers::anthropic::AnthropicMessage;
use crate::models::providers::gemini::GeminiContent;
use crate::models::providers::openai::{
    OpenAiContentPart, OpenAiImageUrl, OpenAiMessage, OpenAiMessageContent,
};

impl From<&AiMessage> for OpenAiMessage {
    fn from(message: &AiMessage) -> Self {
        let role = match message.role {
            MessageRole::System => "system",
            MessageRole::User => "user",
            MessageRole::Assistant => "assistant",
        }
        .to_string();

        let content = if message.parts.is_empty() {
            OpenAiMessageContent::Text(message.content.clone())
        } else {
            OpenAiMessageContent::Parts(convert_to_openai_parts(message))
        };

        Self { role, content }
    }
}

fn convert_to_openai_parts(message: &AiMessage) -> Vec<OpenAiContentPart> {
    let mut parts = Vec::new();

    if !message.content.is_empty() {
        parts.push(OpenAiContentPart::Text {
            text: message.content.clone(),
        });
    }

    for part in &message.parts {
        match part {
            AiContentPart::Text { text } => {
                parts.push(OpenAiContentPart::Text { text: text.clone() });
            },
            AiContentPart::Image { mime_type, data } => {
                let data_uri = format!("data:{mime_type};base64,{data}");
                parts.push(OpenAiContentPart::ImageUrl {
                    image_url: OpenAiImageUrl {
                        url: data_uri,
                        detail: None,
                    },
                });
            },
            AiContentPart::Audio { .. } => {
                tracing::warn!("Audio content not supported by OpenAI vision, skipping");
            },
            AiContentPart::Video { .. } => {
                tracing::warn!("Video content not supported by OpenAI vision, skipping");
            },
        }
    }

    parts
}

impl From<&AiMessage> for AnthropicMessage {
    fn from(message: &AiMessage) -> Self {
        use crate::models::providers::anthropic::AnthropicContent;

        let role = match message.role {
            MessageRole::System | MessageRole::Assistant => "assistant",
            MessageRole::User => "user",
        }
        .to_string();

        let content = if message.parts.is_empty() {
            AnthropicContent::Text(message.content.clone())
        } else {
            AnthropicContent::Blocks(convert_to_anthropic_blocks(message))
        };

        Self { role, content }
    }
}

fn convert_to_anthropic_blocks(
    message: &AiMessage,
) -> Vec<crate::models::providers::anthropic::AnthropicContentBlock> {
    use crate::models::providers::anthropic::{AnthropicContentBlock, AnthropicImageSource};

    let mut blocks = Vec::new();

    if !message.content.is_empty() {
        blocks.push(AnthropicContentBlock::Text {
            text: message.content.clone(),
        });
    }

    for part in &message.parts {
        match part {
            AiContentPart::Text { text } => {
                blocks.push(AnthropicContentBlock::Text { text: text.clone() });
            },
            AiContentPart::Image { mime_type, data } => {
                blocks.push(AnthropicContentBlock::Image {
                    source: AnthropicImageSource::Base64 {
                        media_type: mime_type.clone(),
                        data: data.clone(),
                    },
                });
            },
            AiContentPart::Audio { .. } => {
                tracing::warn!("Audio content not supported by Anthropic, skipping");
            },
            AiContentPart::Video { .. } => {
                tracing::warn!("Video content not supported by Anthropic, skipping");
            },
        }
    }

    blocks
}

impl From<&AiMessage> for GeminiContent {
    fn from(message: &AiMessage) -> Self {
        use crate::models::providers::gemini::GeminiPart;

        Self {
            role: match message.role {
                MessageRole::System | MessageRole::User => "user",
                MessageRole::Assistant => "model",
            }
            .to_string(),
            parts: vec![GeminiPart::Text {
                text: message.content.clone(),
            }],
        }
    }
}