1//! `/usage`: what the parents' screen shows about thinking and the natural voice. 2 3use ::log::{debug, error, trace, warn}; 4use serde::Serialize; 5use whiskers_ports::{Allowance, Clock, HasAllowance, HasClock, HasHousehold, HasVoice, Hub, ThinkingUsage, Voice}; 6 7use super::{Route, sealed}; 8use crate::Response; 9 10/// `POST /usage`: both allowances, for the parents' screen. Needs the household's choices, the allowance, the 11/// voice (whose credits it reports) and the clock. 12pub struct Usage; 13 14impl sealed::Sealed for Usage {} 15 16impl<C: HasHousehold + HasAllowance + HasVoice + HasClock> Route<C> for Usage { 17 const PATHS: &'static [&'static str] = &["/usage"]; 18 async fn answer(&self, adapter: &C, _: &str, _: &str) -> Response { 19 usage(adapter).await 20 } 21} 22 23/// The thinking half of the reply. 24#[derive(Serialize, Debug, PartialEq, Eq)] 25pub(crate) struct TokenUsage { 26 used: u64, 27 limit: Option<u64>, 28 window_hours: u64, 29 /// When the oldest spending leaves the window, so room starts to return; `None` if nothing is spent. 30 frees_up_at_ms: Option<u64>, 31} 32 33impl From<ThinkingUsage> for TokenUsage { 34 fn from(u: ThinkingUsage) -> Self { 35 Self { used: u.used.get(), limit: u.limit.map(|l| l.get()), window_hours: u.window_hours, frees_up_at_ms: u.frees_up_at.map(|m| m.get()) } 36 } 37} 38 39/// The voice half of the reply. 40#[derive(Serialize, Debug, Default, PartialEq, Eq)] 41pub(crate) struct VoiceUsage { 42 configured: bool, 43 /// The service's own daily allowance, in characters. 44 daily_cap: u32, 45 daily_spent: u32, 46 last_error: Option<String>, 47 /// The voice service's monthly credits; `None` when they could not be read (see `plan_error`). 48 tier: Option<String>, 49 used: Option<u64>, 50 limit: Option<u64>, 51 resets_at_unix: Option<u64>, 52 plan_error: Option<String>, 53} 54 55async fn usage<C: HasHousehold + HasAllowance + HasVoice + HasClock>(adapter: &C) -> Response { 56 let config = match adapter.household().current().await { 57 Ok(h) => h.config(), 58 Err(e) => { 59 error!("/usage: the household's choices cannot be read: {e}"); 60 return Response::empty(500); 61 } 62 }; 63 let now = adapter.clock().now(); 64 let tokens: TokenUsage = match adapter.allowance().thinking_usage(now, &config.tokens).await { 65 Ok(u) => u.into(), 66 Err(e) => { 67 error!("/usage: the thinking allowance cannot be read: {e}"); 68 return Response::empty(500); 69 } 70 }; 71 debug!("/usage: tokens used {} of {:?}", tokens.used, tokens.limit); 72 let voice = match voice_usage(adapter, config.voice_daily_chars).await { 73 Ok(v) => v, 74 Err(e) => { 75 error!("/usage: the voice allowance cannot be read: {e}"); 76 return Response::empty(500); 77 } 78 }; 79 Response::of(&serde_json::json!({ "voice": voice, "tokens": tokens })) 80} 81 82/// The usage the parents see: the service's own counters now, and the voice service's monthly 83/// credits (only if it is set up, since asking is a call out). 84async fn voice_usage<C: HasAllowance + HasVoice + HasClock>(adapter: &C, daily_cap: u32) -> Result<VoiceUsage, whiskers_ports::StoreError> { 85 let spend = adapter.allowance().voice_spend(adapter.clock().now()).await?; 86 let configured = adapter.voice().configured().await; 87 let mut u = VoiceUsage { 88 configured, 89 daily_cap, 90 daily_spent: spend.spent_today, 91 last_error: spend.last_failure.map(|f| f.to_string()), 92 ..VoiceUsage::default() 93 }; 94 if configured { 95 match adapter.voice().credits().await { 96 Ok(c) => { 97 u.tier = c.tier; 98 u.used = c.used; 99 u.limit = c.limit; 100 u.resets_at_unix = c.resets_at_unix; 101 } 102 Err(e) => { 103 warn!("voice usage: the voice service's credits could not be read: {e}"); 104 u.plan_error = Some(e.to_string()); 105 } 106 } 107 } 108 trace!("voice usage: {} of {} chars spent today", u.daily_spent, u.daily_cap); 109 Ok(u) 110}