Compare commits
8 Commits
ff670b78e6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 42d9758ecc | |||
| b34d5b1d97 | |||
| ff86eeff4b | |||
| ff30800337 | |||
| 319db74bce | |||
| f04e24b64d | |||
| 9005edd58c | |||
| 5abdee0d4b |
@@ -54,7 +54,7 @@ blink=yes
|
||||
[touch]
|
||||
# long-press-delay=400
|
||||
|
||||
[colors-dark]
|
||||
[colors]
|
||||
alpha=0.8
|
||||
background=282828
|
||||
foreground=ebdbb2
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"margin-bottom": 16,
|
||||
"margin-left": 16,
|
||||
"margin-right": 16,
|
||||
"modules-left": ["gamemode", "battery", "temperature", "cpu", "memory", "network"],
|
||||
"modules-left": ["gamemode", "battery", "temperature", "cpu", "memory", "custom/codex-primary", "custom/codex-secondary", "network"],
|
||||
"modules-center": [],
|
||||
"modules-right": ["mpris", "pulseaudio", "custom/output-device", "backlight", "idle_inhibitor", "clock"],
|
||||
"clock": {
|
||||
@@ -155,6 +155,22 @@
|
||||
"exec": "flatpak remote-ls --updates --app | wc -l",
|
||||
"exec-if": "test $(flatpak remote-ls --updates --app | wc -l) -gt 0"
|
||||
},
|
||||
"custom/codex-primary": {
|
||||
"interval": 15,
|
||||
"return-type": "json",
|
||||
"exec": "$HOME/.config/waybar/scripts/openai-rate.py --window primary",
|
||||
"exec-if": "test -r ~/.codex/auth.json",
|
||||
"escape": false,
|
||||
"format": " {}"
|
||||
},
|
||||
"custom/codex-secondary": {
|
||||
"interval": 15,
|
||||
"return-type": "json",
|
||||
"exec": "$HOME/.config/waybar/scripts/openai-rate.py --window secondary",
|
||||
"exec-if": "test -r ~/.codex/auth.json",
|
||||
"escape": false,
|
||||
"format": " {}"
|
||||
},
|
||||
"custom/backup": {
|
||||
"interval": 60,
|
||||
"format": "",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_BASE_URL = "https://chatgpt.com/backend-api"
|
||||
|
||||
|
||||
def waybar(text, klass="good", tooltip=None):
|
||||
print(json.dumps({"text": text, "class": klass, "tooltip": tooltip or text}))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def unknown(tooltip):
|
||||
waybar('unk <span font-size="7pt">--</span>', "good", tooltip)
|
||||
|
||||
|
||||
def load_auth():
|
||||
auth_path = Path(os.environ.get("OPENAI_AUTH_FILE", "~/.codex/auth.json")).expanduser()
|
||||
auth = {}
|
||||
if auth_path.is_file():
|
||||
try:
|
||||
auth = json.loads(auth_path.read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
waybar("AI auth unreadable", "critical", f"Could not parse {auth_path}")
|
||||
|
||||
tokens = auth.get("tokens") or {}
|
||||
access_token = tokens.get("access_token")
|
||||
account_id = tokens.get("account_id")
|
||||
if access_token:
|
||||
return access_token, account_id, "~/.codex/auth.json:tokens.access_token"
|
||||
|
||||
api_key = auth.get("OPENAI_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
||||
if api_key:
|
||||
source = "~/.codex/auth.json:OPENAI_API_KEY" if auth.get("OPENAI_API_KEY") else "env:OPENAI_API_KEY"
|
||||
return api_key, account_id, source
|
||||
|
||||
waybar(
|
||||
"AI auth missing",
|
||||
"warning",
|
||||
"No Codex access token found in ~/.codex/auth.json",
|
||||
)
|
||||
|
||||
|
||||
def usage_url():
|
||||
base = os.environ.get("CODEX_RATE_BASE_URL") or os.environ.get("OPENAI_BASE_URL") or DEFAULT_BASE_URL
|
||||
base = base.rstrip("/")
|
||||
if base.endswith("/backend-api/codex"):
|
||||
base = base.removesuffix("/codex")
|
||||
if base.startswith(("https://chatgpt.com", "https://chat.openai.com")) and "/backend-api" not in base:
|
||||
base += "/backend-api"
|
||||
return f"{base}/wham/usage" if "/backend-api" in base else f"{base}/api/codex/usage"
|
||||
|
||||
|
||||
def fetch_usage(token, account_id):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "codex-waybar-rate",
|
||||
}
|
||||
if account_id:
|
||||
headers["ChatGPT-Account-ID"] = account_id
|
||||
|
||||
req = urllib.request.Request(usage_url(), headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as res:
|
||||
return json.load(res)
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:240]
|
||||
unknown(f"Usage endpoint returned HTTP {exc.code}\n{detail}")
|
||||
except (OSError, TimeoutError) as exc:
|
||||
unknown(f"Could not reach Codex usage endpoint\n{exc}")
|
||||
except json.JSONDecodeError as exc:
|
||||
unknown(f"Usage endpoint returned invalid JSON\n{exc}")
|
||||
|
||||
|
||||
def window_label(seconds):
|
||||
if not isinstance(seconds, int) or seconds <= 0:
|
||||
return "usage"
|
||||
|
||||
minutes = math.ceil(seconds / 60)
|
||||
if minutes < 90:
|
||||
return f"{minutes}m"
|
||||
|
||||
hours = round(minutes / 60)
|
||||
if hours < 36:
|
||||
return f"{hours}h"
|
||||
|
||||
days = round(hours / 24)
|
||||
if days < 365:
|
||||
return f"{days}d"
|
||||
|
||||
return f"{round(days / 365)}y"
|
||||
|
||||
|
||||
def reset_text(seconds):
|
||||
if not isinstance(seconds, int) or seconds < 0:
|
||||
return None
|
||||
if seconds < 90:
|
||||
return f"{seconds}s"
|
||||
minutes = round(seconds / 60)
|
||||
if minutes < 90:
|
||||
return f"{minutes}m"
|
||||
hours = round(minutes / 60)
|
||||
if hours < 48:
|
||||
return f"{hours}h"
|
||||
return f"{round(hours / 24)}d"
|
||||
|
||||
|
||||
def usage_class(used, limit_reached=False):
|
||||
if limit_reached or used >= 95:
|
||||
return "critical"
|
||||
if used >= 90:
|
||||
return "warning"
|
||||
if used >= 75:
|
||||
return "regular"
|
||||
return "good"
|
||||
|
||||
|
||||
def format_window(name, window):
|
||||
if not isinstance(window, dict):
|
||||
return None
|
||||
|
||||
used = window.get("used_percent")
|
||||
if not isinstance(used, int):
|
||||
return None
|
||||
|
||||
label = window_label(window.get("limit_window_seconds"))
|
||||
left = max(0, 100 - used)
|
||||
reset = reset_text(window.get("reset_after_seconds"))
|
||||
tooltip = f"{name} {label}: {used}% used"
|
||||
if reset:
|
||||
tooltip += f", resets in {reset}"
|
||||
return {
|
||||
"label": label,
|
||||
"left": left,
|
||||
"used": used,
|
||||
"tooltip": tooltip,
|
||||
}
|
||||
|
||||
|
||||
def output_window(data, source, selector):
|
||||
rate_limit = data.get("rate_limit") or {}
|
||||
window = format_window(selector, rate_limit.get(f"{selector}_window"))
|
||||
if not window:
|
||||
waybar("n/a", "warning", f"No {selector} usage window returned\nsource: {source}")
|
||||
|
||||
klass = usage_class(window["used"], rate_limit.get("limit_reached"))
|
||||
tooltip = [window["tooltip"], f"{window['left']}% remaining", f"source: {source}"]
|
||||
waybar(f"{window['left']}% <span font-size=\"7pt\">{window['label']}</span>", klass, "\n".join(tooltip))
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser(description="Waybar Codex rate-limit widget")
|
||||
parser.add_argument(
|
||||
"--window",
|
||||
choices=("primary", "secondary"),
|
||||
required=True,
|
||||
help="usage window to display",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
token, account_id, source = load_auth()
|
||||
data = fetch_usage(token, account_id)
|
||||
output_window(data, source, args.window)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -238,3 +238,22 @@ window#waybar.fullscreen #window {
|
||||
color: #ebdbb2;
|
||||
padding: 0 1em;
|
||||
}
|
||||
#custom-codex-primary,
|
||||
#custom-codex-secondary {
|
||||
padding: 0 1em;
|
||||
color: rgba(235, 219, 178, 0.2);
|
||||
}
|
||||
#custom-codex-primary.regular,
|
||||
#custom-codex-secondary.regular {
|
||||
color: #ebdbb2;
|
||||
}
|
||||
#custom-codex-primary.warning,
|
||||
#custom-codex-secondary.warning {
|
||||
color: #fabd2f;
|
||||
}
|
||||
#custom-codex-primary.critical,
|
||||
#custom-codex-secondary.critical {
|
||||
color: #282828;
|
||||
background: #fb4934;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user