claude-agent-sdk: expose per-bucket rate-limit utilization to headless SDK consumers
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 intoSDKRateLimitInfo. It only includesutilizationwhenA1z()/z1z()detect a bucket whose current utilization has crossed a threshold. Otherwise the returned object contains onlystatus,resetsAt, and the representativerateLimitType.- Per-bucket data (including
utilizationandresets_at) is cached in the module-localEh8, populated bySM4(). This cache is exposed only viapF1(), which is called only fromC9A()— thestatusLineJSON builder. AJ7()(thestatusLinecommand runner) is invoked from exactly one call site: auseCallbackinside 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗