systemprompt-models 0.73.0

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
//! Scope attribution of an AI request.
//!
//! The value the request is charged to in each subject dimension a tenant
//! registers (`project`, `cost_centre`, ...), which signal decided each value,
//! and the API key that authenticated it.
//!
//! Core owns no organisation model. The entry layer resolves one
//! [`AttributionEntry`] per registered dimension (the
//! `x-systemprompt-scope-<dimension>` header, then the API key's bound value,
//! then the dimension provider's first value) and the gateway carries the set
//! unchanged onto the audit row, the quota windows and the OTLP export.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use serde::{Deserialize, Serialize};
use systemprompt_identifiers::{ApiKeyId, ScopeDimension};

/// Which signal decided an attributed value. Persisted as
/// `ai_request_attributions.source`, a CHECK-constrained column.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AttributionSource {
    Header,
    ApiKey,
    Default,
}

impl AttributionSource {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Header => "header",
            Self::ApiKey => "api_key",
            Self::Default => "default",
        }
    }

    #[must_use]
    pub fn parse(value: &str) -> Option<Self> {
        match value {
            "header" => Some(Self::Header),
            "api_key" => Some(Self::ApiKey),
            "default" => Some(Self::Default),
            _ => None,
        }
    }
}

impl std::fmt::Display for AttributionSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// The value one request is attributed to in one dimension.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttributionEntry {
    pub dimension: ScopeDimension,
    pub value: String,
    pub source: AttributionSource,
}

/// The resolved scope attribution of one request.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RequestAttribution {
    pub entries: Vec<AttributionEntry>,
    pub api_key_id: Option<ApiKeyId>,
}

impl RequestAttribution {
    #[must_use]
    pub fn none() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn value_for(&self, dimension: &str) -> Option<&str> {
        self.entries
            .iter()
            .find(|entry| entry.dimension.as_str() == dimension)
            .map(|entry| entry.value.as_str())
    }
}

/// A value an API key is bound to in one dimension; it attributes the key's
/// requests when they carry no header for that dimension.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScopeBinding {
    pub dimension: ScopeDimension,
    pub value: String,
}

impl ScopeBinding {
    #[must_use]
    pub fn traffic(class: TrafficClass) -> Self {
        Self {
            dimension: ScopeDimension::new(TrafficClass::DIMENSION),
            value: class.as_str().to_owned(),
        }
    }
}

/// The canonical traffic class of a request, persisted as
/// `ai_requests.traffic`. `Evaluation` is reachable only through an API key
/// bound to it, so production usage cannot be relabelled by a header.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrafficClass {
    #[default]
    Production,
    Evaluation,
}

impl TrafficClass {
    pub const DIMENSION: &'static str = "traffic";

    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Production => "production",
            Self::Evaluation => "evaluation",
        }
    }

    #[must_use]
    pub fn parse(value: &str) -> Option<Self> {
        match value {
            "production" => Some(Self::Production),
            "evaluation" => Some(Self::Evaluation),
            _ => None,
        }
    }

    #[must_use]
    pub fn of(attribution: &RequestAttribution) -> Option<Self> {
        attribution
            .value_for(Self::DIMENSION)
            .map_or(Some(Self::Production), Self::parse)
    }
}

impl std::fmt::Display for TrafficClass {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}