claude-multiacc 1.0.14 → 1.0.16
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/README.md +32 -6
- package/bin/claude +215 -12
- package/bin/claude-accounts +306 -30
- package/install.sh +25 -4
- package/lib/report.py +81 -2
- package/package.json +1 -1
- package/tests/run-tests.sh +522 -8
package/lib/report.py
CHANGED
|
@@ -41,6 +41,25 @@ import sys
|
|
|
41
41
|
import time
|
|
42
42
|
|
|
43
43
|
SCHEMA = 'claude-multiacc/pool.v1'
|
|
44
|
+
|
|
45
|
+
# Must match the SHIM's ranking window, or this document calls data "fresh" that
|
|
46
|
+
# selection has already stopped ranking on. The two providers differ on purpose:
|
|
47
|
+
# api.anthropic.com hands out Retry-After: 3600, so bin/claude trusts an hour;
|
|
48
|
+
# bin/codex still uses 900 against chatgpt.com, which has not been measured.
|
|
49
|
+
_STALE_DEFAULT = {'claude': 3600, 'codex': 900}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _stale_after(provider):
|
|
53
|
+
env = {'claude': 'CLAUDE_MULTIACC_STALE_AFTER',
|
|
54
|
+
'codex': 'CODEX_MULTIACC_STALE_AFTER'}.get(provider)
|
|
55
|
+
default = _STALE_DEFAULT.get(provider, 900)
|
|
56
|
+
try:
|
|
57
|
+
v = int(os.environ.get(env) or default) if env else default
|
|
58
|
+
except ValueError:
|
|
59
|
+
v = default
|
|
60
|
+
return v if v > 0 else default
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
VALID_ID = re.compile(r'acct-\d{2}')
|
|
45
64
|
|
|
46
65
|
|
|
@@ -181,7 +200,7 @@ def _codex_credentials(d, jwt_claims):
|
|
|
181
200
|
return 'machine-local', detail
|
|
182
201
|
|
|
183
202
|
|
|
184
|
-
def _usage(d):
|
|
203
|
+
def _usage(d, stale_after):
|
|
185
204
|
lim = _load_json(os.path.join(d, 'limits.json'))
|
|
186
205
|
if not lim:
|
|
187
206
|
return None
|
|
@@ -200,6 +219,19 @@ def _usage(d):
|
|
|
200
219
|
out['age_seconds'] = max(0, int(time.time() - float(fetched)))
|
|
201
220
|
except (TypeError, ValueError):
|
|
202
221
|
out['age_seconds'] = None
|
|
222
|
+
# Past this window the shim stops ranking on these numbers entirely (bin/claude
|
|
223
|
+
# STALE_AFTER). A panel that shows "weekly 2%" without it is reporting a number
|
|
224
|
+
# nothing is using — which is how an 11-day telemetry outage stayed invisible.
|
|
225
|
+
out['stale'] = out['age_seconds'] is None or out['age_seconds'] > stale_after
|
|
226
|
+
# The horizon a stale weekly reading stays true until (bin/claude stale_weekly).
|
|
227
|
+
# Absent on documents written before it existed, which is itself the signal that
|
|
228
|
+
# the reading cannot be used for degraded ranking.
|
|
229
|
+
if lim.get('weekly_resets_epoch'):
|
|
230
|
+
out['weekly_resets_epoch'] = lim['weekly_resets_epoch']
|
|
231
|
+
if lim.get('last_error'):
|
|
232
|
+
out['last_error'] = lim['last_error']
|
|
233
|
+
if lim.get('last_error_at'):
|
|
234
|
+
out['last_error_at'] = _iso(lim['last_error_at'])
|
|
203
235
|
if lim.get('plan'):
|
|
204
236
|
out['plan'] = lim['plan']
|
|
205
237
|
if lim.get('retry_after'):
|
|
@@ -221,6 +253,44 @@ def _last_pick(root, aid):
|
|
|
221
253
|
return last
|
|
222
254
|
|
|
223
255
|
|
|
256
|
+
|
|
257
|
+
def telemetry_state(accounts, now=None):
|
|
258
|
+
"""Three-state verdict on how the shim is CURRENTLY ranking this pool, over the
|
|
259
|
+
same candidate set bin/claude uses: the eligible accounts, or — when every one of
|
|
260
|
+
them is limit-marked — the valid ones it falls back to.
|
|
261
|
+
|
|
262
|
+
'fresh' at least one candidate has telemetry inside the ranking window
|
|
263
|
+
'degraded' none do, but EVERY candidate still has a weekly reading whose bucket
|
|
264
|
+
has not reset yet, so the shim ranks on those (all-or-nothing: one
|
|
265
|
+
unusable reading and the comparison is not apples-to-apples)
|
|
266
|
+
'blind' nothing usable anywhere — every account scores the same and selection
|
|
267
|
+
is effectively a coin flip
|
|
268
|
+
'none' no candidates at all; nothing to say
|
|
269
|
+
|
|
270
|
+
Kept here rather than beside either caller because `status` and `--json` disagreeing
|
|
271
|
+
with the shim is exactly how an eleven-day outage stayed invisible.
|
|
272
|
+
"""
|
|
273
|
+
now = time.time() if now is None else now
|
|
274
|
+
usable = [a for a in accounts if a.get('status') in ('active', 'limited')]
|
|
275
|
+
cands = [a for a in usable if a.get('status') == 'active'] or usable
|
|
276
|
+
if not cands:
|
|
277
|
+
return 'none'
|
|
278
|
+
if any(a.get('usage') and not a['usage'].get('stale') for a in cands):
|
|
279
|
+
return 'fresh'
|
|
280
|
+
for a in cands:
|
|
281
|
+
u = a.get('usage') or {}
|
|
282
|
+
horizon = u.get('weekly_resets_epoch') or 0
|
|
283
|
+
pct = u.get('weekly_percent')
|
|
284
|
+
# bool is an int in Python but not a number to the shim's digit parser, so a
|
|
285
|
+
# document carrying `weekly_percent: true` must read as unusable HERE too —
|
|
286
|
+
# otherwise this says 'degraded' while the shim ranks blind.
|
|
287
|
+
if isinstance(horizon, bool) or isinstance(pct, bool) \
|
|
288
|
+
or not isinstance(horizon, (int, float)) or horizon <= now \
|
|
289
|
+
or not isinstance(pct, (int, float)):
|
|
290
|
+
return 'blind'
|
|
291
|
+
return 'degraded'
|
|
292
|
+
|
|
293
|
+
|
|
224
294
|
def _account_row(root, a, aid, d, provider, jwt_claims, audit_account, now, kind, machine):
|
|
225
295
|
"""One account's row. Everything it reads is already tolerant; build() catches
|
|
226
296
|
anything unexpected on top of that and degrades this account alone."""
|
|
@@ -256,7 +326,7 @@ def _account_row(root, a, aid, d, provider, jwt_claims, audit_account, now, kind
|
|
|
256
326
|
'limit_reset_at': _iso(reset_epoch) if limited and reset_epoch else None,
|
|
257
327
|
'limit_reset_epoch': reset_epoch if limited and reset_epoch else None,
|
|
258
328
|
'limit_detail': marker_detail if limited else None,
|
|
259
|
-
'usage': _usage(d),
|
|
329
|
+
'usage': _usage(d, _stale_after(provider)),
|
|
260
330
|
}
|
|
261
331
|
if kind == 'status':
|
|
262
332
|
row['last_picked'] = _last_pick(root, aid)
|
|
@@ -340,6 +410,15 @@ def build(root, provider, machine, kind='list'):
|
|
|
340
410
|
'needs_login': sum(1 for a in accounts if a['needs_login']),
|
|
341
411
|
'portable': sum(1 for a in accounts if a['portable']),
|
|
342
412
|
'selectable': sum(1 for a in accounts if a['selectable']),
|
|
413
|
+
# The pool-wide verdict, so a dashboard can show the outage rather than a
|
|
414
|
+
# grid of reassuring percentages nothing is ranking on.
|
|
415
|
+
# fresh ranking on current data
|
|
416
|
+
# degraded ranking on old-but-still-true weekly readings
|
|
417
|
+
# blind every account scores the same; selection is a coin flip
|
|
418
|
+
'telemetry': telemetry_state(accounts, now),
|
|
419
|
+
# Kept as its own boolean because a dashboard alerting on one field should
|
|
420
|
+
# not have to know the vocabulary. True ONLY for the coin-flip case.
|
|
421
|
+
'ranking_blind': telemetry_state(accounts, now) == 'blind',
|
|
343
422
|
},
|
|
344
423
|
'warnings': warnings,
|
|
345
424
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|