llm_cost_ops_api/ingestion/
models.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7use validator::Validate;
8
9use llm_cost_ops::domain::{ModelIdentifier, Provider};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
13pub struct UsageWebhookPayload {
14 #[serde(default = "Uuid::new_v4")]
16 pub request_id: Uuid,
17
18 pub timestamp: DateTime<Utc>,
20
21 #[validate(length(min = 1))]
23 pub provider: String,
24
25 #[validate]
27 pub model: ModelWebhook,
28
29 #[validate(length(min = 1, max = 255))]
31 pub organization_id: String,
32
33 #[validate(length(max = 255))]
35 pub project_id: Option<String>,
36
37 #[validate(length(max = 255))]
39 pub user_id: Option<String>,
40
41 #[validate]
43 pub usage: TokenUsageWebhook,
44
45 pub performance: Option<PerformanceMetrics>,
47
48 #[serde(default)]
50 pub tags: Vec<String>,
51
52 #[serde(default)]
54 pub metadata: HashMap<String, serde_json::Value>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
59pub struct ModelWebhook {
60 #[validate(length(min = 1, max = 255))]
61 pub name: String,
62
63 #[validate(length(max = 100))]
64 pub version: Option<String>,
65
66 #[validate(range(min = 1))]
67 pub context_window: Option<u64>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
72pub struct TokenUsageWebhook {
73 #[validate(range(min = 0))]
74 pub prompt_tokens: u64,
75
76 #[validate(range(min = 0))]
77 pub completion_tokens: u64,
78
79 #[validate(range(min = 0))]
80 pub total_tokens: u64,
81
82 #[validate(range(min = 0))]
83 pub cached_tokens: Option<u64>,
84
85 #[validate(range(min = 0))]
86 pub reasoning_tokens: Option<u64>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
91pub struct PerformanceMetrics {
92 #[validate(range(min = 0))]
94 pub latency_ms: Option<u64>,
95
96 #[validate(range(min = 0))]
98 pub time_to_first_token_ms: Option<u64>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
103pub struct BatchIngestionRequest {
104 #[serde(default = "Uuid::new_v4")]
106 pub batch_id: Uuid,
107
108 #[validate(length(min = 1, max = 255))]
110 pub source: String,
111
112 #[validate(length(min = 1, max = 1000))]
114 #[validate]
115 pub records: Vec<UsageWebhookPayload>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct IngestionResponse {
121 pub request_id: Uuid,
123
124 pub status: IngestionStatus,
126
127 pub accepted: usize,
129
130 pub rejected: usize,
132
133 #[serde(skip_serializing_if = "Vec::is_empty")]
135 pub errors: Vec<IngestionError>,
136
137 pub processed_at: DateTime<Utc>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
143#[serde(rename_all = "lowercase")]
144pub enum IngestionStatus {
145 Success,
147
148 Partial,
150
151 Failed,
153
154 Queued,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct IngestionError {
161 pub index: Option<usize>,
163
164 pub code: String,
166
167 pub message: String,
169
170 pub field: Option<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct StreamMessage {
177 pub message_id: String,
179
180 pub event_type: StreamEventType,
182
183 pub created_at: DateTime<Utc>,
185
186 pub payload: UsageWebhookPayload,
188
189 #[serde(default)]
191 pub retry_count: u32,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
196#[serde(rename_all = "snake_case")]
197pub enum StreamEventType {
198 UsageCreated,
200
201 UsageUpdated,
203
204 BatchUploaded,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct IngestionConfig {
211 pub webhook_enabled: bool,
213
214 pub webhook_bind: String,
216
217 pub nats_enabled: bool,
219
220 pub nats_urls: Vec<String>,
222
223 pub nats_subject: String,
225
226 pub redis_enabled: bool,
228
229 pub redis_url: Option<String>,
231
232 pub redis_stream_key: String,
234
235 pub buffer_size: usize,
237
238 pub max_batch_size: usize,
240
241 pub request_timeout_secs: u64,
243
244 pub retry: RetryConfig,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct RetryConfig {
251 pub max_retries: u32,
253
254 pub initial_delay_ms: u64,
256
257 pub max_delay_ms: u64,
259
260 pub backoff_multiplier: f64,
262}
263
264impl Default for IngestionConfig {
265 fn default() -> Self {
266 Self {
267 webhook_enabled: true,
268 webhook_bind: "0.0.0.0:8080".to_string(),
269 nats_enabled: false,
270 nats_urls: vec!["nats://localhost:4222".to_string()],
271 nats_subject: "llm.usage".to_string(),
272 redis_enabled: false,
273 redis_url: None,
274 redis_stream_key: "llm:usage".to_string(),
275 buffer_size: 10000,
276 max_batch_size: 1000,
277 request_timeout_secs: 30,
278 retry: RetryConfig::default(),
279 }
280 }
281}
282
283impl Default for RetryConfig {
284 fn default() -> Self {
285 Self {
286 max_retries: 3,
287 initial_delay_ms: 100,
288 max_delay_ms: 30000,
289 backoff_multiplier: 2.0,
290 }
291 }
292}
293
294impl UsageWebhookPayload {
295 pub fn to_usage_record(&self) -> llm_cost_ops::UsageRecord {
297 use llm_cost_ops::domain::IngestionSource;
298
299 llm_cost_ops::UsageRecord {
300 id: self.request_id,
301 timestamp: self.timestamp,
302 provider: Provider::parse(&self.provider),
303 model: ModelIdentifier {
304 name: self.model.name.clone(),
305 version: self.model.version.clone(),
306 context_window: self.model.context_window,
307 },
308 organization_id: self.organization_id.clone(),
309 project_id: self.project_id.clone(),
310 user_id: self.user_id.clone(),
311 prompt_tokens: self.usage.prompt_tokens,
312 completion_tokens: self.usage.completion_tokens,
313 total_tokens: self.usage.total_tokens,
314 cached_tokens: self.usage.cached_tokens,
315 reasoning_tokens: self.usage.reasoning_tokens,
316 latency_ms: self.performance.as_ref().and_then(|p| p.latency_ms),
317 time_to_first_token_ms: self
318 .performance
319 .as_ref()
320 .and_then(|p| p.time_to_first_token_ms),
321 tags: self.tags.clone(),
322 metadata: serde_json::to_value(&self.metadata).unwrap_or(serde_json::json!({})),
323 ingested_at: Utc::now(),
324 source: IngestionSource::Webhook {
325 endpoint: "/v1/usage".to_string(),
326 },
327 }
328 }
329}