Skip to content

Expose /usage-equivalent rate-limit utilization for OAuth swarms #76

Description

@moodmosaic

Summary

Surface Claude Code's 5-hour and 7-day rolling-window utilization
(the data behind the interactive /usage slash command) to
claude-swarm operators running on an OAuth (Pro / Max / Max 20x)
subscription. Two deliverables:

  1. ./usage.sh — one-shot helper that prints current
    rate_limits.five_hour.used_percentage,
    rate_limits.seven_day.used_percentage, and both resets_at
    timestamps, formatted as a table (default) or JSON (--json).
    Mirrors the shape of ./costs.sh.

  2. Dashboard banner — add a single compact line to
    dashboard.sh's header, next to the model summary, showing
    5h: 42% (reset 17:34) | 7d: 15% when the swarm is using
    OAuth. Refreshes on the existing 3 s tick but throttles the
    underlying probe to once every ~60 s (probes are non-free,
    see below).

Both surfaces must be opt-in and OAuth-scoped. API-key
(auth: apikey / auth: key) swarms already get the equivalent
signal via costs.sh (dollar cost tracks real spend on flat-rate
API billing); rate_limits is absent for them and the helper
should print n/a (API-key auth) rather than probe.

Why it matters for swarms specifically

A single-user Claude Code session on Max 20x can run all day
without hitting the 5h cap. An N-agent swarm on the same
subscription cannot: empirically a 4-6 agent swarm with
effort: high or max burns through the 5-hour window in
30-60 minutes, and the first signal we get today is all N
agents simultaneously returning rate_limit_error from the
API. At that point the existing retry path in
lib/drivers/claude-code.sh:136-149 kicks in with
MAX_RETRY_WAIT, but by then the swarm is already wedged --
and if the 7-day cap gets hit, MAX_RETRY_WAIT is useless
(5h backoff isn't going to recover a 7-day reset).

With /usage visibility, operators can:

  • Pre-flight: check headroom before launching a long run
    (./usage.sh before ./launch.sh start).
  • In-flight: see the swarm crossing 75%/90% thresholds in
    the dashboard and choose to stop the numbered agents before
    the 5h reset wastes the last commits.
  • Sizing: correlate agent count × effort × duration against
    actual window burn to right-size future swarms instead of
    guessing.

The existing max-opus.json smoke-test config
(tests/configs/max-opus.json) already pins "auth": "oauth"
as a first-class swarm shape, so the target user segment is
established.

Design notes

Why we cannot invoke /usage directly

The swarm runs agents in Claude Code's headless/print mode
(claude -p --output-format stream-json --verbose,
lib/drivers/claude-code.sh:24-39). Three constraints rule
out direct support:

  1. Slash commands are TUI-only. /usage is not exposed
    as a CLI flag, and the -p prompt argument is treated as
    conversation input, not a command.

  2. stream-json does not carry rate-limit fields.
    The documented event schema is system/init, stream_event,
    assistant, result, and system/api_retry. Only
    system/api_retry carries anything rate-limit-shaped
    ("error":"rate_limit", error_status, retry_delay_ms)
    and that event fires after a 429, not proactively. None
    of the other events expose anthropic-ratelimit-unified-*.

  3. statusLine is TUI-only too. Claude Code 2.1.80 added
    rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}
    to the JSON piped to a user-configured statusLine
    command -- but statusLine only runs inside the interactive
    TUI event loop, not under -p.

  4. Upstream requests to expose this in headless were closed.
    See Expose rate limit / usage quota info in statusLine JSON input anthropics/claude-code#23843 (state=not_planned,
    2026-03-07) and #29604 (closed as duplicate; the
    statusLine-only support landed instead). We should not
    block on upstream adding a headless hook.

MVP: TUI-probe helper

A disposable container that does enter the TUI long enough
for one API roundtrip, writes the rate_limits JSON via a
statusLine script, and exits. Sketch:

usage.sh (host) -> docker run --rm \
    -v <oauth-token-injection> \
    -v <tmp>:/usage-out \
    -e SWARM_USAGE_PROBE=1 \
    <project>-agent \
    probe-usage.sh

Inside the probe container:

  1. Write ~/.claude/settings.json with a statusLine command
    that jq-emits .rate_limits as a single JSON blob to
    /usage-out/rate_limits.json.
  2. Start claude (interactive), feed it one minimal prompt
    (echo "hi" | claude --model claude-haiku-4-5 --dangerously-skip-permissions
    or equivalent cheap cash-out), wait for the statusLine to
    fire after the first API response, read the file, exit.
  3. Host parses /usage-out/rate_limits.json and prints the
    table / JSON.

Cost: one Haiku round-trip per probe (~<$0.001, a few hundred
input tokens) and ~2-4 s wall time for the probe container to
start, run, and tear down. For the dashboard loop, throttle
to one probe per 60 s; for the CLI (./usage.sh), probe once
per invocation.

Edge cases & behaviour

  • API-key auth: probe helper returns n/a (API-key auth)
    without running; costs.sh is the correct tool for that mode.
  • Mixed swarms (OAuth + API-key groups in one swarm.json):
    probe once using the OAuth credential; the window is
    account-wide, not per-swarm.
  • Older Claude Code (<2.1.80): the statusLine JSON will
    lack rate_limits; helper returns unsupported (Claude Code < 2.1.80). claude_code_version in swarm.json is the
    pin point; document the minimum.
  • Rate limits on the probe itself: if we're already at
    100% the probe's own request gets 429'd. Catch system/api_retry
    with "error":"rate_limit" from the probe's own stream-json
    and report 5h: ≥100% (limit reached) instead of spinning
    in the retry loop.
  • Probe container reuses the swarm's image so no extra
    image build is needed. Entry point is a new
    lib/probe-usage.sh alongside lib/harness.sh.

Non-goals (for this issue)

  • Proxying anthropic-ratelimit-unified-* headers. A
    MITM proxy on ANTHROPIC_BASE_URL would give us per-request
    continuous visibility without a probe, but adds a long-lived
    host-side process, cert management, and a failure mode that
    can stall the entire swarm. Worth a follow-up issue if the
    probe approach proves too coarse.
  • Per-agent /usage breakdown. The rolling window is
    account-scoped; there is no per-agent or per-session
    attribution to be had from this API.
  • /extra-usage configuration. That command configures
    pay-as-you-go overflow billing, which is interactive-only
    and orthogonal to this feature.

Acceptance criteria

  • ./usage.sh prints a table with columns: Window,
    Used %, Resets at (local time). Exits 0 on success,
    1 on probe failure.
  • ./usage.sh --json emits
    {"five_hour":{"used_percentage":…,"resets_at":…}, "seven_day":{"used_percentage":…,"resets_at":…}, "probed_at":…}.
  • On API-key auth the helper prints
    n/a (API-key auth; use ./costs.sh) and exits 0.
  • On Claude Code < 2.1.80 the helper prints
    unsupported (Claude Code < 2.1.80) and exits 0.
  • Dashboard header shows a compact rate-limit line when
    any group uses OAuth. Line is absent (not just blank)
    when all groups are API-key.
  • Unit coverage in tests/test_usage.sh: parse fixture
    statusLine JSON with rate_limits present, absent,
    partial (only five_hour), malformed; formatter
    handles each. Gate the integration test on
    CLAUDE_CODE_OAUTH_TOKEN being set so CI without
    credentials skips cleanly (same pattern as the existing
    OAuth smoke test).
  • USAGE.md gets a new Rate-limit monitoring section
    under the existing Cost tracking section. Explicitly
    document the OAuth-only scope and the Claude Code ≥ 2.1.80
    requirement.
  • README.md Drivers table gets a row note pointing at
    the new section.
  • CHANGELOG.md entry under the next version with a
    1-paragraph rationale (not just "added ./usage.sh").

References

Motivation

Claude Code's /usage command (added around v2.1.x, refined in
2.1.77+) shows subscription-plan utilization for the rolling
5-hour window and the 7-day weekly cap. It reads the
anthropic-ratelimit-unified-* headers the CLI already parses
from every API response. For Pro/Max subscribers -- who pay a
flat subscription rather than per-token -- this is the only
meaningful "how much budget is left" signal, because the
total_cost_usd we currently track and surface in the dashboard
is a synthetic API-price estimate that has no relationship to
the subscription's rolling window.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions