Skip to content

feat(usage): read rate_limits from stdin instead of calling Anthropic API#105

Open
yearth wants to merge 3 commits into
Haleclipse:masterfrom
yearth:feat/usage-from-stdin
Open

feat(usage): read rate_limits from stdin instead of calling Anthropic API#105
yearth wants to merge 3 commits into
Haleclipse:masterfrom
yearth:feat/usage-from-stdin

Conversation

@yearth
Copy link
Copy Markdown

@yearth yearth commented Mar 23, 2026

Background

Claude Code passes rate limit data directly through stdin on every render:

{
  "rate_limits": {
    "five_hour": { "used_percentage": 75.0, "resets_at": 1711270800 },
    "seven_day":  { "used_percentage": 45.0, "resets_at": 1711876200 }
  }
}

The current UsageSegment ignores this and instead makes an outbound HTTP request to api.anthropic.com/api/oauth/usage on every render (with a file-based cache to reduce frequency). This works, but it has real costs.

Problems with the current approach

Latency on cache miss: When the cache expires, the segment blocks on a network request before the statusline can render. On slow connections or when the API is temporarily unreachable, this causes a noticeable delay or a missing segment.

Credential dependency: The segment reads an OAuth token from ~/.claude/.credentials.json. Users who authenticate via SSO, enterprise proxies, or non-standard setups may not have this token available, causing the segment to silently disappear.

Stale data: The cache has a fixed TTL (default 300s). The API response reflects usage at the time of the last request, not the current moment. The stdin value is computed by Claude Code itself and is always current.

Unnecessary complexity: The implementation includes proxy detection, npm version querying for the User-Agent header, cache file management, and OAuth token parsing — none of which are needed if the data is already in stdin.

This PR

  • Add rate_limits field to InputData (parsed from stdin, fully optional for backward compatibility)
  • UsageSegment::collect now tries stdin first via collect_from_stdin
  • If rate_limits is absent (older Claude Code versions), it falls back to collect_from_api — the existing behavior is fully preserved
  • get_circle_icon now returns &'static str to avoid per-render allocations

Result

For users on a current Claude Code version: zero network calls, zero credential reads, always-fresh data, instant render.

For users on older versions: identical behavior to before.

Tests

5 unit tests covering stdin parsing, clamping, missing data, non-finite values, and icon boundaries.

Summary by Sourcery

Read Claude Code rate limit usage from stdin when available and fall back to the existing Anthropic usage API, keeping the Usage segment behavior compatible with older clients.

New Features:

  • Support stdin-provided rate limit data on the Usage segment via new rate_limits fields in the input configuration types.

Enhancements:

  • Prefer stdin rate limit data over network calls in the Usage segment, with a safer cache validity check and shared segment data builder.
  • Avoid per-render allocations for usage icons by returning static string slices.
  • Improve reset time formatting utilities to support both Unix timestamps and RFC 3339 strings.

Tests:

  • Add unit tests covering stdin rate limit parsing, clamping and missing data handling, circle icon boundaries, and basic Usage segment rendering from stdin.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 23, 2026

Reviewer's Guide

UsageSegment now preferentially consumes rate limit data passed via stdin (InputData.rate_limits), falling back to the existing Anthropic OAuth usage API path only when stdin data is unavailable, while also optimizing icon generation and refining time formatting and cache validation.

Sequence diagram for UsageSegment collect with stdin and API fallback

sequenceDiagram
    actor User
    participant ClaudeCode as ClaudeCode_process
    participant Stdin as Stdin_Input
    participant Client as UsageSegment
    participant Config as Config_loader
    participant Cache as FileCache
    participant Api as Anthropic_API

    User->>ClaudeCode: Request statusline render
    ClaudeCode->>Stdin: Write InputData (including rate_limits)
    ClaudeCode->>Client: collect(input)

    activate Client
    Client->>Client: collect_from_stdin(input)
    alt rate_limits present and valid
        Client->>Client: parse_utilization(five_hour)
        Client->>Client: parse_utilization(seven_day)
        Client->>Client: format_reset_time_unix(resets_at)
        Client->>Client: build_segment_data()
        Client-->>ClaudeCode: SegmentData
    else no usable stdin rate_limits
        Client->>Client: collect_from_api()
        Client->>Config: load()
        Config-->>Client: segment_config
        Client->>Cache: load_cache()
        Cache-->>Client: ApiUsageCache?
        Client->>Client: is_cache_valid(cache, cache_duration)
        alt cache valid
            Client->>Client: build_segment_data(from cache)
            Client-->>ClaudeCode: SegmentData
        else cache missing or invalid
            Client->>Api: fetch_api_usage(token)
            alt API success
                Api-->>Client: ApiUsageResponse
                Client->>Client: format_reset_time_rfc3339(resets_at)
                Client->>Cache: save_cache(new ApiUsageCache)
                Client->>Client: build_segment_data(from API)
                Client-->>ClaudeCode: SegmentData
            else API failure
                alt cached_data exists
                    Client->>Client: build_segment_data(from stale cache)
                    Client-->>ClaudeCode: SegmentData
                else no cache
                    Client-->>ClaudeCode: None
                end
            end
        end
    end
    deactivate Client
Loading

Class diagram for updated usage rate limit types and UsageSegment

classDiagram
    direction LR

    class InputData {
        +Model model
        +Workspace workspace
        +String transcript_path
        +Cost cost
        +OutputStyle output_style
        +RateLimits rate_limits
    }

    class RateLimits {
        +RateLimitPeriod five_hour
        +RateLimitPeriod seven_day
    }

    class RateLimitPeriod {
        +f64 used_percentage
        +i64 resets_at
    }

    class Segment {
        <<interface>> Segment
        +collect(input InputData) SegmentData
        +id() SegmentId
    }

    class UsageSegment {
        +new() UsageSegment
        +collect(input InputData) SegmentData
        -collect_from_stdin(input InputData) SegmentData
        -collect_from_api() SegmentData
        -build_segment_data(five_hour_util f64, seven_day_util f64, reset_str String) SegmentData
        -parse_utilization(period RateLimitPeriod) f64
        -get_circle_icon(utilization f64) String
        -format_reset_time_unix(ts i64) String
        -format_reset_time_rfc3339(reset_time_str String) String
        -get_cache_path() PathBuf
        -load_cache() ApiUsageCache
        -save_cache(cache ApiUsageCache)
        -is_cache_valid(cache ApiUsageCache, cache_duration u64) bool
        -get_claude_code_version() String
        -get_proxy_from_settings() String
        -fetch_api_usage(api_base_url String, token String, timeout u64) ApiUsageResponse
    }

    class SegmentData {
        +String primary
        +String secondary
        +HashMap~String, String~ metadata
    }

    class ApiUsageResponse {
        +ApiUsagePeriod five_hour
        +ApiUsagePeriod seven_day
    }

    class ApiUsagePeriod {
        +f64 utilization
        +String resets_at
    }

    class ApiUsageCache {
        +f64 five_hour_utilization
        +f64 seven_day_utilization
        +String resets_at
        +String cached_at
    }

    Segment <|.. UsageSegment

    InputData --> RateLimits : has
    RateLimits --> RateLimitPeriod : has

    UsageSegment --> SegmentData : builds
    UsageSegment --> ApiUsageResponse : uses
    UsageSegment --> ApiUsageCache : caches
    UsageSegment --> InputData : reads
    ApiUsageResponse --> ApiUsagePeriod : contains
Loading

File-Level Changes

Change Details Files
Prefer stdin-provided rate limit data for UsageSegment, with API usage as a transparent fallback.
  • Add optional RateLimits and RateLimitPeriod types to InputData to represent five-hour and seven-day rate limit windows coming from stdin.
  • Implement UsageSegment::collect_from_stdin to read and validate rate_limits from InputData, choosing a display window and reset time from the available periods.
  • Update Segment::collect implementation for UsageSegment to first attempt stdin-based collection, then fall back to the legacy API-based collection path if stdin data is absent or invalid.
src/config/types.rs
src/core/segments/usage.rs
Refactor and constrain usage/time formatting logic for both stdin and API paths.
  • Introduce format_reset_time_unix to format Unix second timestamps from stdin into compact local date/hour strings.
  • Rename and scope the previous reset time formatter to format_reset_time_rfc3339 for API RFC 3339 timestamps, and route the API path through it.
  • Tighten cache validation to reject negative elapsed durations when comparing cached_at against the current time.
src/core/segments/usage.rs
Optimize usage icon and metadata construction while preserving visual behavior.
  • Change get_circle_icon to return a &'static str instead of allocating a new String on each call, keeping the existing percentage buckets intact.
  • Extract common SegmentData assembly into build_segment_data, used by both stdin and API paths, centralizing primary/secondary text and metadata population.
  • Ensure the dynamic_icon metadata stores the static icon string while primary remains the rounded five-hour utilization percentage string.
src/core/segments/usage.rs
Add focused unit tests for stdin behavior, utilization parsing, and icon boundaries.
  • Add tests for collect_from_stdin covering normal usage, clamping utilization over 100, missing rate_limits, only-seven-day data, and both-periods-missing cases.
  • Test parse_utilization to discard non-finite values such as NaN.
  • Test get_circle_icon boundary behavior at key utilization percentages (0%, 50–51%, 100%).
src/core/segments/usage.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In fetch_api_usage the new let _ = credentials::get_oauth_token(); call is redundant since the OAuth token is already passed in as a parameter and the result is discarded; consider removing this to avoid confusion and an extra credentials lookup.
  • collect_from_api takes an input: &InputData parameter that is never used; you can drop this argument (and its uses at the call site) to keep the API cleaner and avoid unused-parameter warnings.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `fetch_api_usage` the new `let _ = credentials::get_oauth_token();` call is redundant since the OAuth token is already passed in as a parameter and the result is discarded; consider removing this to avoid confusion and an extra credentials lookup.
- `collect_from_api` takes an `input: &InputData` parameter that is never used; you can drop this argument (and its uses at the call site) to keep the API cleaner and avoid unused-parameter warnings.

## Individual Comments

### Comment 1
<location path="src/core/segments/usage.rs" line_range="181" />
<code_context>
+    fn fetch_api_usage(
</code_context>
<issue_to_address>
**suggestion:** Extra `get_oauth_token` call in `fetch_api_usage` appears redundant and adds an unnecessary dependency on `credentials` here.

Inside `fetch_api_usage`, the new `let _ = credentials::get_oauth_token();` doesn’t change behavior: the passed-in `token` is what’s used for the `Authorization` header. This call just adds an extra dependency and potential failure/log path unrelated to the actual token in use. Unless you need a specific side effect from `get_oauth_token`, consider removing it so `fetch_api_usage` depends only on its `token` argument.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/core/segments/usage.rs Outdated
@yearth
Copy link
Copy Markdown
Author

yearth commented Mar 23, 2026

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In collect_from_stdin, treating missing seven_day.used_percentage as 0.0 and then requiring seven_day_util > 0.0 to render when five_hour is absent can suppress the segment (and force an API fallback) even when valid 0% usage is reported; consider keeping Option<f64> for seven-day utilization through the selection logic so that Some(0.0) is distinguishable from None.
  • Similarly, defaulting a missing seven_day utilization to 0.0 before passing it into build_segment_data means consumers of metadata["seven_day_utilization"] cannot tell the difference between a truly missing value and a real 0% usage; if that distinction matters, consider storing the raw Option (e.g., via a separate presence flag) instead of conflating them.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `collect_from_stdin`, treating missing `seven_day.used_percentage` as `0.0` and then requiring `seven_day_util > 0.0` to render when `five_hour` is absent can suppress the segment (and force an API fallback) even when valid 0% usage is reported; consider keeping `Option<f64>` for seven-day utilization through the selection logic so that `Some(0.0)` is distinguishable from `None`.
- Similarly, defaulting a missing `seven_day` utilization to `0.0` before passing it into `build_segment_data` means consumers of `metadata["seven_day_utilization"]` cannot tell the difference between a truly missing value and a real 0% usage; if that distinction matters, consider storing the raw `Option` (e.g., via a separate presence flag) instead of conflating them.

## Individual Comments

### Comment 1
<location path="src/core/segments/usage.rs" line_range="238" />
<code_context>
+            .unwrap_or(0.0);
+
+        // Require at least one valid utilization value to show the segment
+        let display_util = five_hour_util.or(if seven_day_util > 0.0 { Some(seven_day_util) } else { None })?;
+
+        let reset_str = rate_limits
</code_context>
<issue_to_address>
**issue:** The fallback to seven_day utilization skips valid 0% values when five_hour is missing.

In the case where `five_hour_util` is `None` and `seven_day_util` is `Some(0.0)`, this returns `None` and hides the segment, making `0%` usage indistinguishable from “no data”. If `0%` is a valid state to display, consider using the presence of the `Option` (e.g. `if let Some(v) = seven_day_util_opt { ... }`) rather than `> 0.0`, or track seven-day availability separately from its numeric value.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

.unwrap_or(0.0);

// Require at least one valid utilization value to show the segment
let display_util = five_hour_util.or(if seven_day_util > 0.0 { Some(seven_day_util) } else { None })?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The fallback to seven_day utilization skips valid 0% values when five_hour is missing.

In the case where five_hour_util is None and seven_day_util is Some(0.0), this returns None and hides the segment, making 0% usage indistinguishable from “no data”. If 0% is a valid state to display, consider using the presence of the Option (e.g. if let Some(v) = seven_day_util_opt { ... }) rather than > 0.0, or track seven-day availability separately from its numeric value.

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.

1 participant