Skip to content

feat: add typed RateLimitEvent message - #648

Merged
qing-ant merged 1 commit into
mainfrom
fix/rate-limit-and-allowed-tools
Mar 12, 2026
Merged

qing-ant merged 1 commit into
mainfrom
fix/rate-limit-and-allowed-tools

Conversation

@qing-ant

@qing-ant qing-ant commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds typed parsing for rate_limit_event messages emitted by the CLI when rate limit status changes for claude.ai subscription users. After #598 stopped the parser from crashing on unknown types, these events were silently dropped — users couldn't warn before hitting limits or back off gracefully when rejected.

What's added

  • RateLimitEvent dataclass with rate_limit_info, uuid, session_id
  • RateLimitInfo dataclass with status, resets_at, rate_limit_type, utilization, overage_status, overage_resets_at, overage_disabled_reason, raw
  • RateLimitStatus ("allowed" | "allowed_warning" | "rejected") and RateLimitType literal types
  • Parser case for rate_limit_event message type
  • Exports for all the above plus StreamEvent (was in Message union but missing from __init__.py)

Field names follow Python conventions (snake_case); camelCase CLI fields are mapped in the parser. The raw field preserves the original dict including any unmodeled fields for forward compatibility.

E2E verification

Captured a real event from the CLI:

status: allowed_warning
resets_at: 1773273600
rate_limit_type: seven_day
utilization: 0.62
raw keys: ['isUsingOverage', 'rateLimitType', 'resetsAt', 'status', 'utilization']

Usage

from claude_agent_sdk import query, RateLimitEvent

async for msg in query(prompt="..."):
    if isinstance(msg, RateLimitEvent):
        if msg.rate_limit_info.status == "allowed_warning":
            print(f"Approaching limit, resets at {msg.rate_limit_info.resets_at}")
        elif msg.rate_limit_info.status == "rejected":
            print("Rate limited — backing off")

Fixes #583, #599, #601, #603

The CLI emits `rate_limit_event` when rate limit status changes for
claude.ai subscription users. After #598 stopped the parser from
crashing on unknown types, these events were silently dropped
(returning None). Users need to see these events to warn before
hitting limits or back off gracefully when rejected.

This adds:
- RateLimitEvent dataclass with rate_limit_info, uuid, session_id
- RateLimitInfo dataclass with status, resets_at, rate_limit_type,
  utilization, overage_status, overage_resets_at, overage_disabled_reason
- RateLimitStatus and RateLimitType literal types
- Parser case for rate_limit_event message type
- Exports for all the above plus StreamEvent (was missing from __init__)

Field names follow Python conventions (snake_case); camelCase CLI
fields are mapped in the parser. The .raw field preserves unmodeled
fields for forward compatibility.

E2E verified against real CLI output:
  status: allowed_warning, resets_at: 1773273600,
  rate_limit_type: seven_day, utilization: 0.62

Fixes #583, #599, #601, #603
@qing-ant
qing-ant force-pushed the fix/rate-limit-and-allowed-tools branch from caebb82 to bdc6270 Compare March 7, 2026 02:33
@qing-ant qing-ant changed the title feat: add RateLimitEvent and fix allowed_tools=[] truthiness bug feat: add typed RateLimitEvent message Mar 7, 2026
@qing-ant
qing-ant enabled auto-merge (squash) March 12, 2026 05:53
@qing-ant
qing-ant merged commit 2d5c3cb into main Mar 12, 2026
9 checks passed
@qing-ant
qing-ant deleted the fix/rate-limit-and-allowed-tools branch March 12, 2026 13:26
qing-ant added a commit that referenced this pull request Aug 11, 2026
…1196)

## Summary

TypeScript-parity gap. In streaming-input mode one connection carries
many user turns, and a `/clear` (or any other flow that discards the
transcript mid-session) resets the conversation and zeroes the running
totals reported on subsequent result messages. The CLI announces this
with a top-level frame:

```json
{"type": "conversation_reset", "new_conversation_id": "<uuid>", "uuid": "<uuid>", "session_id": "<outgoing session id>"}
```

The TypeScript SDK surfaces it as `SDKConversationResetMessage`. The
Python parser only recognized its known message types and dropped this
one through the forward-compat fallthrough in `message_parser.py`, so
Python apps never saw resets — including ones they didn't initiate — and
had no signal to snapshot totals before they zero.

- `types.py`: new `ConversationResetMessage` dataclass
(`new_conversation_id`, `uuid`, `session_id`), added to the `Message`
union next to the other top-level non-system frames (`RateLimitEvent`,
`StreamEvent`).
- `message_parser.py`: parse `type == "conversation_reset"`; missing
required field → `MessageParseError`, same as siblings.
- `__init__.py`: export.

**Compatibility note:** like `RateLimitEvent` (#648), this widens the
public `Message` union. Code that exhaustively matches on `Message` with
`assert_never` will get a new type-check error, and code that raises on
unrecognized message classes will now see a frame that was previously
dropped silently.

## Test plan

- `tests/test_message_parser.py`: parse happy path + missing-field
error.
- `e2e-tests/test_conversation_reset.py` (runs in CI against the real
CLI): open a `ClaudeSDKClient`, run one turn, send `/clear`, assert a
`ConversationResetMessage` arrives before the `/clear` turn's result,
stamped with the outgoing `session_id`, and that the following result
carries a new `session_id`. Passed locally against CLI 2.1.227.
- `ruff check`, `ruff format --check`, `mypy src/`, full `pytest tests/`
green locally.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MessageParseError on rate_limit_event crashes receive_messages() generator

2 participants