char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
aa0b468
main
1import type { AuthResult } from "@earendil-works/pi-ai" ; 2import * as j from "@char/justin" ; 3import type { Provider , SubscriptionUsage , UsageWindow } from "./types.ts" ; 4 5const object = ( value :unknown ) :Record < string , unknown > => 6value !== null && typeof value === "object" && ! Array . isArray ( value ) 7 ?value as Record < string , unknown > 8 :{}; 9const finite = ( value :unknown ) :value isnumber => 10typeof value === "number" && Number . isFinite ( value ); 11 12const goWindowOf = j . validation . compile ( j . union ( 13j . obj ({ percent :j . number , resetsAt :j . optional ( j . union ( j . string , j . null )) }), 14j . obj ({ usagePercent :j . number , resetInSec :j . optional ( j . union ( j . number , j . null )) }), 15)); 16 17export function usageWindows ( provider :Provider , value :unknown , now = Date . now ()) :UsageWindow [] { 18const data = object ( value ); 19const windows :UsageWindow [] = []; 20const add = ( label :string , percent :unknown , reset :unknown ) => { 21if ( ! finite ( percent ) || percent < 0 ) return ; 22const date = new Date ( 23typeof reset === "string" 24 ?Date . parse ( reset ) 25 :finite ( reset ) && reset > 0 26 ?reset * 1000 27 :NaN , 28); 29windows . push ({ 30 label, 31usedPercent :percent , 32resetsAt :Number . isFinite ( date . getTime ()) ?date . toISOString () :null , 33}); 34}; 35if ( provider === "anthropic" ) { 36for ( 37const [ key , label ] of [ 38[ "five_hour" , "5 hours" ], 39[ "seven_day" , "Weekly" ], 40[ "seven_day_opus" , "Weekly · Opus" ], 41[ "seven_day_sonnet" , "Weekly · Sonnet" ], 42[ "seven_day_oauth_apps" , "Weekly · OAuth apps" ], 43] 44) { 45const window = object ( data [ key ]); 46add ( label , window . utilization , window . resets_at ); 47} 48if ( Array . isArray ( data . limits )) { 49for ( const raw of data . limits ) { 50const limit = object ( raw ); 51if ( limit . is_active === false ) continue ; 52const model = object ( object ( limit . scope ). model ). display_name ; 53if ( typeof model !== "string" ) continue ; 54const label = `Weekly · ${ model } ` ; 55if ( ! windows . some (( window ) => window . label === label )) { 56add ( label , limit . percent , limit . resets_at ); 57} 58} 59} 60} else if ( provider === "opencode-go" ) { 61const usage = object ( data . usage ); 62for ( 63const [ key , label ] of [[ "rolling" , "5 hours" ], [ "weekly" , "Weekly" ], [ "monthly" , "Monthly" ]] 64) { 65// older Go endpoints report relative reset times and use top-level *Usage fields. 66const window = goWindowOf ( usage [ key ] ?? data [ ` ${ key } Usage` ]). value ; 67if ( ! window ) continue ; 68if ( "percent" in window ) { 69add ( label , window . percent , window . resetsAt ); 70} else { 71const seconds = window . resetInSec ; 72add ( 73label , 74window . usagePercent , 75finite ( seconds ) && seconds >= 0 ?now / 1000 + seconds :null , 76); 77} 78} 79} else { 80const limits = [{ label :"" , rate_limit :data . rate_limit }]; 81if ( Array . isArray ( data . additional_rate_limits )) { 82for ( const raw of data . additional_rate_limits ) { 83const extra = object ( raw ); 84const label = extra . limit_name ?? extra . metered_feature ; 85if ( typeof label === "string" ) limits . push ({ label, rate_limit :extra . rate_limit }); 86} 87} 88for ( const { label, rate_limit} of limits ) { 89const rate = object ( rate_limit ); 90for ( 91const [ key , fallback ] of [[ "primary_window" , "Session" ], [ "secondary_window" , "Weekly" ]] 92) { 93const window = object ( rate [ key ]); 94const seconds = window . limit_window_seconds ; 95const duration = finite ( seconds ) && seconds > 0 96 ?seconds % 86400 === 0 ?` ${ seconds / 86400 } days` :` ${ seconds / 3600 } hours` 97 :fallback ; 98add ( label ?` ${ label } · ${ duration } ` :duration , window . used_percent , window . reset_at ); 99} 100} 101} 102return windows ; 103} 104 105export function subscriptionUsage ({ 106 auth, 107fetch :request = fetch , 108 now= Date . now , 109} :{ 110auth :( provider :string , signal ?:AbortSignal ) => Promise < AuthResult | undefined >; 111fetch ?:typeof fetch ; 112now ?:() => number ; 113}) { 114const cache = new Map < Provider , { 115token :string ; 116until :number ; 117result :SubscriptionUsage ; 118}>(); 119const pending = new Map < Provider , Promise < SubscriptionUsage >>(); 120 121async function load ( provider :Provider ) :Promise < SubscriptionUsage > { 122const signal = AbortSignal . timeout ( 15_000 ); 123let resolved :AuthResult | undefined ; 124try { 125resolved = await auth ( provider , signal ); 126} catch { 127return { 128status :"error" , 129message :"Could not refresh login. Try signing in again in Settings." , 130}; 131} 132if ( ! resolved ?. auth . apiKey || ( provider !== "opencode-go" && resolved . source !== "OAuth" )) { 133cache . delete ( provider ); 134return { 135status :"unavailable" , 136message :provider === "opencode-go" 137 ?"Add your OpenCode Go API key in Settings to view subscription limits." 138 :"Subscription limits require an OAuth login in Settings; API keys do not expose them." , 139}; 140} 141const token = resolved . auth . apiKey ; 142const cached = cache . get ( provider ); 143if ( cached ?. token === token && cached . until > now ()) return cached . result ; 144let result :SubscriptionUsage ; 145let ttl = 60_000 ; 146try { 147const headers = new Headers ({ authorization :`Bearer ${ token } ` , accept :"application/json" }); 148const url = provider === "anthropic" 149 ?"https://api.anthropic.com/api/oauth/usage" 150 :provider === "opencode-go" 151 ?"https://opencode.ai/inference/go/v1/usage" 152 :"https://chatgpt.com/backend-api/wham/usage" ; 153if ( provider === "anthropic" ) { 154headers . set ( "anthropic-beta" , "oauth-2025-04-20" ); 155} else if ( provider !== "opencode-go" ) { 156try { 157const payload = JSON . parse ( 158atob ( token . split ( "." )[ 1 ]. replaceAll ( "-" , "+" ). replaceAll ( "_" , "/" )), 159); 160const account = payload [ "https://api.openai.com/auth" ]?. chatgpt_account_id ; 161if ( typeof account !== "string" || ! account ) throw new Error ( "Missing account" ); 162headers . set ( "ChatGPT-Account-Id" , account ); 163} catch { 164return { 165status :"error" , 166message :"Could not identify the ChatGPT account. Sign in again in Settings." , 167}; 168} 169} 170const response = await request ( url , { headers, signal, redirect :"error" }); 171if ( ! response . ok ) { 172const message = response . status === 401 || response . status === 403 173 ?provider === "opencode-go" 174 ?"Usage access was denied. Check your OpenCode Go API key and subscription in Settings." 175 :provider === "anthropic" 176 ?"Usage access was denied. Sign in again in Settings; Claude may need the user:profile scope." 177 :"Usage access was denied. Sign in again in Settings." 178 :response . status === 429 179 ?"Usage is rate limited. Try again in a few minutes." 180 :`Usage service returned HTTP ${ response . status } .` ; 181if ( response . status === 429 ) { 182const retry = response . headers . get ( "retry-after" ) ?? "" ; 183const seconds = Number ( retry ); 184const until = retry . trim () && Number . isFinite ( seconds ) 185 ?now () + seconds * 1000 186 :Date . parse ( retry ); 187ttl = Math . max ( 5 * 60_000 , Number . isFinite ( until ) ?until - now () :0 ); 188} 189await response . body ?. cancel (); 190result = { status :"error" , message}; 191} else { 192const windows = usageWindows ( provider , await response . json (), now ()); 193result = windows . length 194 ?{ status :"ok" , fetchedAt :new Date ( now ()). toISOString (), windows} 195 :{ 196status :"unavailable" , 197message :"No subscription usage windows were reported for this account." , 198}; 199} 200} catch { 201result = { 202status :"error" , 203message :"Could not reach the usage service. Try again shortly." , 204}; 205} 206cache . set ( provider , { token, until :now () + ttl , result}); 207return result ; 208} 209 210return ( provider :Provider ) :Promise < SubscriptionUsage > => { 211const existing = pending . get ( provider ); 212if ( existing ) return existing ; 213const next = load ( provider ). finally (() => pending . delete ( provider )); 214pending . set ( provider , next ); 215return next ; 216}; 217}