Skip to main content

tt_preview/
types.rs

1//! Request + response shapes for the preview engine.
2//!
3//! The request is a small subset of the OpenAI chat-completion body —
4//! exactly what we need to estimate. The response is the documented
5//! `PreviewResponse` shape from the spec.
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Deserialize)]
10pub struct PreviewRequest {
11    pub model: String,
12    pub messages: Vec<Message>,
13    #[serde(default)]
14    pub max_tokens: Option<u32>,
15    #[serde(default)]
16    pub tools: Option<serde_json::Value>,
17    /// Honored for tier accounting but ignored for the preview calc itself.
18    #[serde(default)]
19    pub stream: Option<bool>,
20}
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct Message {
24    pub role: String,
25    pub content: serde_json::Value, // string OR array-of-parts
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct PreviewResponse {
30    pub current: CurrentEstimate,
31    pub cache_projections: CacheProjections,
32    pub route_suggestions: Vec<RouteSuggestion>,
33    pub warnings: Vec<String>,
34    pub trace_id: String,
35}
36
37#[derive(Debug, Clone, Serialize)]
38pub struct CurrentEstimate {
39    pub model: String,
40    pub provider: String,
41    pub input_tokens_estimated: u32,
42    pub output_tokens_estimated: u32,
43    pub cost_usd: f64,
44    pub estimation_confidence: EstimationConfidence,
45}
46
47#[derive(Debug, Clone, Copy, Serialize)]
48#[serde(rename_all = "lowercase")]
49pub enum EstimationConfidence {
50    High,
51    Medium,
52    Low,
53}
54
55#[derive(Debug, Clone, Serialize)]
56pub struct CacheProjections {
57    pub l1_hit_savings_usd: f64,
58    pub l1_hit_probability: f32,
59    pub l2_hit_savings_usd: f64,
60    pub l2_hit_probability: f32,
61    pub weighted_savings_usd: f64,
62}
63
64#[derive(Debug, Clone, Serialize)]
65pub struct RouteSuggestion {
66    pub route: String,
67    pub model: String,
68    pub cost_usd: f64,
69    pub savings_usd: f64,
70    pub quality_risk_band: QualityRiskBand,
71    pub rationale: String,
72    pub applicable: bool,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
76#[serde(rename_all = "UPPERCASE")]
77pub enum QualityRiskBand {
78    Low,
79    Medium,
80    High,
81    Unknown,
82}
83
84/// Convenience alias for the legacy name in the spec.
85pub type Suggestion = RouteSuggestion;
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn deserializes_minimal_request() {
93        let json = r#"{"model":"x","messages":[{"role":"user","content":"hi"}]}"#;
94        let req: PreviewRequest = serde_json::from_str(json).unwrap();
95        assert_eq!(req.model, "x");
96        assert_eq!(req.messages.len(), 1);
97    }
98
99    #[test]
100    fn serializes_response_shape_matches_spec() {
101        let r = PreviewResponse {
102            current: CurrentEstimate {
103                model: "claude-sonnet-4-6".into(),
104                provider: "anthropic".into(),
105                input_tokens_estimated: 47,
106                output_tokens_estimated: 12,
107                cost_usd: 0.000189,
108                estimation_confidence: EstimationConfidence::High,
109            },
110            cache_projections: CacheProjections {
111                l1_hit_savings_usd: 0.000189,
112                l1_hit_probability: 0.34,
113                l2_hit_savings_usd: 0.000189,
114                l2_hit_probability: 0.18,
115                weighted_savings_usd: 0.000098,
116            },
117            route_suggestions: vec![],
118            warnings: vec![],
119            trace_id: "trace".into(),
120        };
121        let json = serde_json::to_string(&r).unwrap();
122        assert!(json.contains("\"estimation_confidence\":\"high\""));
123        assert!(json.contains("\"weighted_savings_usd\":0.000098"));
124    }
125}