/v1/messages: the model, through the operator's gateway, narrowed to what Whiskers sends. It
exists so the gateway is never put on the private network: the gateway may be the operator's
subscription with no login of its own, and this is its only guard. Do not widen what it lets through.
POST /v1/messages. Needs the model, the household's choices (the thinking window), the allowance and the clock.
14pub struct Messages;
16impl sealed::Sealed for Messages {} 17 18impl<C: HasThinker + HasHousehold + HasAllowance + HasClock> Route<C> for Messages { 19 const PATHS: &'static [&'static str] = &["/v1/messages"]; 20 async fn answer(&self, adapter: &C, _: &str, body: &str) -> Response { 21 messages(adapter, body).await 22 } 23} 24 25async fn messages<C: HasThinker + HasHousehold + HasAllowance + HasClock>(adapter: &C, body: &str) -> Response { 26 let request = match ThinkRequest::vet(body, adapter.thinker().serves()) { 27 Ok(r) => r, 28 Err(e) => { 29 warn!("/v1/messages refused: {e}"); 30 return Response::text(400, e.to_string()); 31 } 32 }; 33 let limit = match adapter.household().current().await { 34 Ok(h) => h.config().tokens, 35 Err(e) => { 36 error!("/v1/messages: the household's choices cannot be read: {e}"); 37 return Response::empty(500); 38 } 39 }; 40 match adapter.allowance().thinking_room(adapter.clock().now(), &limit).await { 41 Ok(ThinkingRoom::Room) => {} 42 Ok(ThinkingRoom::Spent { frees_up_at }) => { 43 info!("/v1/messages refused: the thinking allowance is spent (room returns at {} ms)", frees_up_at.get()); 44 // The tablet looks for this word. 45 return Response::text(429, "{\"error\":\"budget\"}".into()); 46 } 47 Err(e) => { 48 error!("/v1/messages: the thinking allowance cannot be read: {e}"); 49 return Response::empty(500); 50 } 51 } 52 match adapter.thinker().think(&request).await { 53 Ok(reply) => { 54 debug!("/v1/messages: the gateway answered {}, {} bytes", reply.status, reply.body.len()); 55 if reply.is_answer() { 56 let cost = tokens_in(&reply.body); 57 debug!("/v1/messages: the answer cost {} tokens", cost.get()); 58 if let Err(e) = adapter.allowance().charge_thinking(adapter.clock().now(), cost).await { 59 // The answer exists; losing the record of its cost risks a little overspending. 60 warn!("/v1/messages: the cost of an answer could not be kept: {e}"); 61 } 62 } else { 63 warn!("/v1/messages: the gateway answered {} ({} byte body)", reply.status, reply.body.len()); 64 } 65 Response::bytes(reply.status, Content::Json, reply.body.into_bytes()) 66 } 67 Err(e) => { 68 match &e { 69 ThinkError::NotConfigured(_) => warn!("/v1/messages refused: {e}"), 70 ThinkError::Unreachable(_) | ThinkError::Unreadable(_) => error!("/v1/messages refused: {e}"), 71 } 72 Response::text(502, e.to_string()) 73 } 74 } 75}