1use 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 #[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, }
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
84pub 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}