1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11pub(crate) const SCHEMA: &str = "leanctx.dashboard-pro/1";
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub(crate) struct Request {
16 pub(crate) schema: String,
17 pub(crate) op: String,
18 pub(crate) now_utc: String,
19 pub(crate) project: Project,
20 pub(crate) query: Map<String, Value>,
21 pub(crate) body: Value,
22 pub(crate) inputs: Map<String, Value>,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub(crate) struct Project {
27 pub(crate) root_hash: String,
28 pub(crate) labels: Vec<ProjectLabel>,
29 pub(crate) repo_hashes: Vec<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub(crate) name: Option<String>,
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
35pub(crate) struct ProjectLabel {
36 pub(crate) hash: String,
37 pub(crate) name: String,
38}
39
40#[derive(Debug, Clone, Default, Deserialize)]
42pub(crate) struct Response {
43 pub(crate) status: u16,
44 #[serde(default)]
45 pub(crate) body: Option<Value>,
46 #[serde(default)]
47 pub(crate) file: Option<FileOut>,
48 #[serde(default)]
51 pub(crate) plans: Option<Vec<Value>>,
52 #[serde(default)]
53 pub(crate) error: Option<ErrorOut>,
54}
55
56#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
57pub(crate) struct FileOut {
58 pub(crate) name: String,
59 pub(crate) mime: String,
60 pub(crate) bytes_b64: String,
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
64pub(crate) struct ErrorOut {
65 pub(crate) code: String,
66 pub(crate) message: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(tag = "effect", deny_unknown_fields)]
72pub(crate) enum Plan {
73 #[serde(rename = "alerts.settings")]
74 AlertsSettings { settings: AlertSettings },
75 #[serde(rename = "alerts.notify")]
76 AlertsNotify {
77 at: String,
78 text: String,
79 counts: BTreeMap<String, i64>,
80 test: bool,
81 desktop: bool,
82 webhook: String,
83 },
84 #[serde(rename = "profiles.put")]
85 ProfilesPut {
86 name: String,
87 values: Map<String, Value>,
88 saved_at: String,
89 },
90 #[serde(rename = "profiles.delete")]
91 ProfilesDelete { name: String },
92 #[serde(rename = "config.set")]
93 ConfigSet {
94 key: String,
95 wire: String,
96 expected: Value,
97 weakening: bool,
98 },
99 #[serde(rename = "memory.snapshot")]
100 MemorySnapshot { reason: String },
101 #[serde(rename = "memory.restore")]
102 MemoryRestore {
103 id: String,
104 missing: Vec<String>,
105 changed: Vec<String>,
106 },
107 #[serde(rename = "memory.forget")]
108 MemoryForget { refs: Vec<String>, reason: String },
109 #[serde(rename = "memory.consolidate")]
110 MemoryConsolidate { run: bool },
111 #[serde(rename = "context.overlay")]
112 ContextOverlay { action: String, path: String },
113 #[serde(rename = "adaptive.record")]
114 AdaptiveRecord { item: AppliedItem },
115 #[serde(rename = "adaptive.dismiss")]
116 AdaptiveDismiss { path: String },
117 #[serde(rename = "ledger.export")]
118 LedgerExport {
119 format: String,
120 file_name: String,
121 mime: String,
122 filters: LedgerFilters,
123 project_column: ProjectColumn,
124 },
125}
126
127impl Plan {
128 pub(crate) fn effect(&self) -> &'static str {
129 match self {
130 Self::AlertsSettings { .. } => "alerts.settings",
131 Self::AlertsNotify { .. } => "alerts.notify",
132 Self::ProfilesPut { .. } => "profiles.put",
133 Self::ProfilesDelete { .. } => "profiles.delete",
134 Self::ConfigSet { .. } => "config.set",
135 Self::MemorySnapshot { .. } => "memory.snapshot",
136 Self::MemoryRestore { .. } => "memory.restore",
137 Self::MemoryForget { .. } => "memory.forget",
138 Self::MemoryConsolidate { .. } => "memory.consolidate",
139 Self::ContextOverlay { .. } => "context.overlay",
140 Self::AdaptiveRecord { .. } => "adaptive.record",
141 Self::AdaptiveDismiss { .. } => "adaptive.dismiss",
142 Self::LedgerExport { .. } => "ledger.export",
143 }
144 }
145}
146
147#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
149pub(crate) struct LedgerFilters {
150 pub(crate) scope: Option<Vec<String>>,
151 pub(crate) repos: Option<RepoSelection>,
152 pub(crate) proxy: bool,
153 pub(crate) tool: String,
154 pub(crate) model: String,
155 pub(crate) text: String,
156 pub(crate) from: String,
157 pub(crate) to: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
161#[serde(tag = "kind", rename_all = "snake_case")]
162pub(crate) enum RepoSelection {
163 Only { hashes: Vec<String> },
164 Except { hashes: Vec<String> },
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(tag = "kind", rename_all = "snake_case")]
169pub(crate) enum ProjectColumn {
170 Fixed { name: String },
171 Labels { labels: Vec<ProjectLabel> },
172}
173
174pub(crate) mod lenient {
176 use serde::{Deserialize, Deserializer};
177 use serde_json::Value;
178
179 pub(crate) fn text_of(value: &Value) -> Option<String> {
180 match value {
181 Value::Null => None,
182 Value::String(text) => Some(text.clone()),
183 Value::Bool(flag) => Some(if *flag { "True" } else { "False" }.to_string()),
184 Value::Number(number) => Some(number.to_string()),
185 other => Some(other.to_string()),
186 }
187 }
188
189 pub(crate) fn int_of(value: &Value) -> Option<i64> {
190 match value {
191 Value::Number(number) => number
192 .as_i64()
193 .or_else(|| {
194 number
195 .as_u64()
196 .map(|n| i64::try_from(n).unwrap_or(i64::MAX))
197 })
198 .or_else(|| number.as_f64().map(|f| f.trunc() as i64)),
199 Value::Bool(flag) => Some(i64::from(*flag)),
200 Value::String(text) => text.trim().parse().ok(),
201 _ => None,
202 }
203 }
204
205 pub(crate) fn float_of(value: &Value) -> Option<f64> {
206 match value {
207 Value::Number(number) => number.as_f64(),
208 Value::Bool(flag) => Some(f64::from(u8::from(*flag))),
209 Value::String(text) => text.trim().parse().ok(),
210 _ => None,
211 }
212 }
213
214 pub(crate) fn opt_str<'de, D: Deserializer<'de>>(de: D) -> Result<Option<String>, D::Error> {
215 Ok(text_of(&Value::deserialize(de)?))
216 }
217
218 pub(crate) fn opt_i64<'de, D: Deserializer<'de>>(de: D) -> Result<Option<i64>, D::Error> {
219 Ok(int_of(&Value::deserialize(de)?))
220 }
221
222 pub(crate) fn opt_f64<'de, D: Deserializer<'de>>(de: D) -> Result<Option<f64>, D::Error> {
223 Ok(float_of(&Value::deserialize(de)?))
224 }
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
229pub struct LedgerEntry {
230 #[serde(default, deserialize_with = "lenient::opt_str")]
231 pub ts: Option<String>,
232 #[serde(default, deserialize_with = "lenient::opt_str")]
233 pub(crate) tool: Option<String>,
234 #[serde(default, deserialize_with = "lenient::opt_str")]
235 pub(crate) mechanism: Option<String>,
236 #[serde(default, deserialize_with = "lenient::opt_str")]
237 pub(crate) model_id: Option<String>,
238 #[serde(default, deserialize_with = "lenient::opt_str")]
239 pub(crate) version: Option<String>,
240 #[serde(default, deserialize_with = "lenient::opt_i64")]
241 pub baseline_tokens: Option<i64>,
242 #[serde(default, deserialize_with = "lenient::opt_i64")]
243 pub actual_tokens: Option<i64>,
244 #[serde(default, deserialize_with = "lenient::opt_i64")]
245 pub saved_tokens: Option<i64>,
246 #[serde(default, deserialize_with = "lenient::opt_i64")]
247 pub bounce_adjustment: Option<i64>,
248 #[serde(default, deserialize_with = "lenient::opt_f64")]
249 pub(crate) saved_usd: Option<f64>,
250 #[serde(default, deserialize_with = "lenient::opt_f64")]
251 pub(crate) unit_price_per_m_usd: Option<f64>,
252 #[serde(default, deserialize_with = "lenient::opt_i64")]
253 pub(crate) lines_added: Option<i64>,
254 #[serde(default, deserialize_with = "lenient::opt_i64")]
255 pub(crate) lines_removed: Option<i64>,
256 #[serde(default, deserialize_with = "lenient::opt_i64")]
257 pub(crate) loc_added: Option<i64>,
258 #[serde(default, deserialize_with = "lenient::opt_i64")]
259 pub(crate) loc_removed: Option<i64>,
260 #[serde(default, deserialize_with = "lenient::opt_str")]
261 pub measurement_method: Option<String>,
262 #[serde(default, deserialize_with = "lenient::opt_str")]
263 pub evidence_class: Option<String>,
264 #[serde(
266 default,
267 deserialize_with = "lenient::opt_f64",
268 skip_serializing_if = "Option::is_none"
269 )]
270 pub confidence: Option<f64>,
271 #[serde(default, deserialize_with = "lenient::opt_str")]
272 pub(crate) attribution_group: Option<String>,
273 #[serde(default, deserialize_with = "lenient::opt_str")]
274 pub repo_hash: Option<String>,
275 #[serde(default, deserialize_with = "lenient::opt_str")]
276 pub(crate) prev_hash: Option<String>,
277 #[serde(default, deserialize_with = "lenient::opt_str")]
278 pub(crate) entry_hash: Option<String>,
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
282pub(crate) struct LedgerPage {
283 pub(crate) available: bool,
284 pub(crate) total: i64,
285 pub(crate) offset: i64,
286 pub(crate) limit: i64,
287 pub(crate) entries: Vec<LedgerEntry>,
288 pub(crate) from: String,
289 pub(crate) to: String,
290}
291
292#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
293pub(crate) struct LedgerFacets {
294 pub(crate) entries: i64,
295 pub(crate) first: String,
296 pub(crate) last: String,
297 pub(crate) proxied: i64,
298 pub(crate) models: Vec<ModelCount>,
299 pub(crate) tools: Vec<ToolCount>,
300 pub(crate) repos: Vec<RepoCount>,
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
304pub(crate) struct ModelCount {
305 pub(crate) model: String,
306 pub(crate) entries: i64,
307}
308
309#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
310pub(crate) struct ToolCount {
311 pub(crate) tool: String,
312 pub(crate) entries: i64,
313}
314
315#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
316pub(crate) struct RepoCount {
317 pub(crate) repo: String,
318 pub(crate) entries: i64,
319}
320
321#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
322pub(crate) struct LedgerTotals {
323 pub(crate) tools: BTreeMap<String, ToolTotals>,
324 pub(crate) days: Vec<DayTotals>,
325}
326
327#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
328pub(crate) struct ToolTotals {
329 pub(crate) calls: i64,
330 pub(crate) saved: i64,
331 pub(crate) baseline: i64,
332 pub(crate) usd: f64,
333 pub(crate) corrected: i64,
334 pub(crate) lines_added: i64,
335 pub(crate) lines_removed: i64,
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
339pub(crate) struct DayTotals {
340 pub(crate) day: String,
341 pub(crate) calls: i64,
342 pub(crate) saved: i64,
343 pub(crate) corrected: i64,
344 pub(crate) usd: f64,
345 pub(crate) cache_usd: f64,
346}
347
348pub(crate) type GroupRow = (i64, i64, f64);
350
351#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
352pub(crate) struct ReportBucket {
353 pub(crate) entries: i64,
354 pub(crate) reductions: i64,
355 pub(crate) saved: i64,
356 pub(crate) baseline: i64,
357 pub(crate) actual: i64,
358 pub(crate) usd: f64,
359 pub(crate) would_usd: f64,
360 pub(crate) blended_usd: f64,
361 pub(crate) blended_saved: i64,
362 pub(crate) stand_in_usd: f64,
363 pub(crate) corrections: i64,
364 pub(crate) corrected: i64,
365 pub(crate) corrected_usd: f64,
366 pub(crate) cache_entries: i64,
367 pub(crate) edits: i64,
368 pub(crate) tools: BTreeMap<String, GroupRow>,
369 pub(crate) models: BTreeMap<String, GroupRow>,
370 pub(crate) mechanisms: BTreeMap<String, GroupRow>,
371}
372
373pub(crate) type LedgerDays = BTreeMap<String, BTreeMap<String, ReportBucket>>;
374
375#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
376pub(crate) struct Chain {
377 pub(crate) lines: i64,
378 pub(crate) broken: Option<i64>,
379 pub(crate) spans: BTreeMap<String, (i64, i64, String, String)>,
380}
381
382#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
383pub(crate) struct GuardEvent {
384 pub(crate) at: String,
385 pub(crate) kind: String,
386 pub(crate) count: i64,
387 pub(crate) tool: String,
388}
389
390#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
391pub(crate) struct EgressRow {
392 pub(crate) mtime: i64,
393 pub(crate) provider: String,
394 pub(crate) model: String,
395 pub(crate) locality: String,
396 pub(crate) outcome: String,
397 pub(crate) original: i64,
398 pub(crate) delivered: i64,
399 pub(crate) redactions: i64,
400 pub(crate) blocked: i64,
401 pub(crate) injection: i64,
402 pub(crate) incomplete: i64,
403 pub(crate) mode: String,
404}
405
406#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
407pub(crate) struct AlertSettings {
408 pub(crate) enabled: bool,
409 pub(crate) kinds: Vec<String>,
410 pub(crate) desktop: bool,
411 pub(crate) webhook: String,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
415pub(crate) struct AlertRecord {
416 pub(crate) at: String,
417 pub(crate) text: String,
418 pub(crate) channels: Vec<String>,
419 pub(crate) test: bool,
420}
421
422#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
423pub(crate) struct Profile {
424 pub(crate) name: String,
425 #[serde(default)]
426 pub(crate) values: Map<String, Value>,
427 #[serde(default)]
428 pub(crate) saved_at: String,
429}
430
431#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
432pub(crate) struct ProxyDays {
433 pub(crate) first: String,
434 pub(crate) days: Vec<ProxyDay>,
435 pub(crate) kept: ProxyKept,
436}
437
438#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
439pub(crate) struct ProxyDay {
440 pub(crate) day: String,
441 pub(crate) models: BTreeMap<String, ProxyModelDay>,
442}
443
444#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
445pub(crate) struct ProxyModelDay {
446 pub(crate) prompts: i64,
447 pub(crate) cached_tokens: i64,
448 pub(crate) discount_usd: f64,
449}
450
451#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
452pub(crate) struct ProxyKept {
453 pub(crate) saved: i64,
454 pub(crate) corrected: i64,
455 pub(crate) usd: f64,
456 pub(crate) reductions: i64,
457 pub(crate) corrections: i64,
458}
459
460#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
461pub(crate) struct HostUsageFile {
462 pub(crate) project: String,
463 pub(crate) host: String,
464 pub(crate) days: BTreeMap<String, BTreeMap<String, HostCounts>>,
465}
466
467#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
468pub struct HostCounts {
469 pub requests: i64,
470 pub input: i64,
471 pub cache_read: i64,
472 pub cache_write: i64,
473 pub output: i64,
474}
475
476#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
477pub(crate) struct SnapshotMeta {
478 pub(crate) id: String,
479 pub(crate) created_at: Option<Value>,
480 pub(crate) facts: Option<Value>,
481 pub(crate) reason: Option<Value>,
482}
483
484#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
485pub(crate) struct AdaptiveState {
486 #[serde(default)]
487 pub(crate) applied: Vec<AppliedItem>,
488 #[serde(default)]
489 pub(crate) dismissed: Vec<String>,
490}
491
492#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
493pub(crate) struct AppliedItem {
494 pub(crate) path: String,
495 pub(crate) action: String,
496 #[serde(default)]
497 pub(crate) applied_at: String,
498 #[serde(default)]
499 pub(crate) baseline: Baseline,
500}
501
502#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
503pub(crate) struct Baseline {
504 #[serde(default)]
505 pub(crate) reads: i64,
506 #[serde(default)]
507 pub(crate) original: i64,
508 #[serde(default)]
509 pub(crate) saved: i64,
510 #[serde(default)]
511 pub(crate) ratio: Option<f64>,
512}
513
514#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
515pub(crate) struct HeatmapEntry {
516 #[serde(default, deserialize_with = "lenient::opt_i64")]
517 pub(crate) access_count: Option<i64>,
518 #[serde(default, deserialize_with = "lenient::opt_i64")]
519 pub(crate) total_original_tokens: Option<i64>,
520 #[serde(default, deserialize_with = "lenient::opt_i64")]
521 pub(crate) total_tokens_saved: Option<i64>,
522 #[serde(default, deserialize_with = "lenient::opt_str")]
523 pub(crate) last_access: Option<String>,
524}