tokenmaxxing 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/PKG-INFO +1 -1
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/pyproject.toml +1 -1
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/src/tokenmaxxing/__init__.py +1 -1
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/src/tokenmaxxing/app.py +73 -26
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/.gitignore +0 -0
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/LICENSE +0 -0
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/README.md +0 -0
- {tokenmaxxing-0.2.0 → tokenmaxxing-0.2.1}/src/tokenmaxxing/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenmaxxing
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Menu bar app showing your live Claude Code session and weekly usage as a colored progress bar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alvations/tokenmaxxing
|
|
6
6
|
Project-URL: Repository, https://github.com/alvations/tokenmaxxing
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tokenmaxxing"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Menu bar app showing your live Claude Code session and weekly usage as a colored progress bar."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -74,7 +74,12 @@ APP_NAME = "Claude"
|
|
|
74
74
|
LOADING_TEXT = "loading…"
|
|
75
75
|
TIMER_INTERVAL = 1
|
|
76
76
|
|
|
77
|
-
REFRESH_SECONDS =
|
|
77
|
+
REFRESH_SECONDS = 1800 # 30 min: /api/oauth/usage tolerates ~hourly polling but trips
|
|
78
|
+
# at ~5 min cadence with a sticky multi-hour cooldown. 30 min keeps
|
|
79
|
+
# us well below the throttle threshold; on-demand menu opens still
|
|
80
|
+
# refresh instantly.
|
|
81
|
+
SUSTAINED_429_THRESHOLD = 3 # after this many consecutive 429s, fall back to /v1/messages
|
|
82
|
+
# headers (which cost ~10 tokens/poll) until /usage recovers
|
|
78
83
|
STALE_AFTER_SECONDS = 600 # 10 min: only mark cached data "(stale)" past this age — one
|
|
79
84
|
# missed poll cycle (~5 min) shouldn't trigger the warning
|
|
80
85
|
REFRESH_MAX_BACKOFF = 1800 # Max backoff: 30 minutes
|
|
@@ -480,22 +485,70 @@ def _epoch_to_iso(epoch: float) -> str:
|
|
|
480
485
|
return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
481
486
|
|
|
482
487
|
|
|
483
|
-
def fetch_usage(oauth_data: Optional[dict] = None):
|
|
488
|
+
def fetch_usage(oauth_data: Optional[dict] = None, allow_messages_fallback: bool = False):
|
|
484
489
|
"""Returns (payload_dict, error_str, is_rate_limited, retry_after_seconds).
|
|
485
490
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
491
|
+
Primary path: /api/oauth/usage — token-free, same endpoint the official
|
|
492
|
+
Claude Code client uses (see claude-code submodule services/api/usage.ts).
|
|
493
|
+
Honors the server's Retry-After on 429 so we never poll inside the
|
|
494
|
+
cooldown window.
|
|
495
|
+
|
|
496
|
+
Fallback: if the caller passes allow_messages_fallback=True (the worker
|
|
497
|
+
flips this on after SUSTAINED_429_THRESHOLD consecutive 429s), we make
|
|
498
|
+
one 1-token /v1/messages probe and parse the anthropic-ratelimit-unified-*
|
|
499
|
+
response headers — same data, but costs ~10 tokens per call. Used only to
|
|
500
|
+
rescue the menu from a stuck cooldown; the primary path resumes as soon
|
|
501
|
+
as /usage stops 429ing.
|
|
493
502
|
"""
|
|
494
503
|
token = _lift_claude_env_token()
|
|
495
504
|
if not token and oauth_data:
|
|
496
505
|
token = oauth_data.get("accessToken")
|
|
497
506
|
if not token:
|
|
498
507
|
return None, "no Claude Code token (start a claude session)", False, None
|
|
508
|
+
payload, err, is_rate_limited, retry_after = _fetch_usage_oauth(token)
|
|
509
|
+
if payload is not None:
|
|
510
|
+
return payload, None, False, None
|
|
511
|
+
if is_rate_limited and allow_messages_fallback:
|
|
512
|
+
msg_payload, msg_err, msg_is_rate, msg_retry = _fetch_usage_messages(token)
|
|
513
|
+
if msg_payload is not None:
|
|
514
|
+
return msg_payload, None, False, None
|
|
515
|
+
# Fallback also failed — surface the /usage 429 since that's the path
|
|
516
|
+
# we want to recover, not the messages probe failure.
|
|
517
|
+
return None, err, is_rate_limited, retry_after
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _fetch_usage_oauth(token: str):
|
|
521
|
+
"""Hit /api/oauth/usage. Zero token cost. Throttled if polled too often."""
|
|
522
|
+
req = urllib.request.Request(
|
|
523
|
+
USAGE_URL,
|
|
524
|
+
headers={
|
|
525
|
+
"Authorization": f"Bearer {token}",
|
|
526
|
+
"anthropic-beta": OAUTH_API_VERSION,
|
|
527
|
+
"User-Agent": "claude-limit-app/1.0",
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
try:
|
|
531
|
+
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp:
|
|
532
|
+
return json.loads(resp.read().decode("utf-8")), None, False, None
|
|
533
|
+
except urllib.error.HTTPError as e:
|
|
534
|
+
if e.code == HTTP_STATUS_RATE_LIMITED:
|
|
535
|
+
retry_after = None
|
|
536
|
+
try:
|
|
537
|
+
retry_after = int(e.headers.get("Retry-After") or 0) or None
|
|
538
|
+
except (TypeError, ValueError):
|
|
539
|
+
retry_after = None
|
|
540
|
+
return None, "rate limited — data is stale, retrying soon", True, retry_after
|
|
541
|
+
if e.code == HTTP_STATUS_UNAUTHORIZED:
|
|
542
|
+
return None, "auth expired (start a claude session)", False, None
|
|
543
|
+
return None, f"HTTP {e.code}", False, None
|
|
544
|
+
except urllib.error.URLError as e:
|
|
545
|
+
return None, f"net: {e.reason}", False, None
|
|
546
|
+
except Exception as e: # noqa: BLE001
|
|
547
|
+
return None, str(e), False, None
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _fetch_usage_messages(token: str):
|
|
551
|
+
"""Paid fallback: 1-token /v1/messages probe; read rate-limit headers."""
|
|
499
552
|
body = json.dumps({
|
|
500
553
|
"model": PROBE_MODEL,
|
|
501
554
|
"max_tokens": 1,
|
|
@@ -517,25 +570,12 @@ def fetch_usage(oauth_data: Optional[dict] = None):
|
|
|
517
570
|
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp:
|
|
518
571
|
return _payload_from_ratelimit_headers(resp.headers), None, False, None
|
|
519
572
|
except urllib.error.HTTPError as e:
|
|
520
|
-
# Even error responses (e.g. 529 overload) carry the ratelimit headers
|
|
521
|
-
# — extract them when we can so a brief upstream blip still updates
|
|
522
|
-
# the menu.
|
|
523
|
-
if e.code == HTTP_STATUS_RATE_LIMITED:
|
|
524
|
-
payload = _payload_from_ratelimit_headers(e.headers)
|
|
525
|
-
retry_after = None
|
|
526
|
-
try:
|
|
527
|
-
retry_after = int(e.headers.get("Retry-After") or 0) or None
|
|
528
|
-
except (TypeError, ValueError):
|
|
529
|
-
retry_after = None
|
|
530
|
-
if payload:
|
|
531
|
-
return payload, None, False, None
|
|
532
|
-
return None, "rate limited — data is stale, retrying soon", True, retry_after
|
|
533
|
-
if e.code == HTTP_STATUS_UNAUTHORIZED:
|
|
534
|
-
return None, "auth expired (start a claude session)", False, None
|
|
535
573
|
payload = _payload_from_ratelimit_headers(e.headers)
|
|
536
574
|
if payload:
|
|
537
575
|
return payload, None, False, None
|
|
538
|
-
|
|
576
|
+
if e.code == HTTP_STATUS_UNAUTHORIZED:
|
|
577
|
+
return None, "auth expired (start a claude session)", False, None
|
|
578
|
+
return None, f"HTTP {e.code} on /v1/messages fallback", False, None
|
|
539
579
|
except urllib.error.URLError as e:
|
|
540
580
|
return None, f"net: {e.reason}", False, None
|
|
541
581
|
except Exception as e: # noqa: BLE001
|
|
@@ -760,7 +800,14 @@ class ClaudeMonitorApp(rumps.App):
|
|
|
760
800
|
continue
|
|
761
801
|
|
|
762
802
|
oauth_data = _get_oauth_data()
|
|
763
|
-
|
|
803
|
+
# After SUSTAINED_429_THRESHOLD consecutive /api/oauth/usage 429s,
|
|
804
|
+
# unstick the menu by falling back to the paid /v1/messages
|
|
805
|
+
# header path. We don't enable the fallback by default because it
|
|
806
|
+
# costs ~10 tokens/poll — we only pay that when the free endpoint
|
|
807
|
+
# is genuinely stuck in a multi-hour cooldown.
|
|
808
|
+
allow_fallback = consecutive_failures >= SUSTAINED_429_THRESHOLD
|
|
809
|
+
payload, err, is_rate_limited, retry_after = fetch_usage(
|
|
810
|
+
oauth_data, allow_messages_fallback=allow_fallback)
|
|
764
811
|
# Drop any Refresh-now clicks that arrived during the poll — the
|
|
765
812
|
# in-flight poll already satisfies them, and re-polling immediately
|
|
766
813
|
# can trip the OAuth endpoint's rate limit.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|