runifold-model 0.1.0

Provider-neutral model protocol and streaming primitives for Runifold
Documentation
use std::collections::BTreeMap;

use runifold_core::Usage;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{ContentPart, ModelRef, ProviderData};

/// Why a model stopped producing output.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum FinishReason {
    /// Natural completion.
    Stop,
    /// Output-token or context limit.
    Length,
    /// The model requested one or more tools.
    ToolCalls,
    /// Provider safety or content filter.
    ContentFilter,
    /// The operation was cancelled.
    Cancelled,
    /// Provider reported an error as a terminal reason.
    Error,
    /// Provider-specific reason retained as text.
    Other(String),
    /// No reliable reason was provided.
    #[default]
    Unknown,
}

/// Detailed usage reported by a model provider.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ModelUsage {
    /// Input tokens.
    pub input_tokens: u64,
    /// Output tokens.
    pub output_tokens: u64,
    /// Reasoning tokens, when reported separately.
    pub reasoning_tokens: u64,
    /// Input tokens served from provider cache.
    pub cached_input_tokens: u64,
    /// Input tokens written to provider cache.
    pub cache_write_tokens: u64,
    /// Estimated or reported cost in micro-US-dollars.
    pub cost_microusd: u64,
}

impl ModelUsage {
    /// Returns total model tokens without double-counting usage details.
    ///
    /// Reasoning tokens are normally a subset of output tokens, just as cached
    /// tokens are a subset of input tokens.
    pub fn total_tokens(self) -> u64 {
        self.input_tokens.saturating_add(self.output_tokens)
    }
}

impl From<ModelUsage> for Usage {
    fn from(value: ModelUsage) -> Self {
        Self {
            tokens: value.total_tokens(),
            cost_microusd: value.cost_microusd,
            ..Self::default()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::ModelUsage;

    #[test]
    fn token_totals_do_not_double_count_reasoning_details() {
        let usage = ModelUsage {
            input_tokens: 10,
            output_tokens: 8,
            reasoning_tokens: 3,
            cached_input_tokens: 4,
            ..ModelUsage::default()
        };

        assert_eq!(usage.total_tokens(), 18);
    }
}

/// A visible feature degradation or translation warning.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ModelWarning {
    /// Stable warning code.
    pub code: String,
    /// Safe explanation.
    pub message: String,
    /// Namespaced details.
    pub metadata: BTreeMap<String, Value>,
}

/// A complete canonical model response.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ModelResponse {
    /// Provider response identity.
    pub id: Option<String>,
    /// Actual model that produced the response.
    pub model: ModelRef,
    /// Ordered output content.
    pub content: Vec<ContentPart>,
    /// Normalized terminal reason.
    pub finish_reason: FinishReason,
    /// Detailed model usage.
    pub usage: ModelUsage,
    /// Explicit degradation and compatibility warnings.
    pub warnings: Vec<ModelWarning>,
    /// Namespaced response metadata.
    pub provider_metadata: BTreeMap<String, Value>,
    /// Provider stream events retained without normalization.
    pub provider_events: Vec<ProviderData>,
}