agent-subscriptions: add window_seconds and elapsed_pct to JSON output

Elapsed is null when a window has no reset timestamp (MiniMax v1 fallback)
so pacing consumers fail closed; negative reset_in_seconds clamps to 0.

Claude-Session: https://claude.ai/code/session_01YQDoWNM7XPPii28khFWoMc
This commit is contained in:
Paul O'Reilly
2026-08-02 21:18:02 +12:00
parent 448091ecf4
commit 52eb484ef7
3 changed files with 623 additions and 93 deletions

View File

@@ -2,7 +2,7 @@
## Purpose
Show live subscription usage percentages for each AI provider used by the agent runtimes system (Anthropic OAuth, MiniMax).
Show live subscription usage percentages and weekly-reset timing for each AI provider used by the agent runtimes system (Anthropic OAuth, MiniMax). Output is human-readable by default; a `--output json` mode produces a machine-consumable structure for downstream cron jobs.
## Usage
@@ -10,32 +10,57 @@ Show live subscription usage percentages for each AI provider used by the agent
agent-subscriptions [OPTIONS]
Options:
-n, --dryrun Show what would be probed without making API calls
-h, --help Show this help message and exit
-n, --dryrun Show what would be probed without making API calls
-j, --output json Emit structured JSON to stdout (plain text, no ANSI)
-h, --help Show this help message and exit
```
The default mode (no flag) prints a coloured table suitable for terminals.
## Behaviour
1. Read the Anthropic OAuth token from `~/dev/claude/secrets/anthropic/api_key` (whole-file Bearer token).
2. Probe Anthropic subscription usage:
- First attempt: `GET https://api.anthropic.com/v1/models` with `Authorization: Bearer <token>` — zero token cost; reads `anthropic-ratelimit-unified-5h-utilization` and `anthropic-ratelimit-unified-7d-utilization` response headers.
2. Probe Anthropic subscription usage and reset times:
- First attempt: `GET https://api.anthropic.com/v1/models` with `Authorization: Bearer <token>` — zero token cost.
- Read `anthropic-ratelimit-unified-5h-utilization` and `anthropic-ratelimit-unified-7d-utilization` (fractions 0.01.0; multiply by 100 for percentage).
- Read `anthropic-ratelimit-unified-5h-reset` and `anthropic-ratelimit-unified-7d-reset`. Values are **Unix epoch seconds** (integer string).
- Source: https://platform.claude.com/docs/en/api/rate-limits and https://github.com/anthropics/claude-code/issues/12829 (example values `1764554400`, `1764615600`).
- If headers absent, fall back: `POST https://api.anthropic.com/v1/messages` with model `claude-haiku-4-5-20251001`, `max_tokens=1`, message `"hi"` — same headers on response (or on the HTTPError if 429).
- Values are fractions (0.01.0); multiply by 100 for percentage.
3. Read the MiniMax API key from `~/dev/claude/projects/agent-runtime-secrets/providers/minimax/v1/provider.sops.env` via `sops --decrypt --output-type dotenv` (key: `ANTHROPIC_AUTH_TOKEN`). Uses `SOPS_AGE_KEY_FILE=~/dev/claude/secrets/sops/provider-age-key.txt`.
4. Probe MiniMax subscription usage:
- `GET https://www.minimax.io/v1/token_plan/remains` with `Authorization: Bearer <key>` and `User-Agent: curl/7.88.1` (minimax.io blocks Python-urllib).
- Extract `category_remains[]` where `category == "text_generation"`.
- `five_hour = current_interval_usage_count / current_interval_total_count × 100`
- `seven_day = current_weekly_usage_count / current_weekly_total_count × 100`
5. Display a formatted table. Each provider shows two rows (5-hour and 7-day windows). If a provider probe fails, display `UNAVAILABLE` for that provider's rows.
6. Colour-code the percentage column:
4. Probe MiniMax subscription usage and reset times:
- `GET https://www.minimax.io/v1/token_plan/remains` with `Authorization: Bearer <key>` and `User-Agent: curl/7.88.1` (minimax.io blocks Python-urllib default UA).
- For the `model_remains[].model_name == "general"` entry:
- `five_hour = 100 - current_interval_remaining_percent`
- `seven_day = 100 - current_weekly_remaining_percent`
- 5-hour reset: `end_time` (Unix epoch **milliseconds**) — divide by 1000 for seconds.
- Weekly reset: `weekly_end_time` (Unix epoch **milliseconds**).
- Source for response shape: https://github.com/Hukilow/Minimax-usage/blob/main/PLAN.md
- v1 fallback (`category_remains[]` where `category == "text_generation"`) carries no reset timestamp fields — show `UNKNOWN` reset and continue.
5. Display the formatted table (default) or JSON (`--output json`).
6. Colour-code the percentage column (default output only):
- < 60%: green
- 6080%: yellow
- ≥ 80%: red
- JSON output is **never** coloured (safe to pipe).
## Reset time fields
| Provider | Field | Format |
|-----------|---------------------------------------------|---------------------------|
| Anthropic | `anthropic-ratelimit-unified-5h-reset` | Unix epoch seconds (int) |
| Anthropic | `anthropic-ratelimit-unified-7d-reset` | Unix epoch seconds (int) |
| MiniMax | `model_remains[general].end_time` | Unix epoch **milliseconds** |
| MiniMax | `model_remains[general].weekly_end_time` | Unix epoch **milliseconds** |
Note the unit difference: Anthropic emits seconds; MiniMax emits milliseconds. The implementation normalises both to seconds.
If a reset field is absent (header missing on the Anthropic side, or the v1 fallback path on the MiniMax side), the reset cell in the table shows `UNKNOWN` and the JSON field `reset_at` is set to `null` with `reset_in_seconds: null`. `reset_in_seconds` is computed as `reset_at - now` at probe time.
## Dryrun behaviour
Prints what it would probe without reading credential files or making HTTP calls:
Prints what it would probe without reading credential files or making HTTP calls. The dryrun mode honours `--output json` and emits a JSON sample with placeholder values that match the live JSON schema (same keys, sample numbers, no API calls performed).
Default (no `--output`):
```
[dryrun] Would probe:
@@ -45,34 +70,159 @@ Prints what it would probe without reading credential files or making HTTP calls
GET https://www.minimax.io/v1/token_plan/remains
```
With `--output json` (or `--dryrun --output json`):
```json
{
"probed_at": "2026-08-01T14:23:00+00:00",
"providers": [
{
"provider": "Anthropic",
"available": true,
"windows": {
"five_hour": {
"utilization_pct": 23.4,
"reset_at": "2026-08-01T19:00:00+00:00",
"reset_in_seconds": 16620,
"window_seconds": 18000,
"elapsed_pct": 0.0767
},
"seven_day": {
"utilization_pct": 41.2,
"reset_at": "2026-08-07T23:00:00+00:00",
"reset_in_seconds": 538200,
"window_seconds": 604800,
"elapsed_pct": 0.1101
}
}
},
{
"provider": "MiniMax",
"available": true,
"windows": {
"five_hour": {
"utilization_pct": 12.1,
"reset_at": "2026-08-01T19:00:00+00:00",
"reset_in_seconds": 16620,
"window_seconds": 18000,
"elapsed_pct": 0.0767
},
"seven_day": {
"utilization_pct": 8.3,
"reset_at": "2026-08-07T23:00:00+00:00",
"reset_in_seconds": 538200,
"window_seconds": 604800,
"elapsed_pct": 0.1101
}
}
}
]
}
```
Note: `probed_at` and the reset values are placeholders — the dryrun output does not depend on the actual wall clock, only on the structure. `window_seconds` is always the fixed constant for that window key (never a placeholder); `elapsed_pct` is derived from `reset_in_seconds` and `window_seconds` so the sample stays internally consistent (shown rounded to 4 decimal places above; the implementation does not round).
## Output format
### Default table
```
Agent Subscription Usage
========================
Provider Window Usage
----------- ---------- --------
Anthropic 5-hour 23.4%
Anthropic 7-day 41.2%
MiniMax 5-hour 12.1%
MiniMax 7-day 8.3%
Provider Window Usage Resets
----------- ---------- -------- -------
Anthropic 5-hour 23.4% 4h 37m
Anthropic 7-day 41.2% 6d 8h
MiniMax 5-hour 12.1% 2h 11m
MiniMax 7-day 8.3% 4d 19h
```
Percentage column is ANSI-coloured (green/yellow/red) when output is a TTY. No colour when piped.
- `Resets` column shows **relative time until the window resets** (`1d 2h`, `12h 23m`, `45m`, `12s`).
- 7-day window: days+hours (`6d 8h`). 5-hour window: hours+minutes (`4h 37m`).
- `Resets` shows `UNKNOWN` (dim) when the underlying field is absent.
- Percentage column is ANSI-coloured (green/yellow/red) when stdout is a TTY; `Resets` is always plain.
- JSON output is always plain (no ANSI), regardless of TTY state.
### JSON (`--output json` / `-j`)
Top-level keys:
- `probed_at` (string, ISO 8601 with timezone offset, UTC if unknown): when the probe was performed.
- `providers` (array): one entry per provider in fixed order — `Anthropic` first, then `MiniMax`.
Each provider entry:
- `provider` (string): `"Anthropic"` or `"MiniMax"`.
- `available` (bool): `true` if at least one window returned data; `false` if the whole probe failed.
- On failure, add `error` (string) describing the reason and omit `windows`.
- On success, add `windows` (object) with both `five_hour` and `seven_day` sub-objects.
Each window sub-object:
- `utilization_pct` (float, may be `null`): percentage of the window already used (0.0100.0).
- `reset_at` (string ISO 8601, may be `null`): absolute wall-clock time of the next reset.
- `reset_in_seconds` (int, may be `null`): seconds from `probed_at` until `reset_at`. Downstream consumers computing "days remaining" divide this by 86400. **Clamped to a minimum of 0** — if the reset epoch is already in the past at probe time (clock skew, or probing in the same second the reset fired), the script clamps the value to `0` itself rather than emitting a negative number. This also pins `elapsed_pct` (below) to `1.0` in that case.
- `window_seconds` (int, may be `null`): the nominal length of this window in seconds — a fixed constant, not measured: `18000` for `five_hour`, `604800` for `seven_day`. These are assumed nominal lengths (Anthropic documents 5h/7d unified rate-limit windows; MiniMax v2's `end_time`/`weekly_end_time` fields are consistent with the same cadence) — the script does not attempt runtime verification against either API. `null` only when the window key itself has no defined constant (should not occur for `five_hour`/`seven_day`).
- `elapsed_pct` (float, may be `null`): fraction of the window already elapsed, computed as `1 (reset_in_seconds / window_seconds)`, clamped to `[0.0, 1.0]`. **`null` whenever `reset_in_seconds` is `null`** — i.e. whenever this window has no reset timestamp (the MiniMax v1 fallback path returns utilization with no reset fields). This is a deliberate fail-closed contract: downstream consumers (e.g. an idle-capacity scheduler pacing dispatch against elapsed window time) MUST treat `null` as "cannot pace this window" and never substitute `0.0` or `1.0`.
Example live output:
```json
{
"probed_at": "2026-08-01T14:23:00+00:00",
"providers": [
{
"provider": "Anthropic",
"available": true,
"windows": {
"five_hour": {
"utilization_pct": 23.4,
"reset_at": "2026-08-01T19:00:00+00:00",
"reset_in_seconds": 16620,
"window_seconds": 18000,
"elapsed_pct": 0.0767
},
"seven_day": {
"utilization_pct": 41.2,
"reset_at": "2026-08-07T23:00:00+00:00",
"reset_in_seconds": 538200,
"window_seconds": 604800,
"elapsed_pct": 0.1101
}
}
},
{
"provider": "MiniMax",
"available": false,
"error": "HTTP 401 Unauthorized"
}
]
}
```
The downstream consumer (cron job that drafts workload based on subscription usage thresholds) needs `utilization_pct`, `reset_in_seconds`, and `elapsed_pct` for the `seven_day` window. `utilization_pct` and `reset_in_seconds` are guaranteed present (non-null) on the success path; `elapsed_pct` is guaranteed present only when the provider's reset timestamp is available (true for both providers' `v2`/header paths; `null` for MiniMax's `v1` fallback — see Edge cases).
## Edge cases
- If the Anthropic token file does not exist, print `UNAVAILABLE` for both Anthropic rows and continue.
- If SOPS decryption fails (missing key file, wrong key, sops not installed), print `UNAVAILABLE` for both MiniMax rows and continue.
- If an API call fails for any reason, print `UNAVAILABLE` for that provider's rows and continue.
- If a utilization header is present for only one window, display `N/A` for the missing window.
- Exit 0 even if some providers are unavailable (the tool is informational).
- If the Anthropic token file does not exist, mark Anthropic `available: false`, set `error`, continue.
- If SOPS decryption fails (missing key file, wrong key, sops not installed), mark MiniMax `available: false`, set `error`, continue.
- If an API call fails for any reason, that provider is `available: false` with `error`. The other provider is still probed.
- If a utilization header is present for only one window, the other window has `utilization_pct: null`. The reset field is independent — a window can have a reset timestamp without a utilization value, or vice versa.
- If the reset field is absent for a window: `reset_at: null`, `reset_in_seconds: null`, `elapsed_pct: null`. The default table prints `UNKNOWN` in the `Resets` cell. This is the normal case for MiniMax's v1 fallback path (`category_remains[]`), which carries utilization but no reset timestamps.
- If `reset_in_seconds` would be negative (reset epoch already passed at probe time), it is clamped to `0` before being emitted — never negative in JSON output. `elapsed_pct` for that window is then `1.0` (fully elapsed), not `null`.
- `elapsed_pct` is computed only from `reset_in_seconds` and the fixed `window_seconds` constant; it does not depend on `utilization_pct`. A window can have `elapsed_pct` with `utilization_pct: null`, or vice versa.
- `window_seconds` values (`18000`, `604800`) are the same regardless of provider or probe outcome — they are compile-time constants, not derived from any API response.
- Exit 0 even if some providers are unavailable (the tool is informational). The only non-zero exit is `--help` is not requested and an unknown flag was passed.
- `--output json` and `--dryrun` compose: `--dryrun --output json` emits the dryrun JSON sample without making API calls.
## Examples
```sh
agent-subscriptions # live probe
agent-subscriptions --dryrun # preview only
agent-subscriptions --help # usage
```
agent-subscriptions # live probe, coloured table
agent-subscriptions --output json # live probe, machine-readable JSON
agent-subscriptions -j # short flag for JSON
agent-subscriptions --dryrun # preview only (table)
agent-subscriptions --dryrun -j # preview only (JSON sample)
agent-subscriptions --help # usage
```