opencode-auth-load-balancer 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/v2-entry.js CHANGED
@@ -41812,6 +41812,18 @@ function normalizeWindow(w) {
41812
41812
  win.resetAt = 0;
41813
41813
  return win;
41814
41814
  }
41815
+ function normalizeCredits(c) {
41816
+ if (!isPlainObject4(c))
41817
+ return;
41818
+ const credits = c;
41819
+ if (typeof credits.hasCredits !== "boolean" || typeof credits.unlimited !== "boolean")
41820
+ return;
41821
+ if (!isFiniteNumber(credits.balance))
41822
+ delete credits.balance;
41823
+ if (credits.limitReached !== true)
41824
+ delete credits.limitReached;
41825
+ return credits;
41826
+ }
41815
41827
  function normalizeAccounts(rows) {
41816
41828
  let accounts = null;
41817
41829
  for (let i = 0;i < rows.length; i++) {
@@ -41831,7 +41843,13 @@ function normalizeAccounts(rows) {
41831
41843
  row.usage.capturedAt = 0;
41832
41844
  row.usage.hourly = normalizeWindow(row.usage.hourly);
41833
41845
  row.usage.weekly = normalizeWindow(row.usage.weekly);
41846
+ delete row.usage.credits;
41834
41847
  }
41848
+ const credits = normalizeCredits(row.credits);
41849
+ if (credits)
41850
+ row.credits = credits;
41851
+ else
41852
+ delete row.credits;
41835
41853
  if (!Number.isFinite(row.cooldownUntil))
41836
41854
  row.cooldownUntil = 0;
41837
41855
  if (row.cooldownKind !== undefined && row.cooldownKind !== "quota" && row.cooldownKind !== "auth" && row.cooldownKind !== "transient")
@@ -42084,14 +42102,19 @@ function maxUtil(account, now) {
42084
42102
  function isExhausted(account, cfg, now) {
42085
42103
  return maxUtil(account, now) >= cfg.exhaustedAt;
42086
42104
  }
42105
+ function hasCreditHeadroom(account) {
42106
+ const credits = account.credits;
42107
+ return !!credits && !credits.limitReached && (credits.unlimited || credits.hasCredits);
42108
+ }
42109
+ function hasHeadroom(account, cfg, now) {
42110
+ return !isExhausted(account, cfg, now) || hasCreditHeadroom(account);
42111
+ }
42087
42112
  function isAvailable(account, cfg, now) {
42088
42113
  if (account.disabledReason)
42089
42114
  return false;
42090
42115
  if (account.cooldownUntil > now)
42091
42116
  return false;
42092
- if (isExhausted(account, cfg, now))
42093
- return false;
42094
- return true;
42117
+ return hasHeadroom(account, cfg, now);
42095
42118
  }
42096
42119
  function weeklyUrgency(account, cfg, now) {
42097
42120
  const weekly = account.usage.weekly;
@@ -42122,9 +42145,11 @@ function scoreAccount(account, cfg, now) {
42122
42145
  function compareRanked(x, y) {
42123
42146
  if (x.available !== y.available)
42124
42147
  return x.available ? -1 : 1;
42125
- if (x.available)
42126
- return y.score - x.score;
42127
- return x.weeklyUtil - y.weeklyUtil;
42148
+ if (!x.available)
42149
+ return x.weeklyUtil - y.weeklyUtil;
42150
+ if (x.onCredits !== y.onCredits)
42151
+ return x.onCredits ? 1 : -1;
42152
+ return y.score - x.score;
42128
42153
  }
42129
42154
  var DAY_MS, WEEK_MS2, HOUR_MS, HOURLY_WINDOW_MS, RESET_CUSHION_DAYS = 0.25, SCORE_DEFAULTS;
42130
42155
  var init_score_core = __esm(() => {
@@ -42143,6 +42168,14 @@ var init_score_core = __esm(() => {
42143
42168
  });
42144
42169
 
42145
42170
  // src/usage-merge.ts
42171
+ function reconcileQuotaCooldown(account, cfg, now) {
42172
+ if (account.cooldownKind === "auth" || account.cooldownKind === "transient")
42173
+ return;
42174
+ if (account.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && hasHeadroom(account, cfg, now)) {
42175
+ account.cooldownUntil = 0;
42176
+ delete account.cooldownKind;
42177
+ }
42178
+ }
42146
42179
  function rollWeeklyAnchorForward(anchor, now) {
42147
42180
  if (!Number.isFinite(anchor) || anchor <= 0)
42148
42181
  return 0;
@@ -42159,8 +42192,10 @@ function preserveWeeklyAnchor(incoming, stored, now) {
42159
42192
  return incoming;
42160
42193
  return { utilization: incoming.utilization, resetAt: anchor };
42161
42194
  }
42195
+ var MAX_TRANSIENT_COOLDOWN_MS;
42162
42196
  var init_usage_merge = __esm(() => {
42163
42197
  init_score_core();
42198
+ MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
42164
42199
  });
42165
42200
 
42166
42201
  // src/accounts.ts
@@ -42207,11 +42242,15 @@ function clearLostLogin(row, login) {
42207
42242
  function mergeLoginUsage(row, usage) {
42208
42243
  if (!usage)
42209
42244
  return;
42245
+ if (usage.credits)
42246
+ row.credits = usage.credits;
42210
42247
  row.usage = {
42211
42248
  hourly: usage.hourly ?? row.usage.hourly,
42212
42249
  weekly: usage.weekly ? preserveWeeklyAnchor(usage.weekly, row.usage.weekly, usage.capturedAt) : row.usage.weekly,
42213
42250
  capturedAt: usage.weekly ? usage.capturedAt : row.usage.capturedAt
42214
42251
  };
42252
+ if (usage.weekly)
42253
+ reconcileQuotaCooldown(row, loadScoreConfig(), usage.capturedAt);
42215
42254
  }
42216
42255
  function applyTokens(row, tokens) {
42217
42256
  if (tokens.refresh) {
@@ -42305,6 +42344,7 @@ async function bootstrapFromOpencodeAuth(providerID, getAuth, tokensFromApiKey)
42305
42344
  var init_accounts = __esm(() => {
42306
42345
  init_pairing();
42307
42346
  init_store();
42347
+ init_score_core();
42308
42348
  init_types2();
42309
42349
  init_usage_merge();
42310
42350
  });
@@ -42329,7 +42369,8 @@ function classifyProviderRecovery(accounts, providerID, cfg, now, pollMs) {
42329
42369
  continue;
42330
42370
  const windows = exhaustedWindows(account, cfg, now);
42331
42371
  const cooldownActive = account.cooldownUntil > now;
42332
- if (!cooldownActive && windows.length === 0)
42372
+ const blocking = hasCreditHeadroom(account) ? [] : windows;
42373
+ if (!cooldownActive && blocking.length === 0)
42333
42374
  return { state: "available" };
42334
42375
  if (cooldownActive && (account.cooldownKind === "auth" || account.cooldownKind === "transient"))
42335
42376
  continue;
@@ -42338,7 +42379,7 @@ function classifyProviderRecovery(accounts, providerID, cfg, now, pollMs) {
42338
42379
  hasQuotaCandidate = true;
42339
42380
  const constraints = [];
42340
42381
  let accountHasUnknownReset = false;
42341
- for (const window of windows) {
42382
+ for (const window of blocking) {
42342
42383
  if (window.resetAt > now)
42343
42384
  constraints.push(window.resetAt);
42344
42385
  else
@@ -42651,22 +42692,27 @@ function weeklyUtil(account) {
42651
42692
  function selectAccount(accounts, providerID, now, cfg = DEFAULT_CONFIG, exclude = NO_EXCLUSIONS, incumbentId = null) {
42652
42693
  let bestAvail = null;
42653
42694
  let bestAvailScore = Number.NEGATIVE_INFINITY;
42695
+ let bestAvailOnCredits = true;
42654
42696
  let bestFallback = null;
42655
42697
  let lowestWeeklyUtil = Number.POSITIVE_INFINITY;
42656
42698
  let incumbentAccount = null;
42657
42699
  let incumbentScore = Number.NEGATIVE_INFINITY;
42700
+ let incumbentOnCredits = false;
42658
42701
  for (const account of accounts) {
42659
42702
  if (account.providerID !== providerID || account.disabledReason || exclude.has(account.id))
42660
42703
  continue;
42661
42704
  if (isAvailable(account, cfg, now)) {
42662
42705
  const score = scoreAccount(account, cfg, now);
42663
- if (score > bestAvailScore) {
42706
+ const onCredits = isExhausted(account, cfg, now);
42707
+ if (onCredits === bestAvailOnCredits ? score > bestAvailScore : bestAvailOnCredits) {
42664
42708
  bestAvailScore = score;
42665
42709
  bestAvail = account;
42710
+ bestAvailOnCredits = onCredits;
42666
42711
  }
42667
42712
  if (account.id === incumbentId) {
42668
42713
  incumbentAccount = account;
42669
42714
  incumbentScore = score;
42715
+ incumbentOnCredits = onCredits;
42670
42716
  }
42671
42717
  } else {
42672
42718
  const util = weeklyUtil(account);
@@ -42677,7 +42723,7 @@ function selectAccount(accounts, providerID, now, cfg = DEFAULT_CONFIG, exclude
42677
42723
  }
42678
42724
  }
42679
42725
  if (bestAvail) {
42680
- if (incumbentAccount && incumbentScore >= bestAvailScore * (1 - INCUMBENT_HYSTERESIS))
42726
+ if (incumbentAccount && incumbentOnCredits === bestAvailOnCredits && incumbentScore >= bestAvailScore * (1 - INCUMBENT_HYSTERESIS))
42681
42727
  return { account: incumbentAccount, degraded: false, sticky: false };
42682
42728
  return { account: bestAvail, degraded: false, sticky: false };
42683
42729
  }
@@ -42716,7 +42762,7 @@ function selectForSession(pool, providerID, sessionKey, now, cfg = DEFAULT_CONFI
42716
42762
  const cheap = (!pinnedImminent || cfg.drainMigrate) && isCheapMoment(typeof requestBytes === "function" ? requestBytes() : requestBytes, cfg);
42717
42763
  if (pinnedOverSoft && (pinnedImminent || cheap) || cfg.drainMigrate && cheap) {
42718
42764
  const alt = selectAccount(pool.accounts, providerID, now, cfg, withExcluded(exclude, pinned.id), null);
42719
- if (alt && !alt.degraded) {
42765
+ if (alt && !alt.degraded && !isExhausted(alt.account, cfg, now)) {
42720
42766
  if (pinnedOverSoft) {
42721
42767
  const altUtil = maxUtil(alt.account, now);
42722
42768
  const proactiveBetter = pinnedImminent ? altUtil < pinnedUtil : pinnedUtil - altUtil >= PROACTIVE_MIGRATE_MIN_DELTA && weeklyUrgency(alt.account, cfg, now) >= weeklyUrgency(pinned, cfg, now);
@@ -43246,24 +43292,46 @@ var init_usage_headers = __esm(() => {
43246
43292
  });
43247
43293
 
43248
43294
  // src/providers/usage-http.ts
43249
- async function fetchJson(url, headers, timeoutMs) {
43250
- let response;
43295
+ async function request(url, headers, timeoutMs) {
43251
43296
  try {
43252
- response = await fetch(url, {
43297
+ return await fetch(url, {
43253
43298
  headers,
43254
43299
  signal: AbortSignal.timeout(timeoutMs)
43255
43300
  });
43256
43301
  } catch {
43257
43302
  return null;
43258
43303
  }
43304
+ }
43305
+ async function readJson(response) {
43306
+ if (!response)
43307
+ return null;
43259
43308
  if (!response.ok) {
43260
43309
  await response.body?.cancel().catch(ignore);
43261
43310
  return null;
43262
43311
  }
43263
43312
  return await response.json().catch(() => null);
43264
43313
  }
43314
+ async function fetchJson(url, headers, timeoutMs) {
43315
+ return readJson(await request(url, headers, timeoutMs));
43316
+ }
43317
+ async function fetchUsageJson(url, headers, timeoutMs) {
43318
+ const response = await request(url, headers, timeoutMs);
43319
+ if (response?.status === 401) {
43320
+ await response.body?.cancel().catch(ignore);
43321
+ throw new UsageAuthenticationError;
43322
+ }
43323
+ return readJson(response);
43324
+ }
43325
+ var UsageAuthenticationError;
43265
43326
  var init_usage_http = __esm(() => {
43266
43327
  init_util4();
43328
+ UsageAuthenticationError = class UsageAuthenticationError extends Error {
43329
+ status = 401;
43330
+ constructor() {
43331
+ super("Usage endpoint rejected the access token (401)");
43332
+ this.name = "UsageAuthenticationError";
43333
+ }
43334
+ };
43267
43335
  });
43268
43336
 
43269
43337
  // src/providers/anthropic/version.ts
@@ -43703,7 +43771,7 @@ async function probeUsage(access, now) {
43703
43771
  async function fetchUsage(account, now) {
43704
43772
  if (!account.refresh)
43705
43773
  return probeUsage(account.access, now);
43706
- const json = await fetchJson(USAGE_URL, {
43774
+ const json = await fetchUsageJson(USAGE_URL, {
43707
43775
  authorization: `Bearer ${account.access}`,
43708
43776
  "anthropic-beta": "oauth-2025-04-20",
43709
43777
  "user-agent": usageUserAgent(),
@@ -44035,7 +44103,7 @@ function parseUsages(json, now) {
44035
44103
  return hourly || weekly ? { hourly, weekly, capturedAt: now } : null;
44036
44104
  }
44037
44105
  function getUsages(baseUrl, key) {
44038
- return fetchJson(`${baseUrl}/usages`, { authorization: `Bearer ${key}`, accept: "application/json" }, USAGE_HTTP_TIMEOUT_MS);
44106
+ return fetchUsageJson(`${baseUrl}/usages`, { authorization: `Bearer ${key}`, accept: "application/json" }, USAGE_HTTP_TIMEOUT_MS);
44039
44107
  }
44040
44108
  function usageFetcher(baseUrl) {
44041
44109
  return async (account, now) => parseUsages(await getUsages(baseUrl, account.access), now);
@@ -44292,10 +44360,31 @@ var init_transform2 = __esm(() => {
44292
44360
  function isWeeklyMinutes(minutes) {
44293
44361
  return minutes >= WEEK_MINUTES * 0.95 && minutes <= WEEK_MINUTES * 1.05;
44294
44362
  }
44363
+ function creditsFrom(hasCredits, unlimited, balance, limitReached) {
44364
+ if (typeof hasCredits !== "boolean" || typeof unlimited !== "boolean")
44365
+ return;
44366
+ const amount = typeof balance === "string" && balance.trim() !== "" ? Number(balance) : Number.NaN;
44367
+ return {
44368
+ hasCredits,
44369
+ unlimited,
44370
+ ...Number.isFinite(amount) ? { balance: amount } : {},
44371
+ ...limitReached === true ? { limitReached } : {}
44372
+ };
44373
+ }
44374
+ function headerBool(raw) {
44375
+ const value = raw?.toLowerCase();
44376
+ if (value === "true" || value === "1")
44377
+ return true;
44378
+ if (value === "false" || value === "0")
44379
+ return false;
44380
+ return;
44381
+ }
44295
44382
  function parseUsageHeaders2(headers) {
44296
44383
  const primaryWindowMinutes = Number(headers.get(PRIMARY_WINDOW_MINUTES_HEADER));
44297
44384
  const spec = isWeeklyMinutes(primaryWindowMinutes) ? WEEKLY_PRIMARY_HEADER_SPEC : HEADER_SPEC2;
44298
- return parseWindowPairHeaders(headers, spec);
44385
+ const windows = parseWindowPairHeaders(headers, spec);
44386
+ const credits = creditsFrom(headerBool(headers.get("x-codex-credits-has-credits")), headerBool(headers.get("x-codex-credits-unlimited")), headers.get("x-codex-credits-balance"));
44387
+ return credits ? { ...windows, credits } : windows;
44299
44388
  }
44300
44389
  function endpointWindow2(w) {
44301
44390
  return endpointWindowFrom(w, (win) => win.used_percent, 100, (win) => secondsToMs(Number(win.reset_at ?? 0)));
@@ -44309,16 +44398,19 @@ async function fetchUsage2(account, now) {
44309
44398
  const accountId = resolveAccountId(account);
44310
44399
  if (accountId)
44311
44400
  headers["chatgpt-account-id"] = accountId;
44312
- const json = await fetchJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
44401
+ const json = await fetchUsageJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
44313
44402
  const rateLimit = json?.rate_limit;
44314
44403
  if (!isPlainObject4(rateLimit))
44315
44404
  return null;
44316
44405
  const { primary_window: primary, secondary_window: secondary } = rateLimit;
44317
44406
  const primaryIsWeekly = isWeeklyMinutes(Math.ceil(Number(primary?.limit_window_seconds) / 60));
44407
+ const reported = json?.credits;
44408
+ const credits = isPlainObject4(reported) ? creditsFrom(reported.has_credits, reported.unlimited, reported.balance, reported.overage_limit_reached) : undefined;
44318
44409
  return {
44319
44410
  hourly: endpointWindow2(primaryIsWeekly ? secondary : primary),
44320
44411
  weekly: endpointWindow2(primaryIsWeekly ? primary : secondary),
44321
- capturedAt: now
44412
+ capturedAt: now,
44413
+ ...credits ? { credits } : {}
44322
44414
  };
44323
44415
  }
44324
44416
  var WEEK_MINUTES, PRIMARY_WINDOW_MINUTES_HEADER = "x-codex-primary-window-minutes", HEADER_SPEC2, WEEKLY_PRIMARY_HEADER_SPEC;
@@ -44365,6 +44457,7 @@ var init_adapter3 = __esm(() => {
44365
44457
  },
44366
44458
  parseUsageHeaders: parseUsageHeaders2,
44367
44459
  fetchUsage: fetchUsage2,
44460
+ reportsCredits: true,
44368
44461
  classifyError: classifyHttpStatus
44369
44462
  };
44370
44463
  });
@@ -44398,11 +44491,14 @@ async function refreshUsage(adapters, now, poolSnapshot) {
44398
44491
  aliveIds?.add(account.id);
44399
44492
  if (account.disabledReason && account.disabledReason !== MANUAL_DISABLED_REASON)
44400
44493
  continue;
44401
- if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt <= USAGE_REFRESH_TTL_MS)
44494
+ const fresh = account.usage.capturedAt !== 0 && now - account.usage.capturedAt < USAGE_REFRESH_TTL_MS;
44495
+ if (fresh && account.credits)
44402
44496
  continue;
44403
44497
  const adapter = adapterFor(adapters, account.providerID);
44404
44498
  if (!adapter)
44405
44499
  continue;
44500
+ if (fresh && !adapter.reportsCredits)
44501
+ continue;
44406
44502
  if ((lastPoll.get(account.id) ?? 0) > now - USAGE_REFRESH_TTL_MS)
44407
44503
  continue;
44408
44504
  lastPoll.set(account.id, now);
@@ -44420,20 +44516,29 @@ async function refreshUsage(adapters, now, poolSnapshot) {
44420
44516
  await Promise.all(stale.map(async ({ adapter, account }) => {
44421
44517
  try {
44422
44518
  await ensureAccessToken(adapter, account, now);
44423
- const snapshot = await adapter.fetchUsage(account, now);
44519
+ const rejectedAccess = account.access;
44520
+ let snapshot;
44521
+ try {
44522
+ snapshot = await adapter.fetchUsage(account, now);
44523
+ } catch (error) {
44524
+ if (!(error instanceof UsageAuthenticationError) || !account.refresh)
44525
+ throw error;
44526
+ await ensureAccessToken(adapter, account, now, rejectedAccess);
44527
+ snapshot = await adapter.fetchUsage(account, now);
44528
+ }
44424
44529
  if (snapshot) {
44425
44530
  await mutatePool((p) => {
44426
44531
  const stored = findAccount(p, account.id);
44427
44532
  if (stored) {
44533
+ if (snapshot.credits)
44534
+ stored.credits = snapshot.credits;
44428
44535
  stored.usage = {
44429
44536
  hourly: snapshot.hourly ?? stored.usage.hourly,
44430
44537
  weekly: snapshot.weekly === null ? stored.usage.weekly : preserveWeeklyAnchor(snapshot.weekly, stored.usage.weekly, now),
44431
44538
  capturedAt: snapshot.weekly === null ? stored.usage.capturedAt : now
44432
44539
  };
44433
- if (snapshot.weekly !== null && stored.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && !isExhausted(stored, scoreCfg, now)) {
44434
- stored.cooldownUntil = 0;
44435
- delete stored.cooldownKind;
44436
- }
44540
+ if (snapshot.weekly !== null && stored.cooldownUntil === account.cooldownUntil && stored.cooldownKind === account.cooldownKind)
44541
+ reconcileQuotaCooldown(stored, scoreCfg, now);
44437
44542
  }
44438
44543
  });
44439
44544
  }
@@ -44446,16 +44551,17 @@ function refreshUsageInBackground(adapter, now, poolSnapshot) {
44446
44551
  function refreshAllUsageInBackground(now, poolSnapshot) {
44447
44552
  return refreshUsage(ADAPTERS, now, poolSnapshot);
44448
44553
  }
44449
- var USAGE_REFRESH_TTL_MS, MAX_TRANSIENT_COOLDOWN_MS, lastPoll;
44554
+ var USAGE_REFRESH_TTL_MS, lastPoll;
44450
44555
  var init_usage_refresh = __esm(() => {
44451
44556
  init_store();
44452
44557
  init_registry();
44558
+ init_usage_http();
44453
44559
  init_refresh();
44454
44560
  init_score_core();
44455
44561
  init_types2();
44456
44562
  init_usage_merge();
44563
+ init_usage_merge();
44457
44564
  USAGE_REFRESH_TTL_MS = 5 * 60 * 1000;
44458
- MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
44459
44565
  lastPoll = new Map;
44460
44566
  });
44461
44567
 
@@ -44478,6 +44584,8 @@ async function bestEffort(what, op) {
44478
44584
  function applyUsagePartial(account, partial, now) {
44479
44585
  if (partial.hourly !== undefined)
44480
44586
  account.usage.hourly = partial.hourly;
44587
+ if (partial.credits !== undefined)
44588
+ account.credits = partial.credits;
44481
44589
  if (partial.weekly !== undefined) {
44482
44590
  account.usage.weekly = preserveWeeklyAnchor(partial.weekly, account.usage.weekly, now);
44483
44591
  account.usage.capturedAt = now;
@@ -44546,7 +44654,7 @@ async function recordSuccess(adapter, accountId, partial, sessionKey, now, cfg,
44546
44654
  if (account) {
44547
44655
  if (partial)
44548
44656
  applyUsagePartial(account, partial, now);
44549
- if (partial && account.cooldownUntil > now + ACCOUNT_COOLDOWN_MS && !isExhausted(account, cfg, now)) {
44657
+ if (partial && account.cooldownUntil > now + ACCOUNT_COOLDOWN_MS && hasHeadroom(account, cfg, now)) {
44550
44658
  account.cooldownUntil = 0;
44551
44659
  delete account.cooldownKind;
44552
44660
  }
@@ -44890,7 +44998,18 @@ function byProviderId(a, b) {
44890
44998
  function isExpired(window, now) {
44891
44999
  return !!window && window.resetAt > 0 && window.resetAt <= now;
44892
45000
  }
44893
- function toStatus(account, now, current, available, rank) {
45001
+ function creditsText(credits) {
45002
+ if (!credits)
45003
+ return "";
45004
+ if (credits.limitReached)
45005
+ return "credit limit reached";
45006
+ if (credits.unlimited)
45007
+ return "credits unlimited";
45008
+ if (!credits.hasCredits)
45009
+ return "no credits";
45010
+ return credits.balance === undefined ? "credits available" : `credits ${Math.round(credits.balance * 100) / 100}`;
45011
+ }
45012
+ function toStatus(account, now, current, available, exhausted, rank) {
44894
45013
  const weekly = account.usage.weekly;
44895
45014
  const weeklyExpired = isExpired(weekly, now);
44896
45015
  return {
@@ -44906,6 +45025,7 @@ function toStatus(account, now, current, available, rank) {
44906
45025
  login: credentialTag(account),
44907
45026
  warnings: loginWarnings(account, now),
44908
45027
  lostNotes: lostLoginNotes(account, now),
45028
+ credits: exhausted ? creditsText(account.credits) : "",
44909
45029
  current,
44910
45030
  rank
44911
45031
  };
@@ -44924,14 +45044,17 @@ function buildStatus(pool, now, cfg = DEFAULT_CONFIG) {
44924
45044
  const currentAccountId = pool.lastSelected[providerID] ?? null;
44925
45045
  const ranked = providerAccounts.map((a) => {
44926
45046
  const available = isAvailable(a, cfg, now);
45047
+ const exhausted = isExhausted(a, cfg, now);
44927
45048
  return {
44928
45049
  a,
44929
45050
  available,
45051
+ exhausted,
45052
+ onCredits: available && exhausted,
44930
45053
  score: available ? scoreAccount(a, cfg, now) : Number.NEGATIVE_INFINITY,
44931
45054
  weeklyUtil: a.usage.weekly?.utilization ?? 0
44932
45055
  };
44933
45056
  }).sort(compareRanked);
44934
- const accounts = ranked.map(({ a, available }, i) => toStatus(a, now, a.id === currentAccountId, available, i + 1));
45057
+ const accounts = ranked.map(({ a, available, exhausted }, i) => toStatus(a, now, a.id === currentAccountId, available, exhausted, i + 1));
44935
45058
  return { providerID, accounts };
44936
45059
  });
44937
45060
  }
@@ -44971,12 +45094,10 @@ function stateOf(a, now) {
44971
45094
  if (a.cooldownUntil > now)
44972
45095
  return `cooldown ${relTime(a.cooldownUntil, now)}`;
44973
45096
  if (!a.available)
44974
- return "exhausted";
45097
+ return a.credits ? `exhausted · ${a.credits}` : "exhausted";
44975
45098
  const base = a.current ? "in use" : "ready";
44976
45099
  const tiers = Object.entries(a.modelCooldownsUntil).filter(([, until]) => until > now).sort(([x], [y]) => compareAscii(x, y)).map(([tier, until]) => `${tier} ${relTime(until, now)}`);
44977
- if (tiers.length > 0)
44978
- return `${base} · ${tiers.join(" · ")}`;
44979
- return base;
45100
+ return [base, a.credits, ...tiers].filter(Boolean).join(" · ");
44980
45101
  }
44981
45102
  function providerName(providerID) {
44982
45103
  return PROVIDER_NAMES[providerID] ?? providerID;
@@ -45452,7 +45573,7 @@ class PendingCoordinator {
45452
45573
  await this.notifyPending(ref, recovery);
45453
45574
  try {
45454
45575
  for (;; ) {
45455
- await this.dependencies.sleep(Math.max(0, recovery.nextCheckAt - this.dependencies.now()), controller.signal);
45576
+ await this.dependencies.sleep(Math.max(0, Math.min(recovery.nextCheckAt - this.dependencies.now(), USAGE_REFRESH_TTL_MS)), controller.signal);
45456
45577
  const now = this.dependencies.now();
45457
45578
  await this.dependencies.refreshUsage(now).catch(ignore);
45458
45579
  let next;
@@ -46087,7 +46208,7 @@ function backoffFrom(res, now) {
46087
46208
  function isRateLimited(res) {
46088
46209
  return res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || res.headers.has("retry-after"));
46089
46210
  }
46090
- async function request(url, init, now) {
46211
+ async function request2(url, init, now) {
46091
46212
  let res;
46092
46213
  try {
46093
46214
  res = await globalThis.fetch(url, {
@@ -46139,7 +46260,7 @@ async function boundedText(res) {
46139
46260
  }
46140
46261
  return Buffer.concat(chunks).toString("utf8");
46141
46262
  }
46142
- async function readJson(res) {
46263
+ async function readJson2(res) {
46143
46264
  if (!res.ok)
46144
46265
  throw new SyncError("http");
46145
46266
  const text = await boundedText(res);
@@ -46154,7 +46275,7 @@ async function readJson(res) {
46154
46275
  return json;
46155
46276
  }
46156
46277
  async function createGist(token, content, now = Date.now()) {
46157
- const res = await request(API, {
46278
+ const res = await request2(API, {
46158
46279
  method: "POST",
46159
46280
  headers: { ...headers(token), "content-type": "application/json" },
46160
46281
  body: JSON.stringify({
@@ -46163,7 +46284,7 @@ async function createGist(token, content, now = Date.now()) {
46163
46284
  files: { [GIST_FILE]: { content } }
46164
46285
  })
46165
46286
  }, now);
46166
- const json = await readJson(res);
46287
+ const json = await readJson2(res);
46167
46288
  const owner = isPlainObject4(json.owner) ? json.owner.login : undefined;
46168
46289
  if (typeof json.id !== "string" || !GIST_ID_RE.test(json.id))
46169
46290
  throw new SyncError("http");
@@ -46175,7 +46296,7 @@ async function createGist(token, content, now = Date.now()) {
46175
46296
  async function updateGist(token, id, content, now = Date.now()) {
46176
46297
  let res;
46177
46298
  try {
46178
- res = await request(`${API}/${id}`, {
46299
+ res = await request2(`${API}/${id}`, {
46179
46300
  method: "PATCH",
46180
46301
  headers: { ...headers(token), "content-type": "application/json" },
46181
46302
  body: JSON.stringify({ files: { [GIST_FILE]: { content } } })
@@ -46187,10 +46308,10 @@ async function updateGist(token, id, content, now = Date.now()) {
46187
46308
  }
46188
46309
  if (res.status === 401 || res.status === 403)
46189
46310
  throw new SyncError("write-denied");
46190
- await readJson(res);
46311
+ await readJson2(res);
46191
46312
  }
46192
46313
  async function readGist(id, etag, now = Date.now(), token) {
46193
- const ask = (auth) => request(`${API}/${id}`, {
46314
+ const ask = (auth) => request2(`${API}/${id}`, {
46194
46315
  headers: {
46195
46316
  ...headers(auth),
46196
46317
  ...etag ? { "if-none-match": etag } : {}
@@ -46201,7 +46322,7 @@ async function readGist(id, etag, now = Date.now(), token) {
46201
46322
  res = await ask();
46202
46323
  if (res.status === 304)
46203
46324
  return { changed: false };
46204
- const json = await readJson(res);
46325
+ const json = await readJson2(res);
46205
46326
  const file = isPlainObject4(json.files) ? json.files[GIST_FILE] : undefined;
46206
46327
  if (!isPlainObject4(file) || typeof file.content !== "string")
46207
46328
  throw new SyncError("bad-blob");
@@ -47343,6 +47464,71 @@ var init_start = __esm(() => {
47343
47464
  init_state();
47344
47465
  });
47345
47466
 
47467
+ // src/usage-poll.ts
47468
+ import { readFile as readFile8 } from "node:fs/promises";
47469
+ async function nextPollAt(path) {
47470
+ try {
47471
+ const value = JSON.parse(await readFile8(path, "utf8"));
47472
+ return isPlainObject4(value) && isFiniteNumber(value.nextPollAt) ? value.nextPollAt : 0;
47473
+ } catch (error) {
47474
+ if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT")
47475
+ return 0;
47476
+ throw error;
47477
+ }
47478
+ }
47479
+ function startUsagePoll(deps = {}) {
47480
+ const now = deps.now ?? Date.now;
47481
+ const refresh = deps.refresh ?? refreshAllUsageInBackground;
47482
+ const timers = deps.timers ?? realTimers;
47483
+ let disposed = false;
47484
+ let inflight;
47485
+ async function poll() {
47486
+ const path = `${poolFilePath()}.usage-poll.json`;
47487
+ try {
47488
+ await withLock(`${path}.lock`, {
47489
+ staleMs: 120000,
47490
+ timeoutMs: 0,
47491
+ retryMs: 25,
47492
+ heartbeatMs: 5000
47493
+ }, async () => {
47494
+ const at = now();
47495
+ if (at < await nextPollAt(path))
47496
+ return;
47497
+ await writeJsonAtomic(path, JSON.stringify({ nextPollAt: at + USAGE_REFRESH_TTL_MS }));
47498
+ await refresh(at);
47499
+ });
47500
+ } catch (error) {
47501
+ if (!(error instanceof LockTimeoutError))
47502
+ throw error;
47503
+ }
47504
+ }
47505
+ function tick() {
47506
+ if (disposed)
47507
+ return Promise.resolve();
47508
+ inflight ??= poll().finally(() => {
47509
+ inflight = undefined;
47510
+ });
47511
+ return inflight;
47512
+ }
47513
+ const timer = timers.set(() => void tick().catch(ignore), USAGE_REFRESH_TTL_MS);
47514
+ return {
47515
+ tick,
47516
+ async dispose() {
47517
+ disposed = true;
47518
+ timers.clear(timer);
47519
+ await inflight;
47520
+ }
47521
+ };
47522
+ }
47523
+ var init_usage_poll = __esm(() => {
47524
+ init_lock();
47525
+ init_paths();
47526
+ init_store();
47527
+ init_loop();
47528
+ init_usage_refresh();
47529
+ init_util4();
47530
+ });
47531
+
47346
47532
  // src/legacy.ts
47347
47533
  function zeroOutCost(provider) {
47348
47534
  for (const model of Object.values(provider.models)) {
@@ -47497,8 +47683,12 @@ function createProviderPlugin(adapter) {
47497
47683
  }
47498
47684
  var PROVIDER_LABELS, AnthropicLoadBalancerPlugin, OpenAILoadBalancerPlugin, KimiCodeLoadBalancerPlugin, KimiCodeGlobalLoadBalancerPlugin, lbResult = (output) => ({ title: "Auth Load Balancer", output }), AuthLoadBalancerStatusPlugin = async (input) => {
47499
47685
  const sync = startSync();
47686
+ const usage = startUsagePoll();
47500
47687
  return {
47501
- dispose: async () => sync?.dispose(),
47688
+ dispose: async () => {
47689
+ await usage.dispose();
47690
+ await sync?.dispose();
47691
+ },
47502
47692
  tool: {
47503
47693
  auth_lb_status: tool2({
47504
47694
  description: "Show the auth load-balancer pool: the in-use account per provider, each account's weekly and 5h usage, cooldowns, and the ranked next-candidate accounts. Refreshes stale usage from the provider usage endpoints (throttled) before rendering.",
@@ -47587,6 +47777,7 @@ var init_legacy = __esm(() => {
47587
47777
  init_start();
47588
47778
  init_token_mint();
47589
47779
  init_types2();
47780
+ init_usage_poll();
47590
47781
  init_usage_refresh();
47591
47782
  init_util4();
47592
47783
  PROVIDER_LABELS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auth-load-balancer",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Load-balance opencode requests across multiple Anthropic (Claude) and OpenAI (Codex) OAuth accounts and Kimi Code subscriptions, weighted primarily by weekly usage with drain-before-reset scheduling.",
5
5
  "type": "module",
6
6
  "license": "MIT",