Repository navigation
feat: report provider rate limits and a session's context usage - #145
Open
gopaljigaur wants to merge 1 commit into
Open
gopaljigaur wants to merge 1 commit into
gopaljigaur wants to merge 1 commit into
Conversation
StepFinished, ModelFallback and a new CompletionRetry event carry the provider's response headers and a RateLimit read from OpenAI's and Anthropic's rate limit headers; the error events also carry its HTTP status. Agent.context_usage(session) counts the session's next request against the model's input limit and compaction threshold, as compaction counts it and without a network request.
gopaljigaur
force-pushed
the
feat/rate-limit-visibility
branch
from
October 19, 2026 12:47
9c78b15 to
b60869b
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Rate limits. Run events now carry the provider's HTTP response headers and the rate limits read from them.
StepFinishedgetsheaders(the answering completion's) andrate_limit.CompletionRetryevent (model,delay,status_code,headers,rate_limit,error) is emitted before each retry of a step's completion from feat(core): retry transient provider errors and time out stalled streams #95, streamed or not, and before a stalled stream's one retry. feat(core): retry transient provider errors and time out stalled streams #95 emitted nothing on a retry, so there was no event to extend. The compaction summary's retries emit none, since_ContextWindowhas no run to emit to.ModelFallback(feat: fall back to other models on overload, outage or refusal #126) getsstatus_code,headersandrate_limit: the error's for a failure, the refused response's headers for a refusal.RateLimit(exported fromcyclops) hasremaining_requests,reset_requests,remaining_tokens,reset_tokens.RateLimit.from_headers(headers)reads OpenAI'sx-ratelimit-*and Anthropic'santhropic-ratelimit-*headers and returnsNonewhen none are there. A header it can't read leaves its fieldNone, so an odd value from an OpenAI-compatible server never fails a run._Watchedinfallback.pynow flags only text and reasoning deltas as emitted, so a retry event in a stream doesn't stop the fallback after it.Context usage.
Agent.context_usage(session) -> Optional[ContextUsage]reportstokens(system prompt, the tools a run starts with, transcript),max_input_tokensandcompaction_threshold(Nonewithoutcompaction). It builds the same_ContextWindowa run builds, through_context_window, so the semantics match: an unknown model givesNone, or withcompactionset raises the sameValueErrorwith theregister_modelhint. It uses only_model_infoandtoken_counter, so it makes no network request (#130).acompactandcontext_usageshare a newAgent._requestthat notes the system prompt and tools on the window, and_ContextWindow.threshold()replaces the two inline threshold computations.Docs (docs and website): a Rate limits section in Streaming, the event table, retries in Agents, a Context usage section under Context window, and the API reference.
What LiteLLM 1.105.1 exposes
Checked by answering LiteLLM's real requests from a patched
httpx.AsyncClient.send:_hidden_params["additional_headers"]llm_provider-<name>, plus unprefixedx-ratelimit-{limit,remaining}-{requests,tokens}(no reset)RateLimitError,status_code=429, raw headers inlitellm_response_headersllm_provider-<name>, plus the same four unprefixed headers mapped fromanthropic-ratelimit-*(no reset)llm_provider-x-amzn-requestidand the likeStreams keep the same
additional_headerson the responsestream_chunk_builderassembles, directly and through a Router. Exceptions have noresponse_headersattribute in 1.105.1; the headers are inlitellm_response_headers, which #95 already reads forretry-after.Decisions
httpx.Headersthroughwith_raw_response, and the Vercel AI SDK puts a rawRecord<string, string>onresponse.headersof each step.headersfollows that: the provider's own names, with LiteLLM'sllm_provider-prefix taken off. LiteLLM's unprefixed copies are left out since they duplicate the raw ones. Errors give the same names fromlitellm_response_headers.RateLimitfield names follow OpenAI's headers (remaining_requests,reset_tokens), which LiteLLM also normalizes to. Resets are seconds from the response, asretry-afterand the IETF RateLimit header draft give them: OpenAI's Go durations (6m0s,120ms) are summed, and Anthropic's RFC 3339 times are measured from now, at least 0. LiteLLM normalizes no reset header itself.headershas the limits for anyone who wants them.on_retry, tenacity'sbefore_sleep), and a stream consumer needs it to show a wait.RunResult.erroris LiteLLM's exception, which already hasstatus_codeandlitellm_response_headers; the docs showRateLimit.from_headers(error.litellm_response_headers)._ContextWindowcounts each message once per window, and a window lives for one run (or onecompact()call) because a truncate or compaction between runs invalidates its per-message counts.context_usagebuilds a window the same wayacompactdoes, so each call counts the transcript once, as a run's first step does. Keeping a window on the session across runs would need invalidation fortruncate()and other runs' compactions.compaction_thresholdisceil(threshold * max_input_tokens), the first token count at whichcompact_if_neededcompacts.Tests
tests/test_rate_limits.py, with OpenAI's and Anthropic's real responses and errors parsed by LiteLLM (no network):StepFinished.headersandrate_limitfor both providers, JSON and streamed;retry-afteremitsCompletionRetrywith the status, headers and limits, then the run answers, JSON and streamed;ModelFallbackcarries the 429's status and headers;_Watchedchange);rate_limit=None;RateLimit.from_headers: Go durations, a past Anthropic reset, unreadable or missing headers.tests/test_compaction.py:context_usagewith compaction matches thetokens_beforea manual compaction counts, with threshold 320 of 400, and makes no completion; without compaction it counts the same tokens with no threshold; an unknown model givesNone, orValueErrorwith compaction set.tests/test_model_info.py:context_usagefor unmapped Ollama, Lemonade and Hugging Face models, directly and through a Router, sends no HTTP request, and a registered model gets its window.uv run pytest -n 4: 751 passed, 1 xfailed.pre-commit run --all-filesandmypy cyclopspass. The touched test files also pass on Python 3.10 and at the lowest direct dependencies (litellm 1.102.0).Closes #75