claude-agent-sdk: expose per-bucket rate-limit utilization to headless SDK consumers

Resolved 💬 1 comment Opened Apr 18, 2026 by yakrwarebot[bot] Closed May 26, 2026

Summary

@anthropic-ai/claude-agent-sdk emits rate_limit_event messages carrying SDKRateLimitInfo, but in normal operation (status: "allowed") these events omit the utilization field. As a result, headless SDK consumers cannot display live per-bucket usage percentages (5-hour, 7-day Opus, 7-day Sonnet) for Claude.ai subscribers unless the user is already close to a warning threshold.

The underlying data is cached internally by the SDK (populated from anthropic-ratelimit-unified-* response headers on every API call) and is used by the CLI's statusLine command. However, statusLine only executes from the Claude Code TUI's React tree — it never runs in headless SDK mode. That leaves SDK integrations with no path to the data.

Reproduction

Using @anthropic-ai/claude-agent-sdk@0.2.112, authenticated as a Claude.ai subscriber, under normal low usage:

for await (const msg of query({ prompt, options })) {
  if (msg.type === "rate_limit_event") console.log(msg.rate_limit_info);
}

Observed:

{ "status": "allowed", "resetsAt": 1729281600, "rateLimitType": "five_hour" }

Expected: utilization is also present so consumers can render a percentage. Today it only appears once a bucket crosses a warning threshold.

Root cause (reference, from bundled cli.js v2.1.112)

  • bM4() parses response headers into SDKRateLimitInfo. It only includes utilization when A1z() / z1z() detect a bucket whose current utilization has crossed a threshold. Otherwise the returned object contains only status, resetsAt, and the representative rateLimitType.
  • Per-bucket data (including utilization and resets_at) is cached in the module-local Eh8, populated by SM4(). This cache is exposed only via pF1(), which is called only from C9A() — the statusLine JSON builder.
  • AJ7() (the statusLine command runner) is invoked from exactly one call site: a useCallback inside the TUI's ink/React components. It does not run in headless SDK mode, so the cached data is unreachable to SDK consumers.
  • Hooks (PreToolUse, Stop, SessionStart, …) don't receive rate-limit data in their input JSON, so HTTP hooks aren't a workaround either.

Why this matters

A common SDK use case is surfacing usage to an editor UI (Zed, VS Code, custom ACP clients) so users can see how close they are to quota while coding. Currently the 5h / 7d percentages are strictly unavailable to those clients in normal operation — which is the opposite of when you want to see them.

Proposed solutions

Option A — always include per-bucket utilization on rate_limit_event

Update bM4() (or PnK() downstream) so the emitted SDKRateLimitInfo always includes utilization for the representative bucket, and optionally attach a full snapshot:

type SDKRateLimitInfo = {
  // existing fields…
  utilization?: number;
  // new: full per-bucket snapshot of Eh8
  buckets?: Partial<Record<
    'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet',
    { utilization: number; resetsAt: number }
  >>;
};

Drop-in; preserves the existing event shape and adds the full snapshot when present.

Option B — new SDK message rate_limits_snapshot

Emit a new SDKMessage type carrying the full rate_limits object, matching what the statusLine JSON already ships:

type SDKRateLimitsSnapshotMessage = {
  type: "rate_limits_snapshot";
  rate_limits: {
    five_hour?:         { used_percentage: number; resets_at: number };
    seven_day?:         { used_percentage: number; resets_at: number };
    seven_day_opus?:    { used_percentage: number; resets_at: number };
    seven_day_sonnet?:  { used_percentage: number; resets_at: number };
  };
  uuid: UUID;
  session_id: string;
};

Fired once per API response (or whenever SM4() updates Eh8). Parity with the statusLine consumer, and keeps rate_limit_event semantics unchanged — rate_limit_event stays about state transitions, while the snapshot carries live metrics.

Either works. Option A is a one-liner; Option B is a cleaner separation. Happy to send a PR.

Affected integrations

Any SDK consumer building a usage UI, including zed-industries/claude-code-acp (context: https://github.com/zed-industries/claude-code-acp/pull/566), and likely any future @anthropic-ai/* SDK-based editor plugins.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗