Files
small-scripts/specs/agent-subscriptions.spec.md
Paul O'Reilly 52eb484ef7 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
2026-08-02 21:18:02 +12:00

228 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# agent-subscriptions
## Purpose
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
```
agent-subscriptions [OPTIONS]
Options:
-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 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).
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 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. 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:
Anthropic — ~/dev/claude/secrets/anthropic/api_key (Bearer OAuth token)
GET https://api.anthropic.com/v1/models
MiniMax — ~/dev/claude/projects/agent-runtime-secrets/providers/minimax/v1/provider.sops.env (SOPS)
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 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
```
- `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, 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, 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
```