diff --git a/main/modules/CodexUsage.qml b/main/modules/CodexUsage.qml index 13275be..9f6f451 100644 --- a/main/modules/CodexUsage.qml +++ b/main/modules/CodexUsage.qml @@ -17,12 +17,14 @@ ClippingRectangle { readonly property int cornerRadius: 8 property var primary: ({ "label": "5h", + "available": false, "usedPercent": 0, "resetIn": "", "resetAt": "" }) property var secondary: ({ "label": "7d", + "available": false, "usedPercent": 0, "resetIn": "", "resetAt": "" @@ -52,6 +54,9 @@ ClippingRectangle { } function resetText(windowData) { + if (!windowData.available) + return "not reported by Codex"; + return `resets in ${windowData.resetIn || "?"} at ${windowData.resetAt || "?"}`; } @@ -263,8 +268,9 @@ ClippingRectangle { UsageBar { width: parent.width label: String(detailRoot.usage.label || "") - percent: detailRoot.percent - accent: detailRoot.accent + percent: detailRoot.percent + available: Boolean(detailRoot.usage.available) + accent: detailRoot.accent fillColor: detailRoot.fillColor } @@ -282,6 +288,7 @@ ClippingRectangle { required property string label required property real percent + required property bool available required property string accent required property string fillColor property int barHeight: 22 @@ -302,7 +309,7 @@ ClippingRectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom - width: Math.max(3, parent.width * rowRoot.percent / 100) + width: rowRoot.available ? Math.max(3, parent.width * rowRoot.percent / 100) : 0 radius: 0 color: rowRoot.fillColor } @@ -329,7 +336,7 @@ ClippingRectangle { font.family: theme.fontFamily font.pixelSize: rowRoot.fontPixelSize horizontalAlignment: Text.AlignRight - text: `${Math.round(rowRoot.percent)}%` + text: rowRoot.available ? `${Math.round(rowRoot.percent)}%` : "--" } } diff --git a/main/scripts/codex_usage.py b/main/scripts/codex_usage.py index 5be62d5..06b1296 100755 --- a/main/scripts/codex_usage.py +++ b/main/scripts/codex_usage.py @@ -107,12 +107,13 @@ def call_rate_limits(): def usage_window(label, window, now): + window = window or {} try: - used = int(round(float((window or {}).get("usedPercent", 0)))) + used = int(round(float(window.get("usedPercent", 0)))) except (TypeError, ValueError): used = 0 - resets_at = (window or {}).get("resetsAt") + resets_at = window.get("resetsAt") try: reset_dt = ( datetime.datetime.fromtimestamp(int(resets_at), datetime.timezone.utc) @@ -124,6 +125,7 @@ def usage_window(label, window, now): return { "label": label, + "available": bool(window), "usedPercent": max(0, min(100, used)), "resetIn": human_delta(reset_dt, now) if reset_dt else "", "resetAt": reset_dt.astimezone().strftime("%H:%M" if label == "5h" else "%d.%m") @@ -132,6 +134,26 @@ def usage_window(label, window, now): } +def rate_limit_windows(limits, now): + windows = {"5h": usage_window("5h", {}, now), "7d": usage_window("7d", {}, now)} + for window in (limits.get("primary"), limits.get("secondary")): + if not window: + continue + + try: + duration = int(window.get("windowDurationMins")) + except (TypeError, ValueError): + continue + + # Codex names these primary/secondary, but their positions are not stable. + if duration == 300: + windows["5h"] = usage_window("5h", window, now) + elif duration == 10080: + windows["7d"] = usage_window("7d", window, now) + + return windows + + def main(): try: limits, err = call_rate_limits() @@ -147,8 +169,9 @@ def main(): return now = datetime.datetime.now(datetime.timezone.utc) - primary = usage_window("5h", limits.get("primary") or {}, now) - secondary = usage_window("7d", limits.get("secondary") or {}, now) + windows = rate_limit_windows(limits, now) + primary = windows["5h"] + secondary = windows["7d"] max_pct = max(primary["usedPercent"], secondary["usedPercent"]) state = "critical" if max_pct >= 90 else "warning" if max_pct >= 66 else "normal"