Files
small-scripts/specs/agent-subscriptions.spec.md
Paul O'Reilly 8935d58bd2 agent-subscriptions: use /api/oauth/usage and profile for Anthropic usage and plan info
Response rate-limit headers are no longer returned. Read the access token
from the Claude Code profile credentials (user:profile scope), report
5h/7d usage and resets, and add plan info with an inferred monthly renewal.

Claude-Session: https://claude.ai/code/session_01Gz3N3q53Bx9pbjB9tZT9dt
2026-10-14 16:41:22 +13:00

13 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 access token from claudeAiOauth.accessToken in $AGENT_SUBSCRIPTIONS_CLAUDE_DIR/.credentials.json (default ~/.claude-oreillyit-anthropic). This is the interactive-login credential, which carries the user:profile scope; the setup-token key in secrets/anthropic/api_key does not and gets 403 oauth_scope_insufficient. The file is read-only here: the script never refreshes it. If expiresAt has passed, Anthropic is reported unavailable with a "run claude once" hint.
  2. Probe Anthropic with anthropic-beta: oauth-2025-04-20:
    • GET https://api.anthropic.com/api/oauth/usage returns five_hour and seven_day objects with utilization (percent, 0-100) and resets_at (ISO 8601 string). The old anthropic-ratelimit-unified-* response headers are no longer returned and are not used.
    • GET https://api.anthropic.com/api/oauth/profile (best-effort) returns organization.rate_limit_tier, subscription_status, subscription_created_at. No renewal date is exposed, so next_renewal_inferred assumes a monthly cycle on the day-of-month of subscription_created_at (day clamped to 28).
  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
    • 60–80%: yellow
    • ≥ 80%: red
    • JSON output is never coloured (safe to pipe).

Reset time fields

Provider Field Format
Anthropic usage.five_hour.resets_at ISO 8601 string
Anthropic usage.seven_day.resets_at ISO 8601 string
MiniMax model_remains[general].end_time Unix epoch milliseconds
MiniMax model_remains[general].weekly_end_time Unix epoch milliseconds

Note the format difference: Anthropic emits ISO strings; 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  — ~/.claude-oreillyit-anthropic/.credentials.json (Bearer OAuth token)
               GET https://api.anthropic.com/api/oauth/usage, /api/oauth/profile
  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).

Subscription info (Anthropic)

Default output prints a line below the table: Anthropic plan: <tier> (<status>), renews ~<date> (in <rel>, inferred). JSON adds an optional subscription object to the Anthropic provider entry: plan, status, started, next_renewal_inferred (ISO 8601). The object is omitted when the profile call fails. The renewal date is inferred, not authoritative.

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

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