mentra-provider 0.4.1

Shared provider core for Mentra
Documentation
use serde::Deserialize;

use crate::{
    ContentBlockDelta, ContentBlockStart, HostedToolSearchCall, ProviderEvent, ReasoningProvenance,
    Role,
};

use super::model::{AnthropicResponse, AnthropicUsage};

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum AnthropicStreamEvent {
    MessageStart {
        message: AnthropicResponse,
    },
    ContentBlockStart {
        index: usize,
        content_block: AnthropicStreamContentBlock,
    },
    ContentBlockDelta {
        index: usize,
        delta: AnthropicContentBlockDelta,
    },
    ContentBlockStop {
        index: usize,
    },
    MessageDelta {
        delta: AnthropicMessageDelta,
    },
    MessageStop,
    Ping,
    Error {
        error: AnthropicStreamError,
    },
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum AnthropicStreamContentBlock {
    Text {},
    Thinking {
        #[serde(default)]
        thinking: String,
    },
    RedactedThinking {
        data: String,
    },
    ToolUse {
        id: String,
        name: String,
    },
    ServerToolUse {
        id: String,
        name: String,
    },
    #[serde(other)]
    Unsupported,
}

impl AnthropicStreamContentBlock {
    pub(crate) fn into_provider_events(
        self,
        index: usize,
        provenance: &ReasoningProvenance,
    ) -> Vec<ProviderEvent> {
        match self {
            AnthropicStreamContentBlock::Text {} => vec![ProviderEvent::ContentBlockStarted {
                index,
                kind: ContentBlockStart::Text,
            }],
            AnthropicStreamContentBlock::Thinking { thinking } => {
                let mut events = vec![ProviderEvent::ContentBlockStarted {
                    index,
                    kind: ContentBlockStart::Thinking {
                        encrypted_content: None,
                        id: None,
                        provenance: Some(provenance.clone()),
                        redacted: false,
                    },
                }];
                if !thinking.is_empty() {
                    events.push(ProviderEvent::ContentBlockDelta {
                        index,
                        delta: ContentBlockDelta::ThinkingText(thinking),
                    });
                }
                events
            }
            AnthropicStreamContentBlock::RedactedThinking { data } => vec![
                ProviderEvent::ContentBlockStarted {
                    index,
                    kind: ContentBlockStart::Thinking {
                        encrypted_content: None,
                        id: None,
                        provenance: Some(provenance.clone()),
                        redacted: true,
                    },
                },
                ProviderEvent::ContentBlockDelta {
                    index,
                    delta: ContentBlockDelta::ThinkingSignature(data),
                },
            ],
            AnthropicStreamContentBlock::ToolUse { id, name } => {
                vec![ProviderEvent::ContentBlockStarted {
                    index,
                    kind: ContentBlockStart::ToolUse { id, name },
                }]
            }
            AnthropicStreamContentBlock::ServerToolUse { id, name } => name
                .starts_with("tool_search")
                .then(|| ProviderEvent::ContentBlockStarted {
                    index,
                    kind: ContentBlockStart::HostedToolSearch {
                        call: HostedToolSearchCall {
                            id,
                            status: Some("in_progress".to_string()),
                            query: None,
                        },
                    },
                })
                .into_iter()
                .collect(),
            AnthropicStreamContentBlock::Unsupported => Vec::new(),
        }
    }

    pub(crate) fn is_supported(&self) -> bool {
        !matches!(self, AnthropicStreamContentBlock::Unsupported)
    }
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum AnthropicContentBlockDelta {
    TextDelta {
        text: String,
    },
    InputJsonDelta {
        partial_json: String,
    },
    ThinkingDelta {
        thinking: String,
    },
    SignatureDelta {
        signature: String,
    },
    #[serde(other)]
    Unsupported,
}

impl AnthropicContentBlockDelta {
    pub(crate) fn into_provider_delta(self) -> Option<ContentBlockDelta> {
        match self {
            AnthropicContentBlockDelta::TextDelta { text } => Some(ContentBlockDelta::Text(text)),
            AnthropicContentBlockDelta::InputJsonDelta { partial_json } => {
                Some(ContentBlockDelta::ToolUseInputJson(partial_json))
            }
            AnthropicContentBlockDelta::ThinkingDelta { thinking } => {
                Some(ContentBlockDelta::ThinkingText(thinking))
            }
            AnthropicContentBlockDelta::SignatureDelta { signature } => {
                Some(ContentBlockDelta::ThinkingSignature(signature))
            }
            AnthropicContentBlockDelta::Unsupported => None,
        }
    }
}

#[derive(Deserialize)]
pub(crate) struct AnthropicMessageDelta {
    pub(crate) stop_reason: Option<String>,
    #[serde(default)]
    pub(crate) usage: Option<AnthropicUsage>,
}

#[derive(Deserialize)]
pub(crate) struct AnthropicStreamError {
    #[serde(rename = "type")]
    pub(crate) kind: String,
    pub(crate) message: String,
}

impl AnthropicStreamEvent {
    pub(crate) fn into_provider_events(
        self,
        reasoning_provenance: &ReasoningProvenance,
    ) -> Result<Vec<ProviderEvent>, AnthropicStreamError> {
        match self {
            AnthropicStreamEvent::MessageStart { message } => {
                let usage = message
                    .usage
                    .clone()
                    .and_then(AnthropicUsage::into_token_usage);
                let mut events = vec![ProviderEvent::MessageStarted {
                    id: message.id,
                    model: message.model,
                    role: match message.role.as_str() {
                        "user" => Role::User,
                        "assistant" => Role::Assistant,
                        _ => Role::Unknown(message.role),
                    },
                }];
                if let Some(usage) = usage {
                    events.push(ProviderEvent::MessageDelta {
                        stop_reason: None,
                        usage: Some(usage),
                    });
                }
                Ok(events)
            }
            AnthropicStreamEvent::ContentBlockStart {
                index,
                content_block,
            } => Ok(content_block.into_provider_events(index, reasoning_provenance)),
            AnthropicStreamEvent::ContentBlockDelta { index, delta } => Ok(delta
                .into_provider_delta()
                .map(|delta| vec![ProviderEvent::ContentBlockDelta { index, delta }])
                .unwrap_or_default()),
            AnthropicStreamEvent::ContentBlockStop { index } => {
                Ok(vec![ProviderEvent::ContentBlockStopped { index }])
            }
            AnthropicStreamEvent::MessageDelta { delta } => Ok(vec![ProviderEvent::MessageDelta {
                stop_reason: delta.stop_reason,
                usage: delta.usage.and_then(AnthropicUsage::into_token_usage),
            }]),
            AnthropicStreamEvent::MessageStop => Ok(vec![ProviderEvent::MessageStopped]),
            AnthropicStreamEvent::Ping => Ok(Vec::new()),
            AnthropicStreamEvent::Error { error } => Err(error),
        }
    }
}