claude-multiacc 1.0.19 → 1.0.21
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 +3 -3
- package/bin/claude +33 -5
- package/bin/codex +5 -3
- package/package.json +1 -1
- package/tests/run-tests.sh +76 -2
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ and backs off on **every** non-2xx, not just a 429 — honoring `Retry-After`, a
|
|
|
122
122
|
for 6 h on a refusal the server marks `x-should-retry: false`. `limits --force` overrides
|
|
123
123
|
all of it. The cadence is deliberately unhurried: several machines polling the same
|
|
124
124
|
accounts every minute earns a 429 with `Retry-After: 3600`, and telemetry then goes stale
|
|
125
|
-
for an hour at a time — which is exactly when every account starts scoring *
|
|
125
|
+
for an hour at a time — which is exactly when every account starts scoring *unknown* and
|
|
126
126
|
the picker loses its ability to tell them apart.
|
|
127
127
|
|
|
128
128
|
> **The usage endpoint needs an OAuth login, not a setup token.** A portable
|
|
@@ -131,7 +131,7 @@ the picker loses its ability to tell them apart.
|
|
|
131
131
|
> user:profile`: setup tokens are minted without that scope. So an account whose
|
|
132
132
|
> `.credentials.json` grant has lapsed keeps working perfectly while going **permanently
|
|
133
133
|
> dark for telemetry**, and a pool where that happens to every account ranks everything
|
|
134
|
-
>
|
|
134
|
+
> unknown and picks at random. `claude-accounts status` says `RANKING IS BLIND` when the
|
|
135
135
|
> pool is in that state, and the shim prints an hourly warning on a terminal. The fix is
|
|
136
136
|
> a real sign-in on the machine that polls (`claude-accounts login <acct-NN>`); since
|
|
137
137
|
> `limits.json` is one of the things `sync` pushes, only the **source** machine needs it —
|
|
@@ -145,7 +145,7 @@ working accounts, while the cutoff declares one unusable, and an account reading
|
|
|
145
145
|
hour ago may be well past 90% now. When nothing is in-window at all, a stale weekly
|
|
146
146
|
reading is still used **if its bucket has not reset yet** (a weekly bucket only rises
|
|
147
147
|
until then, so the number remains a true lower bound) — logged as `ranking=DEGRADED`.
|
|
148
|
-
Only when even that is unavailable does selection
|
|
148
|
+
Only when even that is unavailable does selection rank the account last, logged as
|
|
149
149
|
`ranking=BLIND`.
|
|
150
150
|
|
|
151
151
|
If an account's OAuth access token has been expired for a while (idle account,
|
package/bin/claude
CHANGED
|
@@ -214,17 +214,17 @@ age_human() { # $1 = seconds
|
|
|
214
214
|
# confirm this reset asymmetry — an account whose only near-full bucket is the cheap
|
|
215
215
|
# session one must NOT rank behind one burning durable weekly headroom.)
|
|
216
216
|
# score = weekly%*1000 + session% weekly,session in [0,100]
|
|
217
|
-
# Stale/unreadable telemetry ranks
|
|
217
|
+
# Stale/unreadable telemetry ranks LAST (weekly 100, session 100), never "free" —
|
|
218
218
|
# EXCEPT in a blind pool (SEL_DEGRADED=1), where a still-valid stale weekly reading is
|
|
219
|
-
# used instead.
|
|
220
|
-
#
|
|
219
|
+
# used instead. Unknown data must never beat a truthful usage reading; when every
|
|
220
|
+
# candidate is unknown, the equal worst-case scores still preserve fail-open selection.
|
|
221
221
|
SEL_DEGRADED=0
|
|
222
222
|
sel_score_of() { # $1 = acct dir
|
|
223
223
|
local w s
|
|
224
224
|
if ! w="$(fresh_field "$1" weekly_percent)" && ! w="$(fresh_field "$1" max_percent)"; then
|
|
225
|
-
if [ "$SEL_DEGRADED" = 1 ]; then w="$(stale_weekly "$1")" || w=
|
|
225
|
+
if [ "$SEL_DEGRADED" = 1 ]; then w="$(stale_weekly "$1")" || w=100; else w=100; fi
|
|
226
226
|
fi
|
|
227
|
-
s="$(fresh_field "$1" session_percent)" || s=
|
|
227
|
+
s="$(fresh_field "$1" session_percent)" || s=100
|
|
228
228
|
printf '%s\n' $((w * 1000 + s))
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -1046,13 +1046,27 @@ argv_set_model() { # $1 = model id
|
|
|
1046
1046
|
PARK_SOFT_AUTH=3600 # 1h: a mis-parked healthy account is back within the hour
|
|
1047
1047
|
PARK_SOFT_ORG=21600 # 6h: an org policy will not change in minutes
|
|
1048
1048
|
|
|
1049
|
+
# True when the tail of stdout carries a stream-json result marked is_error. Bounded
|
|
1050
|
+
# to the tail because that is where the final object lands, and because a task log
|
|
1051
|
+
# runs to hundreds of KB of transcript that must not be rescanned per attempt.
|
|
1052
|
+
stream_reported_error() {
|
|
1053
|
+
tail -c 8192 "$tmpd/out" 2>/dev/null \
|
|
1054
|
+
| LC_ALL=C grep -qE '"type"[[:space:]]*:[[:space:]]*"result"' 2>/dev/null || return 1
|
|
1055
|
+
tail -c 8192 "$tmpd/out" 2>/dev/null \
|
|
1056
|
+
| LC_ALL=C grep -qE '"is_error"[[:space:]]*:[[:space:]]*true' 2>/dev/null
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1049
1059
|
attempt=1
|
|
1050
1060
|
cur="$pick"
|
|
1051
1061
|
rc=0
|
|
1062
|
+
stream_err=0 # the run exited 0 but the stream said otherwise
|
|
1052
1063
|
rotated=0 # at most one account rotation, exactly as before
|
|
1053
1064
|
model_fb_used=0 # ...and at most one model fallback after it
|
|
1054
1065
|
ARGV=("$@")
|
|
1055
1066
|
while :; do
|
|
1067
|
+
# Per ATTEMPT: a stream error seen on an earlier account must never decide the
|
|
1068
|
+
# exit status of a later one that failed for its own, real reason.
|
|
1069
|
+
stream_err=0
|
|
1056
1070
|
tok="$(acct_token "$cur")"
|
|
1057
1071
|
if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
|
|
1058
1072
|
if [ -n "$tok" ]; then
|
|
@@ -1062,6 +1076,17 @@ while :; do
|
|
|
1062
1076
|
fi
|
|
1063
1077
|
rc=$?
|
|
1064
1078
|
exec 3<&-
|
|
1079
|
+
# A `--output-format stream-json` run reports an API error INSIDE the stream and
|
|
1080
|
+
# still exits 0: the failure arrives as the final result object carrying
|
|
1081
|
+
# "is_error":true, not as a non-zero status. EVERY app-robot task takes that path,
|
|
1082
|
+
# so gating the retry branch on $rc alone meant the pool never rotated and never
|
|
1083
|
+
# fell back for the exact failures it exists for — the shim saw a clean success
|
|
1084
|
+
# every time while the task died in a second having done no work.
|
|
1085
|
+
#
|
|
1086
|
+
# Only the FINAL result counts. A 429 that appears mid-stream and is retried by
|
|
1087
|
+
# the client itself ends in a healthy result, and re-running that would throw away
|
|
1088
|
+
# a completed run.
|
|
1089
|
+
if [ "$rc" -eq 0 ] && stream_reported_error; then rc=1; stream_err=1; fi
|
|
1065
1090
|
if [ "$rc" -ne 0 ] && { [ "$rotated" = 0 ] || [ "$model_fb_used" = 0 ]; } \
|
|
1066
1091
|
&& grep -qiE "$ERRPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
|
|
1067
1092
|
# Atomic marker writes: a reader must never observe a half-written marker
|
|
@@ -1143,4 +1168,7 @@ done
|
|
|
1143
1168
|
|
|
1144
1169
|
cat "$tmpd/out"
|
|
1145
1170
|
cat "$tmpd/err" >&2
|
|
1171
|
+
# The exit status belongs to the CLI, not to us: a stream-json caller parses the
|
|
1172
|
+
# stream and expects 0 here. We only borrowed rc to decide whether to retry.
|
|
1173
|
+
[ "$stream_err" = 1 ] && [ "$rc" = 1 ] && rc=0
|
|
1146
1174
|
exit "$rc"
|
package/bin/codex
CHANGED
|
@@ -144,11 +144,13 @@ fresh_field() { # fresh_field <acct dir> <json key> -> integer if telemetry fres
|
|
|
144
144
|
# window only refills on its multi-day reset, while the ~5h window self-heals, so
|
|
145
145
|
# session is a mild tiebreaker only (same reset asymmetry as the claude pool).
|
|
146
146
|
# score = weekly%*1000 + session% weekly,session in [0,100]
|
|
147
|
-
# Stale/unreadable telemetry ranks
|
|
147
|
+
# Stale/unreadable telemetry ranks LAST (weekly 100, session 100), never "free".
|
|
148
|
+
# Equal worst-case scores keep an entirely unknown pool selectable, but an unknown
|
|
149
|
+
# account can never beat a candidate with truthful usage telemetry.
|
|
148
150
|
sel_score_of() { # $1 = acct dir
|
|
149
151
|
local w s
|
|
150
|
-
w="$(fresh_field "$1" weekly_percent)" || w="$(fresh_field "$1" max_percent)" || w=
|
|
151
|
-
s="$(fresh_field "$1" session_percent)" || s=
|
|
152
|
+
w="$(fresh_field "$1" weekly_percent)" || w="$(fresh_field "$1" max_percent)" || w=100
|
|
153
|
+
s="$(fresh_field "$1" session_percent)" || s=100
|
|
152
154
|
printf '%s\n' $((w * 1000 + s))
|
|
153
155
|
}
|
|
154
156
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
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": {
|
package/tests/run-tests.sh
CHANGED
|
@@ -90,6 +90,17 @@ if [ -f "$ctl" ] && grep -qx "orgfail:$acct" "$ctl" 2>/dev/null; then
|
|
|
90
90
|
fi
|
|
91
91
|
# A limit scoped to ONE MODEL, on EVERY account: rotating cannot help, only
|
|
92
92
|
# switching model can. Satisfied the moment the run carries the fallback model.
|
|
93
|
+
# The shape every app-robot task actually produces: --output-format stream-json
|
|
94
|
+
# reports the API error INSIDE the stream and exits 0.
|
|
95
|
+
if [ -f "$ctl" ] && grep -qx "streamlimit" "$ctl" 2>/dev/null; then
|
|
96
|
+
case " $* " in
|
|
97
|
+
*" claude-opus-5 "*|*"--model=claude-opus-5"*)
|
|
98
|
+
echo '{"type":"result","subtype":"success","is_error":false,"result":"done"}' ;;
|
|
99
|
+
*)
|
|
100
|
+
echo '{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"result":"You'"'"'ve reached your Fable 5 limit. Switch to another model, or manage usage credits to continue."}'
|
|
101
|
+
exit 0 ;;
|
|
102
|
+
esac
|
|
103
|
+
fi
|
|
93
104
|
if [ -f "$ctl" ] && grep -qx "modellimit" "$ctl" 2>/dev/null; then
|
|
94
105
|
case " $* " in
|
|
95
106
|
*" claude-opus-5 "*|*"--model=claude-opus-5"*) : ;;
|
|
@@ -242,11 +253,30 @@ done
|
|
|
242
253
|
&& t_ok "CLAUDE_SHIM_SELECT=random restores uniform spread" \
|
|
243
254
|
|| t_fail "random opt-out" "acct-01=$hits1 acct-02=$hits2"
|
|
244
255
|
|
|
245
|
-
#
|
|
256
|
+
# Unknown telemetry ranks behind every truthful reading, never as neutral or free.
|
|
246
257
|
printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$ACC/acct-01/limits.json"
|
|
247
258
|
lj 30 30 30 > "$ACC/acct-02/limits.json"
|
|
248
259
|
out="$(claude 2>&1)"
|
|
249
260
|
check "stale 1% loses to fresh 30% (stale is not trusted)" "CFG=acct-02" "$out"
|
|
261
|
+
lj 88 88 88 > "$ACC/acct-01/limits.json"
|
|
262
|
+
rm -f "$ACC/acct-02/limits.json" "$ACC/.last-pick"
|
|
263
|
+
all1=1
|
|
264
|
+
for _ in $(seq 1 15); do
|
|
265
|
+
case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
266
|
+
done
|
|
267
|
+
[ "$all1" = "1" ] && t_ok "unknown telemetry never beats a known account at 88%" \
|
|
268
|
+
|| t_fail "unknown telemetry ranking" "the unknown account beat truthful 88% usage"
|
|
269
|
+
rm -f "$ACC/acct-01/limits.json" "$ACC/.last-pick"
|
|
270
|
+
hits1=0; hits2=0
|
|
271
|
+
for _ in $(seq 1 40); do
|
|
272
|
+
case "$(claude 2>&1)" in
|
|
273
|
+
*CFG=acct-01*) hits1=$((hits1+1)) ;;
|
|
274
|
+
*CFG=acct-02*) hits2=$((hits2+1)) ;;
|
|
275
|
+
esac
|
|
276
|
+
done
|
|
277
|
+
{ [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
|
|
278
|
+
&& t_ok "an entirely unknown Claude pool still fails open" \
|
|
279
|
+
|| t_fail "unknown Claude pool fail-open" "acct-01=$hits1 acct-02=$hits2"
|
|
250
280
|
rm -f "$ACC"/acct-*/limits.json
|
|
251
281
|
|
|
252
282
|
# ---- 6. explicit pin -------------------------------------------------------
|
|
@@ -601,6 +631,31 @@ case "$out" in
|
|
|
601
631
|
esac
|
|
602
632
|
rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited
|
|
603
633
|
|
|
634
|
+
# ---- 12a2. an in-stream API error still triggers recovery --------------------
|
|
635
|
+
# With --output-format stream-json the CLI exits 0 and reports the 429 as the final
|
|
636
|
+
# result object. Every app-robot task takes that path, so gating retry on the exit
|
|
637
|
+
# status alone meant the pool never rotated and never fell back for the exact
|
|
638
|
+
# failures it exists for — while the shim recorded a clean success every time.
|
|
639
|
+
rm -f "$ACC"/acct-*/.limited; : > "$ACC/selection.log"
|
|
640
|
+
echo "streamlimit" > "$FAKE_CTL"
|
|
641
|
+
out="$(claude -p --model claude-fable-5 --output-format stream-json hello < /dev/null 2>/dev/null)"
|
|
642
|
+
rc=$?
|
|
643
|
+
case "$out" in
|
|
644
|
+
*'"is_error":false'*) t_ok "an in-stream API error is recovered, not reported as success" ;;
|
|
645
|
+
*) t_fail "stream error recovery" "rc=$rc out=$out" ;;
|
|
646
|
+
esac
|
|
647
|
+
[ "$rc" = "0" ] && t_ok "...and the caller still gets exit 0, as the CLI would give" \
|
|
648
|
+
|| t_fail "stream error exit status" "rc=$rc"
|
|
649
|
+
|
|
650
|
+
# A stream that merely MENTIONS a limit mid-run and then completes must be left alone:
|
|
651
|
+
# re-running it would throw away a finished task.
|
|
652
|
+
rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited; : > "$ACC/selection.log"
|
|
653
|
+
out="$(claude -p --model claude-fable-5 hello < /dev/null 2>/dev/null)"
|
|
654
|
+
grep -q "retry from=" "$ACC/selection.log" 2>/dev/null \
|
|
655
|
+
&& t_fail "spurious retry" "a healthy run was retried" \
|
|
656
|
+
|| t_ok "a healthy run is never retried"
|
|
657
|
+
rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited
|
|
658
|
+
|
|
604
659
|
# ---- 12b. pipe stdin skips retry buffering but passes bytes through -----------
|
|
605
660
|
out="$(printf 'pipe-data' | claude -p --echo-stdin 2>/dev/null)"
|
|
606
661
|
[ "$out" = "pipe-data" ] && t_ok "pipe stdin passes through (no retry buffering)" || t_fail "pipe stdin passthrough" "got: $out"
|
|
@@ -2489,11 +2544,30 @@ done
|
|
|
2489
2544
|
{ [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ] && [ $((hits1+hits2)) -eq 40 ]; } \
|
|
2490
2545
|
&& t_ok "codex: equal scores spread randomly (acct-01=$hits1 acct-02=$hits2)" \
|
|
2491
2546
|
|| t_fail "codex tie spreading" "acct-01=$hits1 acct-02=$hits2 (want both >0, total 40)"
|
|
2492
|
-
#
|
|
2547
|
+
# Unknown telemetry ranks behind every truthful reading, never as neutral or free.
|
|
2493
2548
|
printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$CX/acct-01/limits.json"
|
|
2494
2549
|
cxlj 30 30 30 > "$CX/acct-02/limits.json"
|
|
2495
2550
|
out="$(codex 2>&1)"
|
|
2496
2551
|
check "codex: stale 1% loses to fresh 30%" "CFG=acct-02" "$out"
|
|
2552
|
+
cxlj 88 88 88 > "$CX/acct-01/limits.json"
|
|
2553
|
+
rm -f "$CX/acct-02/limits.json" "$CX/.last-pick"
|
|
2554
|
+
all1=1
|
|
2555
|
+
for _ in $(seq 1 15); do
|
|
2556
|
+
case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
2557
|
+
done
|
|
2558
|
+
[ "$all1" = "1" ] && t_ok "codex: unknown telemetry never beats known 88%" \
|
|
2559
|
+
|| t_fail "codex unknown telemetry ranking" "the unknown account beat truthful 88% usage"
|
|
2560
|
+
rm -f "$CX/acct-01/limits.json" "$CX/.last-pick"
|
|
2561
|
+
hits1=0; hits2=0
|
|
2562
|
+
for _ in $(seq 1 40); do
|
|
2563
|
+
case "$(codex 2>&1)" in
|
|
2564
|
+
*CFG=acct-01*) hits1=$((hits1+1)) ;;
|
|
2565
|
+
*CFG=acct-02*) hits2=$((hits2+1)) ;;
|
|
2566
|
+
esac
|
|
2567
|
+
done
|
|
2568
|
+
{ [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
|
|
2569
|
+
&& t_ok "codex: an entirely unknown pool still fails open" \
|
|
2570
|
+
|| t_fail "codex unknown pool fail-open" "acct-01=$hits1 acct-02=$hits2"
|
|
2497
2571
|
rm -f "$CX"/acct-*/limits.json
|
|
2498
2572
|
|
|
2499
2573
|
# ---- C4. pin -----------------------------------------------------------------------
|