systemprompt-models 0.55.1

Foundation data models for systemprompt.io AI governance infrastructure. Shared DTOs, config, and domain types consumed by every layer of the MCP governance pipeline.
Documentation
//! The provider-neutral response and streaming-event model.
//!
//! Outbound adapters parse a buffered upstream reply into a
//! [`CanonicalResponse`] or map upstream SSE bytes to a stream of
//! [`CanonicalEvent`]s. Stop reasons are normalised here, with per-dialect
//! string mappings.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use super::request::{CanonicalContent, flatten_part};
use super::usage::{CanonicalUsage, CanonicalUsageUpdate};
use crate::wire::inspect::ForwardedSurface;

/// Why the upstream model stopped, in provider-neutral terms.
///
/// `Refusal` is the model (or its safety layer) declining to continue —
/// Anthropic `refusal`, `OpenAI` `content_filter`, Gemini `SAFETY` and its
/// siblings. `Other` is reserved for a reason no dialect classifies; a turn
/// that ends on it *with* content still relays as a clean stop, while one
/// that ends on it with nothing is an upstream error, and the raw reason is
/// carried beside it so nothing is masked on the way to the audit row.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CanonicalStopReason {
    EndTurn,
    MaxTokens,
    StopSequence,
    ToolUse,
    Refusal,
    Other,
}

impl CanonicalStopReason {
    pub const fn anthropic_str(self) -> &'static str {
        match self {
            Self::MaxTokens => "max_tokens",
            Self::StopSequence => "stop_sequence",
            Self::ToolUse => "tool_use",
            Self::Refusal => "refusal",
            Self::EndTurn | Self::Other => "end_turn",
        }
    }

    pub const fn openai_str(self) -> &'static str {
        match self {
            Self::MaxTokens => "length",
            Self::ToolUse => "tool_calls",
            Self::Refusal => "content_filter",
            Self::EndTurn | Self::StopSequence | Self::Other => "stop",
        }
    }

    // Why: a provider that cut the turn off (refusal, an unknown reason)
    // must not relay as a clean empty turn; only "nothing to say" (`STOP`,
    // an exhausted budget) is a legitimate empty terminal.
    #[must_use]
    pub const fn empty_terminal_is_error(self) -> bool {
        matches!(self, Self::Refusal | Self::Other)
    }

    pub fn from_anthropic(s: &str) -> Self {
        match s {
            "end_turn" => Self::EndTurn,
            "max_tokens" => Self::MaxTokens,
            "stop_sequence" => Self::StopSequence,
            "tool_use" => Self::ToolUse,
            "refusal" => Self::Refusal,
            _ => Self::Other,
        }
    }

    // Why: Gemini and some OpenAI-compatible providers report generic stop reasons
    // alongside tool calls.
    #[must_use]
    pub const fn with_tool_use(self, has_tool_use: bool) -> Self {
        match self {
            Self::EndTurn | Self::Other if has_tool_use => Self::ToolUse,
            other => other,
        }
    }

    pub fn from_openai(s: &str) -> Self {
        match s {
            "stop" => Self::EndTurn,
            "length" => Self::MaxTokens,
            "tool_calls" | "function_call" => Self::ToolUse,
            "content_filter" => Self::Refusal,
            _ => Self::Other,
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct GroundedSource {
    pub uri: String,
    pub title: Option<String>,
    pub snippet: Option<String>,
    pub relevance: Option<f32>,
}

#[derive(Debug, Clone, Default)]
pub struct Grounding {
    pub sources: Vec<GroundedSource>,
    pub queries: Vec<String>,
}

#[derive(Debug, Clone, Default)]
pub struct CodeExecutionOutput {
    pub language: Option<String>,
    pub code: String,
    pub result: Option<String>,
    pub outcome: Option<String>,
}

#[derive(Debug, Clone, Default)]
pub struct CanonicalResponse {
    pub id: String,
    pub model: String,
    pub content: Vec<CanonicalContent>,
    pub stop_reason: Option<CanonicalStopReason>,
    pub usage: CanonicalUsage,
    pub grounding: Option<Grounding>,
    pub code_execution: Option<CodeExecutionOutput>,
    pub raw_finish_reason: Option<String>,
    pub received_surface: ForwardedSurface,
}

impl CanonicalResponse {
    pub fn content_units(&self) -> Vec<String> {
        let mut units = Vec::with_capacity(self.content.len() + self.received_surface.len());
        for part in &self.content {
            let mut out = String::new();
            flatten_part(&mut out, part);
            if !out.is_empty() {
                units.push(out);
            }
        }
        for leaf in self.received_surface.leaves() {
            units.push(leaf.value.clone());
        }
        units
    }
}

#[derive(Debug, Clone)]
pub enum CanonicalEvent {
    MessageStart {
        id: String,
        model: String,
        usage: CanonicalUsage,
    },
    ContentBlockStart {
        index: u32,
        block: ContentBlockKind,
    },
    TextDelta {
        index: u32,
        text: String,
    },
    ThinkingDelta {
        index: u32,
        text: String,
    },
    SignatureDelta {
        index: u32,
        signature: String,
    },
    EncryptedContentDelta {
        index: u32,
        data: String,
    },
    ToolUseDelta {
        index: u32,
        partial_json: String,
    },
    ContentBlockStop {
        index: u32,
    },
    UsageDelta(CanonicalUsageUpdate),
    MessageStop {
        id: String,
        stop_reason: Option<CanonicalStopReason>,
        raw_finish_reason: Option<String>,
    },
    Error(String),
}

#[derive(Debug, Clone)]
pub enum ContentBlockKind {
    Text,
    Thinking {
        id: Option<String>,
        signature: Option<String>,
    },
    ToolUse {
        id: String,
        name: String,
        signature: Option<String>,
    },
}