Menu

#452 feat(llm): honor the provider's own rate-limit reset time

closed
nobody
None
2026-07-16
2026-07-16
Anonymous
No

Originally created by: Akarsh-Hegde

Summary

  • When the resolver's chosen LLM provider (Claude Code, Codex, ...) rate-limits, it previously always backed off for a flat 30 minutes before retrying.
  • The provider's own error text usually carries the real reset hint ("5-hour limit reached ∙ resets 3pm", "try again in 5 hours"). src/llm/reset_time.rs parses that (absolute clock time or relative duration) and the resolver now waits exactly that long instead of guessing.
  • Falls back to the existing flat RATE_LIMIT_BACKOFF (30 min) when the message doesn't match a recognised shape — e.g. Codex's weekly-limit message names a full date, which is deliberately out of scope.
  • Parsed waits are capped at 6h and given a 60s margin so a retry never lands exactly on the boundary.

Why

Observed live: hitting the Claude Code rate limit around 3:45pm with a reset at 4:20pm meant Meridian's worklog pipeline (which now routes through the user's own claude CLI subscription when llm_provider = "claude") was stuck on a flat 30-minute backoff — either retrying too early (still rate-limited) or waiting longer than necessary after the window actually reopened. This makes the backoff track the provider's real window.

Test plan

  • [x] cargo fmt / cargo clippy -- -D warnings clean
  • [x] cargo test — 9 new unit tests in reset_time.rs covering absolute/relative/ambiguous/past-rollover/cap/unparseable cases, plus updated resolver.rs tests for the new start_backoff(Duration) signature
  • [x] Full pre-push suite (fmt + clippy + ui build + ui tests + security audit + cargo test) passed

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ctcf2YbLSiWTT5q47RZxNg

Related

Tickets: #452
Tickets: #454

Discussion

  • Anonymous

    Anonymous - 2026-07-16

    Originally posted by: coderabbitai[bot]

    [!IMPORTANT]

    Review skipped

    Auto reviews are disabled on base/target branches other than the default branch.

    Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.


    ⚙️ Run configuration

    Configuration used: Organization UI

    Review profile: ASSERTIVE

    Plan: Pro Plus

    Run ID: 74736ea1-73af-43ea-8f4d-114fa41e91cb

    You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

    Use the checkbox below for a quick retry:
    - [ ] 🔍 Trigger review

    ✨ Finishing Touches
    🧪 Generate unit tests (beta) - [ ] Create PR with unit tests - [ ] Commit unit tests in branch `feat/llm-rate-limit-reset-retry`

    Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

    ❤️ Share - [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

    Comment @coderabbitai help to get the list of available commands.

     
  • Anonymous

    Anonymous - 2026-07-16

    Originally posted by: adityaharishch

    Review: [#452] — honor the provider's own rate-limit reset time

    Overview

    Adds src/llm/reset_time.rs, a small parser that extracts a concrete wait duration from a rate-limit error message (absolute clock time like "resets 3pm", or relative like "try again in 3h 42m"), and wires it into resolver.rs's start_backoff so the in-memory backoff tracks the provider's real reset window instead of a flat 30-minute guess. Falls back to the existing RATE_LIMIT_BACKOFF when nothing parses (e.g. Codex's weekly-limit message, which names a full date and is deliberately out of scope). Adds a 60s margin and a 6h cap to every parsed wait.

    Well-scoped, well-documented (module doc explains the ambiguity-resolution rule for bare hours), and backed by 12 new unit tests covering real observed strings from Claude Code, Codex, and GitHub Copilot, plus mechanics-level edge cases (past-time rollover, ambiguous bare hour, cap, unparseable).

    Correctness

    • Parsing logic (parse_relative/parse_absolute/take_number/parse_number_unit) is careful and each branch is exercised by a test with a real-world string. Traced through several by hand (bare-pm rollover, "It resets at" phrasing, compact 3h 42m, spelled-out "2 hours 15 minutes") — all check out.
    • One correctness caveat worth flagging (not necessarily a blocker): parse_absolute resolves the clock time using chrono::Local at the call site (resolver.rs passes chrono::Local::now()), but ignores any timezone the message itself states (e.g. "resets 10:40pm (Asia/Calcutta)"). This is fine as long as the machine's system timezone always matches the CLI's own local time (true today since the summariser runs on the user's own machine), but it's an implicit assumption that isn't documented in reset_time.rs's doc comment — worth a one-line note so a future reader doesn't assume the (Zone) suffix is actually being honored.
    • Minor robustness gap: parse_number_unit computes amount * 3600 / amount * 60 with plain u64 multiplication on a value parsed straight out of the provider's message text. A pathological/garbled string with a very large digit run (parses fine into u64 but overflows on * 3600) would panic in a debug/test build (attempt to multiply with overflow) rather than gracefully falling back like every other unparseable shape does. Since this is real subprocess-stderr text and the project's convention is to validate at system boundaries, consider checked_mul/saturating_mul here for defense in depth — low likelihood, cheap fix.

    Code quality / conventions

    • Matches repo conventions: file header comment present, module-level //! doc with # Related section, doc comments on every pub fn, #[cfg(test)] unit tests in-module.
    • resolver.rs's doc updates are consistent with the behavior change (backoff table entry, new "How long the backoff actually lasts" section).
    • Good practice keeping reset_time DB-free/pure (easy to unit test, no chrono::Local::now() baked in — now is a parameter).

    Test coverage

    • Strong: 12 tests split into "parsing mechanics" and per-provider sections, each citing its real-world source (OTel spool capture, filed GitHub issues, decompiled CLI bundle). This is exactly the kind of provenance that keeps regression tests trustworthy over time.
    • No test for a 24h-format bare hour without am/pm outside the 1-12 disambiguation branch (e.g. "resets 15:00" where hour > 12) — the code path exists (None => vec![hour] in candidate_hours) but isn't exercised. Not a bug, just uncovered.
    • resolver.rs tests were mechanically updated for the new start_backoff(Duration) signature — no new resolver-level test asserts that a parsed (non-default) duration actually gets threaded through complete_inner end-to-end; coverage there stays at the reset_time unit level. Given the size of the change this is a reasonable trade-off, just noting the gap.

    Other

    • Base branch is feat/llm-provider-enum, not pre-main — presumably intentional stacking on the not-yet-merged provider-enum work rather than an oversight, but flagging since the repo's CLAUDE.md states all feature/fix PRs target pre-main. Worth retargeting once the base lands.

    Verdict

    Solid, well-tested, narrowly-scoped change. Nothing blocking; the two correctness notes (timezone assumption, multiplication overflow) are worth a small follow-up but don't need to hold up the merge.

     

    Related

    Tickets: #452

  • Anonymous

    Anonymous - 2026-07-16

    Originally posted by: Akarsh-Hegde

    Addressed the review in f130fcb1:

    • Timezone assumption — added a "Timezone" section to reset_time.rs's module doc explaining that a parsed absolute time trusts the machine's own chrono::Local clock and never reads the zone the provider prints (e.g. "(Asia/Calcutta)"), and why that's safe here (every backend is a subprocess on this same machine, so the CLI's own rendering and Local can never disagree).
    • Multiplication overflowparse_number_unit now uses checked_mul/checked_add instead of */+= on the parsed digit run. A pathological string that fits in u64 but overflows once scaled to seconds now degrades to None (the caller's flat backoff) instead of panicking. Added a regression test (a_pathological_digit_run_does_not_panic).
    • Test coverage gap — added a_24h_format_hour_outside_1_12_is_unambiguous, covering the previously-untested hour > 12 / no-am-pm branch ("resets 14:30").

    Two points from the review I left as-is, since you flagged them as non-blocking/acceptable yourselves:

    • Base branch (feat/llm-provider-enum instead of pre-main) — intentional stacking on the not-yet-merged provider-enum work, per direction at the start of this thread. Will retarget once that branch lands on pre-main.
    • No resolver-level end-to-end test for a parsed duration threading through complete_inner — agreed this is a reasonable trade-off given the size of the change; coverage stays at the reset_time unit level (12+ tests now, each pinned to a real observed message per provider).

    546 tests passing, fmt/clippy/UI build/UI tests/security audit all clean on push.

     
  • Anonymous

    Anonymous - 2026-07-16

    Ticket changed by: Akarsh-Hegde

    • status: open --> closed
     

Log in to post a comment.