1 import Base 2 import ./json.bend as Json 3 import ./clock.bend as Clock 4 import ./policy/usage.bend as PUsage 5 import ./policy/types.bend as PT 6 7 # Usage windows, poll planning, failure backoff and decision trust: the pure 8 # rules of usage_store.py and poll_policy.py (claude-swap 0.27.2). 9 # Epochs are unix seconds (U32). Percentages are tenths of a percent (U32), 10 # so `42.5` is 425 and `100.0` is 1000. There is no jitter on the poll 11 # deadline (Bend has no random source); the reference's is +-10%. 12 13 def serve_ttl() -> U32: 14 180 15 16 def decision_trust() -> U32: 17 300 18 19 def trust_ceiling() -> U32: 20 3600 21 22 def rate_limit_trust_ceiling() -> U32: 23 7200 24 25 def claim_ttl() -> U32: 26 90 27 28 def min_interval() -> U32: 29 180 30 31 def urgent_interval() -> U32: 32 60 33 34 def active_ceiling() -> U32: 35 300 36 37 def candidate_default() -> U32: 38 300 39 40 def candidate_ceiling() -> U32: 41 600 42 43 def exhausted_interval() -> U32: 44 600 45 46 def reset_slack() -> U32: 47 60 48 49 def recent_429_window() -> U32: 50 3600 51 52 def post_429_min() -> U32: 53 360 54 55 def post_429_cap() -> U32: 56 1800 57 58 def edge_backoff() -> U32: 59 300 60 61 def backoff_base() -> U32: 62 30 63 64 def backoff_cap() -> U32: 65 600 66 67 def retry_margin() -> U32: 68 900 69 70 def retry_floor_cap() -> U32: 71 4500 72 73 def escalation_margin() -> U32: 74 150 75 76 def min_u(+a: U32, +b: U32) -> U32: 77 Clock.sel(U32.is_lt(a, b), a, b) 78 79 def max_u(+a: U32, +b: U32) -> U32: 80 Clock.sel(U32.is_gt(a, b), a, b) 81 82 # a - b, floored at 0. 83 def sub0(+a: U32, +b: U32) -> U32: 84 Clock.sel(U32.is_gt(a, b), (a - b : U32), 0) 85 86 def absdiff(+a: U32, +b: U32) -> U32: 87 Clock.sel(U32.is_gt(a, b), (a - b : U32), (b - a : U32)) 88 89 def tenths.mk(+i: String, +f: String) -> Maybe<&2, U32>: 90 do Maybe<&2, U32>: 91 ip : U32 <- U32.read(i) 92 f1 : U32 <- Clock.num(f ++ "00", 0n, 1n) 93 f2 : U32 <- Clock.num(f ++ "00", 1n, 1n) 94 return (ip * 10 + f1 + Clock.sel(U32.is_ge(f2, 5), 1, 0) : U32) 95 96 def tenths.pick(ok: Bool, +i: String, +f: String) -> Maybe<&2, U32>: 97 match ok: 98 case True{}: 99 tenths.mk(i, f) 100 case False{}: 101 None{} 102 103 def tenths.parts(ps: List<&2, String>) -> Maybe<&2, U32>: 104 match ps: 105 case Con{+i, Nil{}}: 106 tenths.pick(Clock.all_digits(i) && Bool.not(String.is_empty(i)) && Nat.is_le(String.length(i), 7n), i, "") 107 case Con{+i, Con{+f, Nil{}}}: 108 tenths.pick(Clock.all_digits(i) && Clock.all_digits(f) && Bool.not(String.is_empty(i)) && Nat.is_le(String.length(i), 7n), i, f) 109 case _: 110 None{} 111 112 # `42.5` -> 425, rounded to a tenth; None for anything but a plain decimal. 113 def tenths(s: String) -> Maybe<&2, U32>: 114 tenths.parts(String.split(s, '.')) 115 116 def tenths.json(j: Json.Json) -> Maybe<&2, U32>: 117 match j: 118 case Json.JNum{t}: 119 tenths(t) 120 case _: 121 None{} 122 123 # One usage window: its percentage (tenths) and when it resets. 124 type Win is Data: 125 Win{pct: U32, reset: Maybe<&2, U32>} 126 127 def win.parse(m: Maybe<&2, String>) -> Maybe<&2, U32>: 128 match m: 129 case None{}: 130 None{} 131 case Some{s}: 132 Clock.parse_iso8601(s) 133 134 def win.reset(+w: Json.Json) -> Maybe<&2, U32>: 135 win.parse(Json.as_string(Maybe.default(&2, Json.Json, Json.get("resets_at", w), Json.JNull{}))) 136 def min_acc(acc: Maybe<&2, U32>, +p: U32) -> Maybe<&2, U32>: 137 match acc: 138 case None{}: 139 Some{p} 140 case Some{a}: 141 Some{min_u(a, p)} 142 143 def any.step(r: Maybe<&2, U32>, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 144 match r: 145 case Some{x}: 146 min_acc(acc, x) 147 case None{}: 148 acc 149 150 # The earliest reset of any relevant window (past or future): trust ends there. 151 def earliest_reset(ws: List<&2, Win>, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 152 match ws: 153 case Nil{}: 154 acc 155 case Con{Win{p, r}, t}: 156 earliest_reset(t, any.step(r, acc)) 157 158 # When to poll next and the interval it came from. 159 type Sched is Data: 160 Sched{next: U32, interval: U32} 161 162 def interval.moving(+base: U32) -> U32: 163 max_u(min_interval(), (base / 2 : U32)) 164 165 def interval.still(+base: U32, +ceil: U32) -> U32: 166 min_u(ceil, max_u(min_interval(), (base * 3 / 2 : U32))) 167 168 # Default for unknown utilization, halve on movement, back off x1.5 otherwise. 169 def interval.base(prev: Maybe<&2, U32>, +dflt: U32) -> U32: 170 match prev: 171 case Some{+b}: 172 Clock.sel(U32.is_eq(b, 0), dflt, b) 173 case None{}: 174 dflt 175 176 def interval.pick(known: Bool, moving: Bool, +dflt: U32, +base: U32, +ceil: U32) -> U32: 177 match known moving: 178 case False{} _: 179 dflt 180 case True{} True{}: 181 interval.moving(base) 182 case True{} False{}: 183 interval.still(base, ceil) 184 185 def is_moving(prev: Maybe<&2, U32>, new: Maybe<&2, U32>) -> Bool: 186 match prev new: 187 case Some{a} Some{b}: 188 U32.is_ge(absdiff(a, b), 10) 189 case _ _: 190 False{} 191 192 def is_known(prev: Maybe<&2, U32>, new: Maybe<&2, U32>) -> Bool: 193 Maybe.is_some(&2, U32, prev) && Maybe.is_some(&2, U32, new) 194 195 def new_ge(new: Maybe<&2, U32>, +floor: U32) -> Bool: 196 match new: 197 case Some{n}: 198 U32.is_ge(n, floor) 199 case None{}: 200 False{} 201 202 def exhausted(new: Maybe<&2, U32>) -> Bool: 203 match new: 204 case Some{n}: 205 U32.is_ge(n, 1000) 206 case None{}: 207 False{} 208 209 def plan.r429(recent: Bool, +base: U32, +i: U32) -> U32: 210 Clock.sel(recent, min_u(post_429_cap(), max_u(i, max_u((base * 3 / 2 : U32), post_429_min()))), i) 211 212 def plan.exh(ex: Bool, +i: U32) -> U32: 213 Clock.sel(ex, max_u(i, exhausted_interval()), i) 214 215 def plan.interval(+i0: U32, +urgent: Bool, +recent: Bool, +base: U32, +ex: Bool) -> U32: 216 plan.exh(ex, plan.r429(recent, base, Clock.sel(urgent, urgent_interval(), i0))) 217 218 def plan.cap(+next: U32, r: Maybe<&2, U32>) -> U32: 219 match r: 220 case Some{x}: 221 min_u(next, (x + reset_slack() : U32)) 222 case None{}: 223 next 224 225 def keep.m(ok: Bool, m: Maybe<&2, U32>) -> Maybe<&2, U32>: 226 match ok: 227 case True{}: 228 m 229 case False{}: 230 None{} 231 232 def plan.reset.pick(ex: Bool, lim: Maybe<&2, U32>, fut: Maybe<&2, U32>, +now: U32) -> Maybe<&2, U32>: 233 match ex lim: 234 case True{} Some{+x}: 235 keep.m(U32.is_gt(x, now), Some{x}) 236 case True{} None{}: 237 None{} 238 case False{} _: 239 fut 240 241 def plan.fin(+now: U32, +interval: U32, r: Maybe<&2, U32>) -> Sched: 242 Sched{plan.cap((now + interval : U32), r), interval} 243 244 def nat_maybe(m: Maybe<&2, U32>) -> Maybe<&2, Nat>: 245 match m: 246 case Some{x}: 247 Some{U32.to_nat(x)} 248 case None{}: 249 None{} 250 251 def reset_nat(+w: Json.Json) -> Maybe<&2, Nat>: 252 nat_maybe(win.reset(w)) 253 254 def centi(m: Maybe<&2, U32>) -> Maybe<&2, Nat>: 255 match m: 256 case Some{t}: 257 Some{U32.to_nat((t * 10 : U32))} 258 case None{}: 259 None{} 260 261 def pct_centi(+w: Json.Json) -> Maybe<&2, Nat>: 262 centi(tenths.json(Maybe.default(&2, Json.Json, Json.get("pct", w), Json.JNull{}))) 263 264 def scoped.mk(n: Maybe<&2, String>, p: Maybe<&2, Nat>, r: Maybe<&2, Nat>) -> Maybe<&2, PT.Scoped>: 265 match n p: 266 case Some{name} Some{x}: 267 Some{PT.Scoped{name, x, r}} 268 case _ _: 269 None{} 270 271 def scoped.item(+w: Json.Json) -> Maybe<&2, PT.Scoped>: 272 scoped.mk(Json.as_string(Maybe.default(&2, Json.Json, Json.get("name", w), Json.JNull{})), pct_centi(w), reset_nat(w)) 273 274 def scoped.cons(m: Maybe<&2, PT.Scoped>, rest: List<&2, PT.Scoped>) -> List<&2, PT.Scoped>: 275 match m: 276 case Some{x}: 277 x <> rest 278 case None{}: 279 rest 280 281 def scoped.all(ws: List<&2, Json.Json>) -> List<&2, PT.Scoped>: 282 match ws: 283 case Nil{}: 284 [] 285 case Con{+w, t}: 286 scoped.cons(scoped.item(w), scoped.all(t)) 287 288 def window_of(+u: Json.Json, +k: String) -> Json.Json: 289 Maybe.default(&2, Json.Json, Json.get(k, u), Json.JNull{}) 290 291 # The policy core's view of a `usage` object (centi-percent windows). 292 def policy_usage(+u: Json.Json) -> PT.Usage: 293 PT.Usage{0n, pct_centi(window_of(u, "five_hour")), reset_nat(window_of(u, "five_hour")), pct_centi(window_of(u, "seven_day")), reset_nat(window_of(u, "seven_day")), scoped.all(Json.to_list(window_of(u, "scoped"))), None{}, 0n, PT.ErrNone{}, 0n, None{}, 0n, 0n, 0n, 0n, PT.NoSentinel{}} 294 295 def maybe_u32(m: Maybe<&2, Nat>) -> Maybe<&2, U32>: 296 match m: 297 case Some{x}: 298 Some{U32.from_nat(x)} 299 case None{}: 300 None{} 301 302 def win.back(ws: List<&2, PT.Win>) -> List<&2, Win>: 303 match ws: 304 case Nil{}: 305 [] 306 case Con{PT.Win{p, r}, t}: 307 Win{U32.from_nat(Nat.div(p, 10n)), maybe_u32(r)} <> win.back(t) 308 309 # Every window that gates an account (5h, 7d, and the scoped windows of the 310 # named models): policy's `windows`, back as tenths for the planner. 311 def relevant(+usage: Json.Json, +models: List<&2, String>) -> List<&2, Win>: 312 win.back(PUsage.windows(policy_usage(usage), models)) 313 314 def future.pick(ok: Bool, +x: U32, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 315 match ok: 316 case True{}: 317 min_acc(acc, x) 318 case False{}: 319 acc 320 321 def future.step(r: Maybe<&2, U32>, +now: U32, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 322 match r: 323 case Some{+x}: 324 future.pick(U32.is_gt(x, now), x, acc) 325 case None{}: 326 acc 327 328 def earliest_future(ws: List<&2, Win>, +now: U32, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 329 match ws: 330 case Nil{}: 331 acc 332 case Con{Win{p, r}, t}: 333 earliest_future(t, now, future.step(r, now, acc)) 334 335 def max_acc(acc: Maybe<&2, U32>, +p: U32) -> Maybe<&2, U32>: 336 match acc: 337 case None{}: 338 Some{p} 339 case Some{a}: 340 Some{max_u(a, p)} 341 342 def limiting.step(full: Bool, r: Maybe<&2, U32>, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 343 match full r: 344 case True{} Some{x}: 345 max_acc(acc, x) 346 case _ _: 347 acc 348 349 def limiting_reset(ws: List<&2, Win>, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 350 match ws: 351 case Nil{}: 352 acc 353 case Con{Win{p, r}, t}: 354 limiting_reset(t, limiting.step(U32.is_ge(p, 1000), r, acc)) 355 356 def plan.go(+usage: Json.Json, +models: List<&2, String>, +now: U32, +interval: U32, +ex: Bool) -> Sched: 357 plan.fin(now, interval, plan.reset.pick(ex, limiting_reset(relevant(usage, models), None{}), earliest_future(relevant(usage, models), now, None{}), now)) 358 359 def plan.with(+base: U32, +known: Bool, +moving: Bool, +ex: Bool, +in_band: Bool, +usage: Json.Json, +active: Bool, +models: List<&2, String>, +recent: Bool, +now: U32, +ceil: U32, +dflt: U32) -> Sched: 360 plan.go(usage, models, now, 361 plan.interval(interval.pick(known, moving, dflt, base, ceil), active && moving && Bool.not(recent) && in_band, recent, base, ex), 362 ex) 363 364 def dflt_of(active: Bool) -> U32: 365 Clock.sel(active, min_interval(), candidate_default()) 366 367 def ceil_of(active: Bool) -> U32: 368 Clock.sel(active, active_ceiling(), candidate_ceiling()) 369 370 def plan.start(prev_interval: Maybe<&2, U32>, +prev: Maybe<&2, U32>, +new: Maybe<&2, U32>, +usage: Json.Json, +active: Bool, +threshold: U32, +models: List<&2, String>, recent: Bool, +now: U32) -> Sched: 371 plan.with(interval.base(prev_interval, dflt_of(active)), is_known(prev, new), is_moving(prev, new), exhausted(new), new_ge(new, sub0(threshold, escalation_margin())), 372 usage, active, models, recent, now, ceil_of(active), dflt_of(active)) 373 374 def max_pct(ws: List<&2, Win>, acc: Maybe<&2, U32>) -> Maybe<&2, U32>: 375 match ws: 376 case Nil{}: 377 acc 378 case Con{Win{p, r}, t}: 379 max_pct(t, max_acc(acc, p)) 380 381 def binding(usage: Json.Json, models: List<&2, String>) -> Maybe<&2, U32>: 382 max_pct(relevant(usage, models), None{}) 383 384 # plan_after_fetch (no jitter): `threshold` in tenths, `models` the scoped 385 # names that gate the account, times in unix seconds. 386 def plan_after_fetch(prev_interval: Maybe<&2, U32>, prev_usage: Json.Json, +new_usage: Json.Json, is_active: Bool, +threshold: U32, +models: List<&2, String>, recent_429: Bool, +now: U32) -> Sched: 387 plan.start(prev_interval, binding(prev_usage, models), binding(new_usage, models), new_usage, is_active, threshold, models, recent_429, now)