Skip to main content

flowcat_core/processor/
metrics.rs

1// SPDX-License-Identifier: Apache-2.0
2//
3//! Metrics carried on [`Frame::Metrics`](crate::processor::frame::Frame::Metrics).
4//!
5//! Field-for-field port of pipecat `metrics/metrics.py` (PROCESSOR-DESIGN §5.2):
6//! TTFB, processing time, LLM/TTS usage, and turn-prediction metrics.
7
8use serde::{Deserialize, Serialize};
9
10/// One metrics record. Mirrors pipecat's `MetricsData` subclasses
11/// (`metrics.py:29/39/68/78/101`).
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub enum MetricsData {
14    /// Time-to-first-byte for a service (`metrics.py:29`).
15    Ttfb {
16        processor: String,
17        model: Option<String>,
18        seconds: f64,
19    },
20    /// End-to-end processing time for one frame/turn (`metrics.py:39`).
21    Processing {
22        processor: String,
23        model: Option<String>,
24        seconds: f64,
25    },
26    /// LLM token usage (`metrics.py:68`).
27    LlmUsage {
28        processor: String,
29        model: Option<String>,
30        tokens: LlmTokenUsage,
31    },
32    /// TTS character usage (`metrics.py:78`).
33    TtsUsage { processor: String, characters: u64 },
34    /// End-of-turn prediction metric (`metrics.py:101`).
35    TurnPrediction {
36        processor: String,
37        is_complete: bool,
38        probability: f32,
39        e2e_processing_ms: f64,
40    },
41}
42
43/// LLM token accounting. Mirrors pipecat `LLMTokenUsage` (`metrics.py:49`).
44#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
45pub struct LlmTokenUsage {
46    pub prompt_tokens: u64,
47    pub completion_tokens: u64,
48    pub total_tokens: u64,
49    pub cache_read_input_tokens: Option<u64>,
50    pub cache_creation_input_tokens: Option<u64>,
51    pub reasoning_tokens: Option<u64>,
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn metrics_data_serde_roundtrips_every_variant() {
60        let cases = vec![
61            MetricsData::Ttfb {
62                processor: "stt".into(),
63                model: Some("nova-2".into()),
64                seconds: 0.123,
65            },
66            MetricsData::Processing {
67                processor: "llm".into(),
68                model: None,
69                seconds: 1.5,
70            },
71            MetricsData::LlmUsage {
72                processor: "llm".into(),
73                model: Some("gpt-4o".into()),
74                tokens: LlmTokenUsage {
75                    prompt_tokens: 10,
76                    completion_tokens: 20,
77                    total_tokens: 30,
78                    cache_read_input_tokens: Some(4),
79                    cache_creation_input_tokens: None,
80                    reasoning_tokens: Some(2),
81                },
82            },
83            MetricsData::TtsUsage {
84                processor: "tts".into(),
85                characters: 42,
86            },
87            MetricsData::TurnPrediction {
88                processor: "turn".into(),
89                is_complete: true,
90                probability: 0.97,
91                e2e_processing_ms: 12.0,
92            },
93        ];
94        for m in cases {
95            let json = serde_json::to_string(&m).expect("serialize");
96            let back: MetricsData = serde_json::from_str(&json).expect("deserialize");
97            assert_eq!(m, back);
98        }
99    }
100
101    #[test]
102    fn llm_token_usage_defaults_to_zero() {
103        let u = LlmTokenUsage::default();
104        assert_eq!(u.total_tokens, 0);
105        assert!(u.cache_read_input_tokens.is_none());
106    }
107}