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

12 KiB
Raw Blame History

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).
    • 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):

{
  "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:

{
  "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

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