1 import Base 2 import ../json.bend as Json 3 import ../store.bend as Store 4 import ../store/base.bend as SBase 5 import ../store/seq.bend as SSeq 6 import ../usage.bend as Usage 7 import ../usage/cred.bend as UCred 8 import ../policy/types.bend as T 9 import ../policy/usage.bend as PU 10 import ../policy/rank.bend as PR 11 import ./rows.bend as Rows 12 import ./events.bend as Ev 13 import ./config.bend as Cfg 14 import ./gather.bend as G 15 import ./state.bend as St 16 17 # Pure bridges between the daemon's data (config, state file, cache rows) and 18 # the policy's types, and the other way to events. 19 20 def nat(+n: U32) -> Nat: 21 U32.to_nat(n) 22 23 def strategy2(+cf: Bool) -> T.Strategy: 24 match cf: 25 case True{}: 26 T.ConsumeFirst{} 27 case False{}: 28 T.Best{} 29 30 def strategy_of(+s: String) -> T.Strategy: 31 strategy2(String.eq(s, "consume-first")) 32 33 # serve TTL 180 s; the rest is the settings. 34 def pol_cfg(+c: Cfg.Cfg) -> T.Config: 35 match c: 36 case Cfg.Cfg{th, iv, cd, hy, st, ia, ut, ms}: 37 T.Config{nat(th), ms, strategy_of(st), nat(iv), nat(cd), nat(hy), 180n, nat(ut), ia} 38 39 # ---- autoswitch_state.json -> State ------------------------------------------------------------ 40 41 def sel_mt(+c: Bool, +x: Maybe<&2, T.Trigger>, +y: Maybe<&2, T.Trigger>) -> Maybe<&2, T.Trigger>: 42 match c: 43 case True{}: 44 x 45 case False{}: 46 y 47 48 def trigger_of(+s: String) -> Maybe<&2, T.Trigger>: 49 sel_mt(String.eq(s, "proactive"), Some{T.Proactive{}}, 50 sel_mt(String.eq(s, "at-limit"), Some{T.AtLimit{}}, 51 sel_mt(String.eq(s, "failover"), Some{T.Failover{}}, 52 sel_mt(String.eq(s, "consume-first"), Some{T.ConsumeFirstT{}}, None{})))) 53 54 def num_nat(+m: Maybe<&2, Json.Json>) -> Maybe<&2, Nat>: 55 Rows.nat_opt(Rows.bind_s(Rows.num_text(m), t => Rows.epoch_text(t))) 56 57 def centi_nat(+m: Maybe<&2, Json.Json>) -> Maybe<&2, Nat>: 58 Rows.nat_opt(Rows.bind_s(Rows.num_text(m), t => Cfg.centi(t))) 59 60 def slot_nat(+m: Maybe<&2, Json.Json>) -> Maybe<&2, Nat>: 61 Rows.nat_opt(Rows.bind_s(Rows.str_text(m), t => Cfg.read_u32(t))) 62 63 def snap2(+tr: Maybe<&2, Json.Json>, +st: Json.Json) -> T.Snap: 64 match tr: 65 case Some{Json.JStr{s}}: 66 T.Snap{centi_nat(Json.get("leftHeadroom", st)), num_nat(Json.get("leftRecoveryAt", st)), trigger_of(s)} 67 case _: 68 T.NoSnap{} 69 70 def snap_of(+st: Json.Json) -> T.Snap: 71 snap2(Json.get("leftTrigger", st), st) 72 73 def state_of(+st: Json.Json) -> T.State: 74 T.State{Rows.nat_opt(St.last_switch_at(st)), slot_nat(Json.get("lastSwitchTo", st)), num_nat(Json.get("lastSwitchFrom", st)), snap_of(st)} 75 76 # ---- accounts and usages ------------------------------------------------------------------------ 77 78 def accounts_of(+es: List<&2, G.Ent>) -> List<&2, T.Account>: 79 match es: 80 case Nil{}: 81 [] 82 case Con{G.Ent{s, e, o, c, a}, t}: 83 a <> accounts_of(t) 84 85 def oauth_of(+creds: String) -> Json.Json: 86 SSeq.get_or("claudeAiOauth", Json.unwrap(Json.parse(creds)), Json.JNull{}) 87 88 def sel_sn(+c: Bool, +x: T.Sentinel, +y: T.Sentinel) -> T.Sentinel: 89 match c: 90 case True{}: 91 x 92 case False{}: 93 y 94 95 def sent2(+is_active: Bool, +has: Bool, +api: Bool, +creds: String, +now: U32) -> T.Sentinel: 96 match has api: 97 case False{} _: 98 T.OtherSentinel{} 99 case True{} True{}: 100 T.OtherSentinel{} 101 case True{} False{}: 102 sel_sn(is_active && UCred.expired(oauth_of(creds), now), T.TokenExpired{}, T.NoSentinel{}) 103 104 # The overlay that hides a row: an expired token on the active account (idle 105 # Claude Code), or an account with no usable credential or no quota to read. 106 def sentinel_of(+is_active: Bool, +e: G.Ent, +now: U32) -> T.Sentinel: 107 match e: 108 case G.Ent{slot, email, org, +creds, acct}: 109 sent2(is_active, SBase.nonempty(creds), SBase.looks_like_api_key(creds), creds, now) 110 111 def row_guard(+row: Json.Json, +email: String, +org: String) -> Json.Json: 112 Store.sel.j(String.eq(SSeq.field_str(row, "email"), email) && String.eq(SSeq.field_str(row, "organizationUuid"), org), row, Json.JNull{}) 113 114 def row_of(+rows: Json.Json, +e: G.Ent) -> Json.Json: 115 match e: 116 case G.Ent{slot, email, org, creds, acct}: 117 row_guard(SSeq.get_or(slot, rows, Json.JNull{}), email, org) 118 119 def is_slot(+active: Maybe<&2, String>, +slot: String) -> Bool: 120 match active: 121 case Some{a}: 122 String.eq(a, slot) 123 case None{}: 124 False{} 125 126 def usage_one(+rows: Json.Json, +e: G.Ent, +active: Maybe<&2, String>, +now: U32) -> T.Usage: 127 match e: 128 case G.Ent{+slot, +email, +org, +creds, +acct}: 129 Rows.usage_of(nat(SSeq.str_u32(slot)), row_of(rows, e), sentinel_of(is_slot(active, slot), e, now)) 130 131 def usages_of(+rows: Json.Json, +es: List<&2, G.Ent>, +active: Maybe<&2, String>, +now: U32) -> List<&2, T.Usage>: 132 match es: 133 case Nil{}: 134 [] 135 case Con{e, t}: 136 usage_one(rows, e, active, now) <> usages_of(rows, t, active, now) 137 138 def sel_mu(+c: Bool, +x: Maybe<&2, T.Usage>, +y: Maybe<&2, T.Usage>) -> Maybe<&2, T.Usage>: 139 match c: 140 case True{}: 141 x 142 case False{}: 143 y 144 145 def usage_pick(+u: T.Usage, +slot: Nat, +rest: Maybe<&2, T.Usage>) -> Maybe<&2, T.Usage>: 146 match u: 147 case T.Usage{+s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}: 148 sel_mu(Nat.is_eq(s, slot), Some{u}, rest) 149 150 def usage_for(+us: List<&2, T.Usage>, +slot: Nat) -> Maybe<&2, T.Usage>: 151 match us: 152 case Nil{}: 153 None{} 154 case Con{u, t}: 155 usage_pick(u, slot, usage_for(t, slot)) 156 157 def sel_me(+c: Bool, +x: Maybe<&2, G.Ent>, +y: Maybe<&2, G.Ent>) -> Maybe<&2, G.Ent>: 158 match c: 159 case True{}: 160 x 161 case False{}: 162 y 163 164 def ent_pick(+e: G.Ent, +slot: String, +rest: Maybe<&2, G.Ent>) -> Maybe<&2, G.Ent>: 165 match e: 166 case G.Ent{+s, em, o, c, a}: 167 sel_me(String.eq(s, slot), Some{e}, rest) 168 169 def ent_for(+es: List<&2, G.Ent>, +slot: String) -> Maybe<&2, G.Ent>: 170 match es: 171 case Nil{}: 172 None{} 173 case Con{e, t}: 174 ent_pick(e, slot, ent_for(t, slot)) 175 176 def email2(+m: Maybe<&2, G.Ent>) -> String: 177 match m: 178 case Some{G.Ent{s, em, o, c, a}}: 179 em 180 case None{}: 181 "" 182 183 # ---- the poll event ---------------------------------------------------------------------------- 184 185 def email_of(+es: List<&2, G.Ent>, +slot: String) -> String: 186 email2(ent_for(es, slot)) 187 188 def scoped_keep(+hit: Bool, +w: Ev.Win, +rest: List<&2, Ev.Win>) -> List<&2, Ev.Win>: 189 match hit: 190 case True{}: 191 w <> rest 192 case False{}: 193 rest 194 195 def scoped_wins(+ss: List<&2, T.Scoped>, +ms: List<&2, String>, +now: Nat) -> List<&2, Ev.Win>: 196 match ss: 197 case Nil{}: 198 [] 199 case Con{T.Scoped{n, p, r}, t}: 200 scoped_keep(PU.model.wanted(String.to_lower(n), ms), Ev.Win{n, U32.from_nat(p)}, scoped_wins(t, ms, now)) 201 202 def opt_win(+name: String, +m: Maybe<&2, Nat>, +rest: List<&2, Ev.Win>) -> List<&2, Ev.Win>: 203 match m: 204 case Some{p}: 205 Ev.Win{name, U32.from_nat(p)} <> rest 206 case None{}: 207 rest 208 209 def win_rows(+u: T.Usage, +ms: List<&2, String>, +now: Nat) -> List<&2, Ev.Win>: 210 match u: 211 case T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}: 212 opt_win("5h", five, opt_win("7d", seven, scoped_wins(ss, ms, now))) 213 214 def head_u32(+m: Maybe<&2, Nat>) -> Maybe<&2, U32>: 215 match m: 216 case Some{n}: 217 Some{U32.from_nat(n)} 218 case None{}: 219 None{} 220 221 def err_text(+row: Json.Json) -> Maybe<&2, String>: 222 Rows.str_text(Json.get("lastError", row)) 223 224 def sel_wl(+c: Bool, +x: List<&2, Ev.Win>, +y: List<&2, Ev.Win>) -> List<&2, Ev.Win>: 225 match c: 226 case True{}: 227 x 228 case False{}: 229 y 230 231 def poll_row2(+slot: String, +trusted: Bool, +u: T.Usage, +err: Maybe<&2, String>, +ms: List<&2, String>, +now: Nat) -> Ev.Row: 232 Ev.Row{slot, head_u32(PU.headroom(u, now, ms)), sel_wl(trusted, win_rows(u, ms, now), []), err} 233 234 # One poll row: windows only for a trusted reading; the error only when the 235 # usage is unknown. 236 def poll_row(+e: G.Ent, +u: T.Usage, +rows: Json.Json, +ms: List<&2, String>, +now: Nat) -> Ev.Row: 237 match e: 238 case G.Ent{+slot, email, org, creds, acct}: 239 poll_row2(slot, PU.trusted(u, now, ms), u, err_text(row_of(rows, e)), ms, now) 240 241 def row_opt(+e: G.Ent, +u: Maybe<&2, T.Usage>, +rows: Json.Json, +ms: List<&2, String>, +now: Nat) -> Maybe<&2, Ev.Row>: 242 match u: 243 case Some{x}: 244 Some{poll_row(e, x, rows, ms, now)} 245 case None{}: 246 None{} 247 248 def push_row(+m: Maybe<&2, Ev.Row>, +rest: List<&2, Ev.Row>) -> List<&2, Ev.Row>: 249 match m: 250 case Some{r}: 251 r <> rest 252 case None{}: 253 rest 254 255 def ent_slot(+e: G.Ent) -> String: 256 match e: 257 case G.Ent{s, em, o, c, a}: 258 s 259 260 def poll_rows(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +rows: Json.Json, +ms: List<&2, String>, +now: Nat) -> List<&2, Ev.Row>: 261 match es: 262 case Nil{}: 263 [] 264 case Con{e, t}: 265 push_row(row_opt(e, usage_for(us, nat(SSeq.str_u32(ent_slot(e)))), rows, ms, now), poll_rows(t, us, rows, ms, now)) 266 267 def slot_nat_of(+s: String) -> Nat: 268 nat(SSeq.str_u32(s)) 269 270 # {"number": N, "email": ...} for a slot of the roster. 271 def ref_json(+slot: String, +es: List<&2, G.Ent>) -> Json.Json: 272 Ev.ref(slot, email_of(es, slot)) 273 274 def opt_json(+m: Maybe<&2, String>) -> Json.Json: 275 match m: 276 case Some{x}: 277 Json.JStr{x} 278 case None{}: 279 Json.JNull{} 280 281 def snap_accounts(+s: G.Snap) -> List<&2, T.Account>: 282 accounts_of(G.snap_ents(s)) 283 284 def snap_state(+s: G.Snap) -> T.State: 285 state_of(G.snap_state(s))