1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[derive(Debug, Clone)]
7pub struct PolicyVersionInfo {
8 pub user_id: Uuid,
9 pub policy_version: u64,
10 pub generated_at: OffsetDateTime,
11}
12
13#[derive(Debug, Clone)]
16pub struct PolicySnapshot {
17 pub user_id: Uuid,
18 pub policy_version: u64,
19 pub model_catalog: Vec<ModelCatalogEntry>,
20 pub kill_switches: KillSwitches,
21}
22
23#[allow(clippy::struct_excessive_bools)]
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct KillSwitches {
27 pub disable_premium_tier: bool,
28 pub force_standard_tier: bool,
29 pub disable_web_search: bool,
30 pub disable_file_search: bool,
31 pub disable_images: bool,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ModelCatalogEntry {
37 pub model_id: String,
39 pub provider_model_id: String,
42 pub display_name: String,
44 pub description: String,
46 pub version: String,
48 pub provider_id: String,
50 pub provider_display_name: String,
53 pub icon: String,
55 pub tier: ModelTier,
57 pub enabled: bool,
58 pub multimodal_capabilities: Vec<String>,
60 pub context_window: u32,
62 pub max_output_tokens: u32,
64 pub max_input_tokens: u32,
66 pub input_tokens_credit_multiplier_micro: u64,
68 pub output_tokens_credit_multiplier_micro: u64,
70 pub multiplier_display: String,
72 pub estimation_budgets: EstimationBudgets,
74 pub max_retrieved_chunks_per_turn: u32,
76 pub general_config: ModelGeneralConfig,
78 pub preference: ModelPreference,
80}
81
82#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
84pub struct EstimationBudgets {
85 pub bytes_per_token_conservative: u32,
87 pub fixed_overhead_tokens: u32,
89 pub safety_margin_pct: u32,
91 pub image_token_budget: u32,
93 pub tool_surcharge_tokens: u32,
95 pub web_search_surcharge_tokens: u32,
97 pub minimal_generation_floor: u32,
99}
100
101impl Default for EstimationBudgets {
102 fn default() -> Self {
103 Self {
104 bytes_per_token_conservative: 4,
105 fixed_overhead_tokens: 100,
106 safety_margin_pct: 10,
107 image_token_budget: 1000,
108 tool_surcharge_tokens: 500,
109 web_search_surcharge_tokens: 500,
110 minimal_generation_floor: 50,
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ModelApiParams {
118 pub temperature: f64,
119 pub top_p: f64,
120 pub frequency_penalty: f64,
121 pub presence_penalty: f64,
122 pub stop: Vec<String>,
123}
124
125#[allow(clippy::struct_excessive_bools)]
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ModelFeatures {
129 pub streaming: bool,
130 pub function_calling: bool,
131 pub structured_output: bool,
132 pub fine_tuning: bool,
133 pub distillation: bool,
134 pub fim_completion: bool,
135 pub chat_prefix_completion: bool,
136}
137
138#[allow(clippy::struct_excessive_bools)]
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ModelInputType {
142 pub text: bool,
143 pub image: bool,
144 pub audio: bool,
145 pub video: bool,
146}
147
148#[allow(clippy::struct_excessive_bools)]
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ModelToolSupport {
152 pub web_search: bool,
153 pub file_search: bool,
154 pub image_generation: bool,
155 pub code_interpreter: bool,
156 pub computer_use: bool,
157 pub mcp: bool,
158}
159
160#[allow(clippy::struct_excessive_bools)]
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ModelSupportedEndpoints {
164 pub chat_completions: bool,
165 pub responses: bool,
166 pub realtime: bool,
167 pub assistants: bool,
168 pub batch_api: bool,
169 pub fine_tuning: bool,
170 pub embeddings: bool,
171 pub videos: bool,
172 pub image_generation: bool,
173 pub image_edit: bool,
174 pub audio_speech_generation: bool,
175 pub audio_transcription: bool,
176 pub audio_translation: bool,
177 pub moderations: bool,
178 pub completions: bool,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ModelTokenPolicy {
184 pub input_tokens_credit_multiplier: f64,
185 pub output_tokens_credit_multiplier: f64,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ModelPerformance {
191 pub response_latency_ms: u32,
192 pub speed_tokens_per_second: u32,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ModelGeneralConfig {
198 #[serde(rename = "type")]
200 pub config_type: String,
201 pub tier: String,
203 #[serde(with = "time::serde::rfc3339")]
204 pub available_from: OffsetDateTime,
205 pub max_file_size_mb: u32,
206 pub api_params: ModelApiParams,
207 pub features: ModelFeatures,
208 pub input_type: ModelInputType,
209 pub tool_support: ModelToolSupport,
210 pub supported_endpoints: ModelSupportedEndpoints,
211 pub token_policy: ModelTokenPolicy,
212 pub performance: ModelPerformance,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ModelPreference {
218 pub is_default: bool,
219 pub sort_order: i32,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229pub enum ModelTier {
230 #[serde(alias = "standard")]
231 Standard,
232 #[serde(alias = "premium")]
233 Premium,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct UserLimits {
240 pub user_id: Uuid,
241 pub policy_version: u64,
242 pub standard: TierLimits,
243 pub premium: TierLimits,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct TierLimits {
249 pub limit_daily_credits_micro: i64,
250 pub limit_monthly_credits_micro: i64,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct UsageTokens {
256 pub input_tokens: u64,
257 pub output_tokens: u64,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct UsageEvent {
266 pub tenant_id: Uuid,
267 pub user_id: Uuid,
268 pub chat_id: Uuid,
269 pub turn_id: Uuid,
270 pub request_id: Uuid,
271 pub effective_model: String,
272 pub selected_model: String,
273 pub terminal_state: String,
274 pub billing_outcome: String,
275 pub usage: Option<UsageTokens>,
276 pub actual_credits_micro: i64,
277 pub settlement_method: String,
278 pub policy_version_applied: i64,
279 #[serde(with = "time::serde::rfc3339")]
280 pub timestamp: OffsetDateTime,
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
292 fn kill_switches_default_all_disabled() {
293 let ks = KillSwitches::default();
294 assert!(!ks.disable_premium_tier);
295 assert!(!ks.force_standard_tier);
296 assert!(!ks.disable_web_search);
297 assert!(!ks.disable_file_search);
298 assert!(!ks.disable_images);
299 }
300
301 #[test]
307 fn estimation_budgets_default_matches_spec() {
308 let eb = EstimationBudgets::default();
309 assert_eq!(eb.bytes_per_token_conservative, 4);
310 assert_eq!(eb.fixed_overhead_tokens, 100);
311 assert_eq!(eb.safety_margin_pct, 10);
312 assert_eq!(eb.image_token_budget, 1000);
313 assert_eq!(eb.tool_surcharge_tokens, 500);
314 assert_eq!(eb.web_search_surcharge_tokens, 500);
315 assert_eq!(eb.minimal_generation_floor, 50);
316 }
317
318 fn sample_general_config() -> ModelGeneralConfig {
323 ModelGeneralConfig {
324 config_type: "model.general.v1".to_owned(),
325 tier: "premium".to_owned(),
326 available_from: OffsetDateTime::UNIX_EPOCH,
327 max_file_size_mb: 25,
328 api_params: ModelApiParams {
329 temperature: 0.7,
330 top_p: 1.0,
331 frequency_penalty: 0.0,
332 presence_penalty: 0.0,
333 stop: vec![],
334 },
335 features: ModelFeatures {
336 streaming: true,
337 function_calling: false,
338 structured_output: false,
339 fine_tuning: false,
340 distillation: false,
341 fim_completion: false,
342 chat_prefix_completion: false,
343 },
344 input_type: ModelInputType {
345 text: true,
346 image: false,
347 audio: false,
348 video: false,
349 },
350 tool_support: ModelToolSupport {
351 web_search: false,
352 file_search: false,
353 image_generation: false,
354 code_interpreter: false,
355 computer_use: false,
356 mcp: false,
357 },
358 supported_endpoints: ModelSupportedEndpoints {
359 chat_completions: true,
360 responses: false,
361 realtime: false,
362 assistants: false,
363 batch_api: false,
364 fine_tuning: false,
365 embeddings: false,
366 videos: false,
367 image_generation: false,
368 image_edit: false,
369 audio_speech_generation: false,
370 audio_transcription: false,
371 audio_translation: false,
372 moderations: false,
373 completions: false,
374 },
375 token_policy: ModelTokenPolicy {
376 input_tokens_credit_multiplier: 1.0,
377 output_tokens_credit_multiplier: 3.0,
378 },
379 performance: ModelPerformance {
380 response_latency_ms: 500,
381 speed_tokens_per_second: 100,
382 },
383 }
384 }
385
386 #[test]
387 fn general_config_serializes_type_not_config_type() {
388 let config = sample_general_config();
389 let json = serde_json::to_value(&config).unwrap();
390
391 assert!(json.get("type").is_some(), "expected JSON key 'type'");
392 assert!(
393 json.get("config_type").is_none(),
394 "config_type must not appear in JSON output"
395 );
396 assert_eq!(json["type"], "model.general.v1");
397 }
398
399 #[test]
400 fn general_config_serde_roundtrip_preserves_rename() {
401 let original = sample_general_config();
402 let json = serde_json::to_value(&original).unwrap();
403 let deserialized: ModelGeneralConfig = serde_json::from_value(json).unwrap();
404
405 assert_eq!(deserialized.config_type, original.config_type);
406 assert_eq!(deserialized.tier, original.tier);
407 }
408
409 #[test]
414 fn model_tier_serializes_as_pascal_case() {
415 let json = serde_json::to_value(ModelTier::Premium).unwrap();
416 assert_eq!(json, serde_json::json!("Premium"));
417
418 let json = serde_json::to_value(ModelTier::Standard).unwrap();
419 assert_eq!(json, serde_json::json!("Standard"));
420 }
421
422 #[test]
423 fn model_tier_deserializes_lowercase_aliases() {
424 let premium: ModelTier = serde_json::from_value(serde_json::json!("premium")).unwrap();
425 assert_eq!(premium, ModelTier::Premium);
426
427 let standard: ModelTier = serde_json::from_value(serde_json::json!("standard")).unwrap();
428 assert_eq!(standard, ModelTier::Standard);
429 }
430
431 #[test]
432 fn model_tier_rejects_unknown_casing() {
433 let result = serde_json::from_value::<ModelTier>(serde_json::json!("PREMIUM"));
434 assert!(result.is_err());
435 }
436
437 #[test]
442 fn kill_switches_serde_roundtrip_with_enabled_switches() {
443 let ks = KillSwitches {
444 disable_premium_tier: true,
445 force_standard_tier: false,
446 disable_web_search: true,
447 disable_file_search: false,
448 disable_images: true,
449 };
450 let json = serde_json::to_value(&ks).unwrap();
451 let deserialized: KillSwitches = serde_json::from_value(json).unwrap();
452
453 assert!(deserialized.disable_premium_tier);
454 assert!(!deserialized.force_standard_tier);
455 assert!(deserialized.disable_web_search);
456 assert!(!deserialized.disable_file_search);
457 assert!(deserialized.disable_images);
458 }
459
460 #[test]
461 fn kill_switches_default_roundtrips_all_false() {
462 let ks = KillSwitches::default();
463 let json = serde_json::to_value(&ks).unwrap();
464 let deserialized: KillSwitches = serde_json::from_value(json).unwrap();
465
466 assert!(!deserialized.disable_premium_tier);
467 assert!(!deserialized.force_standard_tier);
468 assert!(!deserialized.disable_web_search);
469 assert!(!deserialized.disable_file_search);
470 assert!(!deserialized.disable_images);
471 }
472}