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:
@@ -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.0–1.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.0–1.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
|
||||
- 60–80%: 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.0–100.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
|
||||
```
|
||||
Reference in New Issue
Block a user