claude-multiacc 2.0.22 → 2.0.24
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 +6 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/codex_reset.py +11 -21
- package/lib/codex_reset_windows.py +36 -0
- package/package.json +1 -1
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +1 -1
- package/tests/test_codex_reset_polling.py +94 -0
- package/tests/test_codex_reset_windows.py +43 -0
package/README.md
CHANGED
|
@@ -378,6 +378,12 @@ Codex-specific notes:
|
|
|
378
378
|
and writes a per-account/window idempotency key before redemption so a lost response
|
|
379
379
|
or simultaneous fleet poll cannot spend a second reset. Set
|
|
380
380
|
`CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
|
|
381
|
+
A finished-limit verdict takes precedence over a conflicting percentage. The
|
|
382
|
+
redemption key excludes unused model windows whose reset clocks move between reads.
|
|
383
|
+
Eligible accounts, including spent ones, check the credit endpoint every five
|
|
384
|
+
minutes even if the usage summary still says zero credits. A newly granted reset
|
|
385
|
+
is redeemed during that pass and clears the old limit; no-credit results are
|
|
386
|
+
checked again next pass, without waiting for the weekly reset date.
|
|
381
387
|
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
|
|
382
388
|
addon's no-API-keys rule.
|
|
383
389
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/codex_reset.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Automatic redemption of earned Codex usage-limit reset credits.
|
|
2
2
|
|
|
3
3
|
The Codex CLI exposes reset credits through the same authenticated backend as
|
|
4
|
-
usage telemetry.
|
|
5
|
-
|
|
4
|
+
usage telemetry. Each scheduled eligible usage pass reads the credit endpoint,
|
|
5
|
+
even when the usage response's embedded credit summary is empty or out of date.
|
|
6
|
+
A pending idempotency key is written
|
|
6
7
|
before the mutation so a lost response can be retried without spending twice.
|
|
7
8
|
"""
|
|
8
9
|
|
|
@@ -16,6 +17,8 @@ import urllib.parse
|
|
|
16
17
|
import urllib.request
|
|
17
18
|
import uuid
|
|
18
19
|
|
|
20
|
+
from codex_reset_windows import reset_windows
|
|
21
|
+
|
|
19
22
|
RESET_AT_USED_PERCENT = 95
|
|
20
23
|
REDEEM_COOLDOWN_SECONDS = 900
|
|
21
24
|
STATE_FILE = ".usage-reset.json"
|
|
@@ -100,21 +103,8 @@ def _pending_request(account_dir: str, now: int) -> tuple[dict, str]:
|
|
|
100
103
|
def _redeem_request_id(account_id: str, usage: dict, headers: dict, now: int) -> str:
|
|
101
104
|
"""Return one fleet-stable UUID for this account's current limit windows."""
|
|
102
105
|
subject = str(headers.get("chatgpt-account-id") or account_id)
|
|
103
|
-
resets =
|
|
104
|
-
|
|
105
|
-
while stack:
|
|
106
|
-
node = stack.pop()
|
|
107
|
-
if isinstance(node, dict):
|
|
108
|
-
value = node.get("reset_at")
|
|
109
|
-
if value is not None:
|
|
110
|
-
try:
|
|
111
|
-
resets.append(int(float(value)))
|
|
112
|
-
except (TypeError, ValueError):
|
|
113
|
-
pass
|
|
114
|
-
stack.extend(node.values())
|
|
115
|
-
elif isinstance(node, list):
|
|
116
|
-
stack.extend(node)
|
|
117
|
-
cycle = sorted(set(resets)) or [f"hour-{now // 3600}"]
|
|
106
|
+
_, resets = reset_windows(usage, RESET_AT_USED_PERCENT)
|
|
107
|
+
cycle = resets or [f"hour-{now // 3600}"]
|
|
118
108
|
seed = json.dumps({"account": subject, "resets": cycle}, sort_keys=True)
|
|
119
109
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, seed))
|
|
120
110
|
|
|
@@ -124,16 +114,16 @@ def try_auto_redeem(account_dir: str, account_id: str, used_percent: int,
|
|
|
124
114
|
now: int | None = None) -> dict:
|
|
125
115
|
"""Redeem one available reset and return a non-secret outcome document."""
|
|
126
116
|
current = int(now if now is not None else time.time())
|
|
127
|
-
|
|
117
|
+
finished, _ = reset_windows(usage, RESET_AT_USED_PERCENT)
|
|
118
|
+
if not auto_reset_enabled() or (used_percent < RESET_AT_USED_PERCENT and not finished):
|
|
128
119
|
return {"status": "not_eligible"}
|
|
129
120
|
state, disposition = _pending_request(account_dir, current)
|
|
130
121
|
if disposition == "cooldown":
|
|
131
122
|
return {"status": "cooldown", "outcome": state.get("outcome")}
|
|
132
123
|
list_url, consume_url = _reset_urls(usage_url)
|
|
133
124
|
if disposition == "new":
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return {"status": "no_credit"}
|
|
125
|
+
# Newly granted credits can precede the usage response's embedded summary.
|
|
126
|
+
# The scheduled usage cadence bounds these reads, including spent accounts.
|
|
137
127
|
try:
|
|
138
128
|
available_count, credit_id = _credit_to_redeem(_request(list_url, headers))
|
|
139
129
|
except Exception as error:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Redemption eligibility and identity use exhausted windows, never idle model clocks."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _number(value):
|
|
5
|
+
try:
|
|
6
|
+
return float(value)
|
|
7
|
+
except (TypeError, ValueError, OverflowError):
|
|
8
|
+
return 0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def reset_windows(usage: dict, threshold: int) -> tuple[bool, list[int]]:
|
|
12
|
+
finished, resets, stack = False, set(), [usage]
|
|
13
|
+
while stack:
|
|
14
|
+
node = stack.pop()
|
|
15
|
+
if isinstance(node, list):
|
|
16
|
+
stack.extend(node)
|
|
17
|
+
continue
|
|
18
|
+
if not isinstance(node, dict):
|
|
19
|
+
continue
|
|
20
|
+
hard_limit = node.get("limit_reached") is True or node.get("allowed") is False
|
|
21
|
+
finished = finished or hard_limit
|
|
22
|
+
windows = [node] if _number(node.get("used_percent")) >= threshold else []
|
|
23
|
+
if hard_limit:
|
|
24
|
+
reported = [node[key] for key in ("primary_window", "secondary_window")
|
|
25
|
+
if isinstance(node.get(key), dict)]
|
|
26
|
+
peak = max((_number(window.get("used_percent")) for window in reported), default=0)
|
|
27
|
+
windows.extend(window for window in reported if _number(window.get("used_percent")) == peak)
|
|
28
|
+
for window in windows:
|
|
29
|
+
try:
|
|
30
|
+
reset = int(float(window.get("reset_at")))
|
|
31
|
+
if reset > 0:
|
|
32
|
+
resets.add(reset)
|
|
33
|
+
except (TypeError, ValueError, OverflowError):
|
|
34
|
+
pass
|
|
35
|
+
stack.extend(node.values())
|
|
36
|
+
return finished, sorted(resets)
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tests/run-tests.sh
CHANGED
|
@@ -6558,7 +6558,7 @@ unset FAKE_KEYCHAIN_DIR
|
|
|
6558
6558
|
|
|
6559
6559
|
# The reset-credit contract is easier to prove against a stateful local HTTP server
|
|
6560
6560
|
# than file:// fixtures: it pins thresholding, credit ordering and idempotent POST retry.
|
|
6561
|
-
if CODEX_MULTIACC_AUTO_RESET=1 python3 "$REPO_DIR/tests
|
|
6561
|
+
if CODEX_MULTIACC_AUTO_RESET=1 python3 -m unittest discover -s "$REPO_DIR/tests" -p 'test_codex_reset*.py'; then
|
|
6562
6562
|
t_ok "codex: automatic earned-reset integration suite"
|
|
6563
6563
|
else
|
|
6564
6564
|
t_fail "codex automatic reset suite" "see unittest output above"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Scheduled passes discover newly granted resets despite an old usage summary."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
import test_codex_reset as fixtures
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResetPollingTest(fixtures.CodexPoolSandbox, unittest.TestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
super().setUp()
|
|
15
|
+
now = int(time.time())
|
|
16
|
+
self.server.state["usage"] = {"rate_limit": {"allowed": False, "limit_reached": True,
|
|
17
|
+
"primary_window": {"used_percent": 100, "limit_window_seconds": 604800,
|
|
18
|
+
"reset_at": now + 6 * 86400}},
|
|
19
|
+
"rate_limit_reset_credits": {"available_count": 0}}
|
|
20
|
+
self.server.state["credits"] = {"available_count": 0, "credits": []}
|
|
21
|
+
|
|
22
|
+
def run_scheduled(self, enabled="1"):
|
|
23
|
+
base = f"http://127.0.0.1:{self.server.server_port}/wham"
|
|
24
|
+
env = dict(os.environ, CODEX_ACCOUNTS_ROOT=str(self.pool), CODEX_MULTIACC_NO_SYNC="1",
|
|
25
|
+
CODEX_MULTIACC_AUTO_RESET=enabled, CODEX_MULTIACC_USAGE_URL=f"{base}/usage",
|
|
26
|
+
PYTHONDONTWRITEBYTECODE="1")
|
|
27
|
+
for key in ("CODEX_MULTIACC_MIN_FETCH", "CODEX_MULTIACC_RESET_CREDITS_URL",
|
|
28
|
+
"CODEX_MULTIACC_RESET_CONSUME_URL"):
|
|
29
|
+
env.pop(key, None)
|
|
30
|
+
result = subprocess.run([fixtures.REPO / "bin/codex-accounts", "limits", "--quiet"],
|
|
31
|
+
capture_output=True, text=True, env=env, timeout=20, check=False)
|
|
32
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
33
|
+
|
|
34
|
+
def next_pass(self):
|
|
35
|
+
path = self.pool / "acct-01/limits.json"
|
|
36
|
+
usage = json.loads(path.read_text())
|
|
37
|
+
usage["fetched_at"] = int(time.time()) - 301
|
|
38
|
+
path.write_text(json.dumps(usage))
|
|
39
|
+
|
|
40
|
+
def grant_credit(self):
|
|
41
|
+
self.server.state["credits"] = {"available_count": 1, "credits": [
|
|
42
|
+
{"id": "newly-granted", "status": "available", "expires_at": "2099-01-01T00:00:00Z"}]}
|
|
43
|
+
self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
|
|
44
|
+
|
|
45
|
+
def test_limited_account_polls_again_and_uses_a_new_credit(self):
|
|
46
|
+
self.run_scheduled()
|
|
47
|
+
self.assertEqual(self.server.state["credit_gets"], 1)
|
|
48
|
+
marker = self.pool / "acct-01/.limited"
|
|
49
|
+
self.assertTrue(marker.exists())
|
|
50
|
+
self.assertEqual(self.server.state["posts"], [])
|
|
51
|
+
self.run_scheduled()
|
|
52
|
+
self.assertEqual(self.server.state["credit_gets"], 1) # Normal fetch cadence still applies.
|
|
53
|
+
self.next_pass()
|
|
54
|
+
self.run_scheduled()
|
|
55
|
+
self.assertEqual(self.server.state["credit_gets"], 2)
|
|
56
|
+
self.assertTrue(marker.exists())
|
|
57
|
+
self.grant_credit()
|
|
58
|
+
self.next_pass()
|
|
59
|
+
self.run_scheduled()
|
|
60
|
+
self.assertEqual(self.server.state["credit_gets"], 3)
|
|
61
|
+
self.assertEqual(self.server.state["posts"][0]["credit_id"], "newly-granted")
|
|
62
|
+
self.assertFalse(marker.exists())
|
|
63
|
+
receipt = json.loads((self.pool / "acct-01/.usage-reset.json").read_text())
|
|
64
|
+
self.assertEqual(receipt["state"], "complete")
|
|
65
|
+
usage = json.loads((self.pool / "acct-01/limits.json").read_text())
|
|
66
|
+
self.assertEqual(usage["fetched_at"], 0) # Pre-reset exhaustion cannot exclude it again.
|
|
67
|
+
|
|
68
|
+
def test_malformed_usage_summary_does_not_hide_a_credit(self):
|
|
69
|
+
self.server.state["usage"]["rate_limit_reset_credits"]["available_count"] = "unknown"
|
|
70
|
+
self.grant_credit()
|
|
71
|
+
self.run_scheduled()
|
|
72
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
73
|
+
|
|
74
|
+
def test_credit_lookup_failure_preserves_limit_and_retries_next_pass(self):
|
|
75
|
+
self.server.state["credits"] = [] # An unreadable credit response is not a reset.
|
|
76
|
+
self.run_scheduled()
|
|
77
|
+
self.assertTrue((self.pool / "acct-01/.limited").exists())
|
|
78
|
+
self.assertEqual(self.server.state["posts"], [])
|
|
79
|
+
self.grant_credit()
|
|
80
|
+
self.next_pass()
|
|
81
|
+
self.run_scheduled()
|
|
82
|
+
self.assertEqual(self.server.state["credit_gets"], 2)
|
|
83
|
+
self.assertFalse((self.pool / "acct-01/.limited").exists())
|
|
84
|
+
|
|
85
|
+
def test_disabled_automation_does_not_query_or_spend_credits(self):
|
|
86
|
+
self.grant_credit()
|
|
87
|
+
self.run_scheduled(enabled="0")
|
|
88
|
+
self.assertEqual(self.server.state["credit_gets"], 0)
|
|
89
|
+
self.assertEqual(self.server.state["posts"], [])
|
|
90
|
+
self.assertTrue((self.pool / "acct-01/.limited").exists())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
unittest.main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Earned resets honor finished limits and ignore unrelated rolling windows."""
|
|
2
|
+
import time
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import test_codex_reset as fixtures
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResetWindowsTest(fixtures.CodexPoolSandbox, unittest.TestCase):
|
|
9
|
+
run_limits = fixtures.CodexResetIntegrationTest.run_limits
|
|
10
|
+
|
|
11
|
+
def test_finished_limit_redeems_even_below_numeric_reset_threshold(self):
|
|
12
|
+
now = int(time.time())
|
|
13
|
+
self.server.state["usage"] = {"rate_limit": {"allowed": False, "limit_reached": True,
|
|
14
|
+
"primary_window": {"used_percent": 94, "limit_window_seconds": 604800, "reset_at": now + 86400}},
|
|
15
|
+
"rate_limit_reset_credits": {"available_count": 1}}
|
|
16
|
+
self.server.state["credits"] = {"available_count": 1, "credits": []}
|
|
17
|
+
self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
|
|
18
|
+
result = self.run_limits()
|
|
19
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
20
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
21
|
+
|
|
22
|
+
def test_unused_model_windows_do_not_change_the_fleet_redemption_key(self):
|
|
23
|
+
now = int(time.time())
|
|
24
|
+
self.server.state["usage"] = {"rate_limit": {"allowed": True,
|
|
25
|
+
"primary_window": {"used_percent": 95, "limit_window_seconds": 604800, "reset_at": now + 86400}},
|
|
26
|
+
"additional_rate_limits": [{"limit_name": "Unused model", "rate_limit": {
|
|
27
|
+
"primary_window": {"used_percent": 0, "limit_window_seconds": 18000, "reset_at": now + 18000}}}],
|
|
28
|
+
"rate_limit_reset_credits": {"available_count": 2}}
|
|
29
|
+
self.server.state["credits"] = {"available_count": 2, "credits": []}
|
|
30
|
+
self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1}),
|
|
31
|
+
(200, {"code": "already_redeemed", "windows_reset": 1})]
|
|
32
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
33
|
+
# A fleet peer has no local receipt and sees a shifted idle-model window.
|
|
34
|
+
(self.pool / "acct-01/.usage-reset.json").unlink()
|
|
35
|
+
self.server.state["usage"]["additional_rate_limits"][0]["rate_limit"]["primary_window"]["reset_at"] += 30
|
|
36
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
37
|
+
posts = self.server.state["posts"]
|
|
38
|
+
self.assertEqual(len(posts), 2)
|
|
39
|
+
self.assertEqual(posts[0]["redeem_request_id"], posts[1]["redeem_request_id"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
unittest.main()
|