feat(api): Make API usage programmatically accessible#7215
Conversation
Fixes an issue where rates like "5000/hour" and "60/min" (as used by DRF's DEFAULT_THROTTLE_RATES) incorrectly hit the multiplier branch, causing a ValueError (e.g., int("hou")).
`parse_rate` now uses the first character when the period is alphabetic, while preserving support for multiplier formats like "5d" and "10s" used by existing callers (e.g., Celery task throttling).
Also fixes a bug in the fallback branch (period[-1] -> period[0]), which only worked for single-character inputs.
Adds a read-only endpoint that allows authenticated users to inspect their API usage programmatically. The response includes: - current_usage: Per-scope throttle status (requests made, remaining, reset time), sorted by utilization in descending order so the most constrained limits appear first. Scopes are derived from DEFAULT_THROTTLE_RATES, so new rate limits added in settings are automatically reflected. - historical_usage: 14-day daily request counts with a running total, sourced from existing Redis stats via invert_user_logs. - membership: The user’s NeonMembership level and active status, or null.
Resolves type errors reported by pre-commit following the feature addition. No functional changes.
db14893 to
522fc98
Compare
albertisfu
left a comment
There was a problem hiding this comment.
Thanks @ERosendo this looks good.
I just have one comment and some additional notes here:
-
ApiUsageViewSetis itself rate-limited by the same user throttle it reports on. This means that if the user gets throttled, they won’t be able to see their usage. For instance, if a user is using this endpoint to checkreset_atafter being throttled in order to retry requests, they won’t be able to call this endpoint once they are throttled.I’m not sure if this is intended behavior, but I think it could be confusing for users.
Maybe we can allow some extra requests to
ApiUsageViewSetbefore it gets throttled itself, so the endpoint remains available when users need it most, right after they’ve been throttled on regular endpoints. -
Also, we should update this PR with main and ensure the updated
APIThrottleconstraint takes effect in this PR:["user", "throttle_type", "rate"]. Currently, this PR only supports one throttle type per user, as the constraint is["user", "throttle_type"].We can also expand the tests to cover multidimensional throttle usage reports.
| num_requests, duration = parse_rate(rate) | ||
| cache_key = f"throttle_{scope}_{user.pk}" | ||
| history = cache.get(cache_key, []) | ||
| valid = [ts for ts in history if ts > current_time - duration] |
There was a problem hiding this comment.
If you make a citations request and there are entries in the citations throttle history, this will raise:
TypeError: '>' not supported between instances of 'list' and 'float'
This happens because the throttle history is a two-dimensional array like:
[[1, 1777779929.627806]]
So, we should properly extract the entries from the citations count and compute the current and remaining bucket, since the budget for citations is based on matched citations.
We might also need to rename these keys:
"requests_made"
"requests_allowed"
"requests_remaining"
to something that represents the number of matched citations instead of requests.
we should cover the described issues in a test to prevent regresessions.
Fixes
This PR fixes #7114
Summary
This PR adds
GET /api/rest/v4/api-usage/, a read-only endpoint that lets authenticated users inspect their own API consumption programmatically. Primarily motivated by MCP integrations that want to monitor usage.The response includes:
current_usage: per-scope throttle status (requests made, remaining, reset time) sorted by utilization descending, so the limit closest to being hit appears first. Scopes are discovered fromDEFAULT_THROTTLE_RATES, so adding a new rate limit to settings makes it visible here automatically.historical_usage: 14-day daily request counts plus a running total, sourced from existing Redis stats viainvert_user_logs.membership:NeonMembershiplevel and active status, or null.Here's an example:
{ "current_usage": [ { "scope": "user", "rate": "5000/hour", "requests_made": 17, "requests_allowed": 5000, "requests_remaining": 4983, "window_duration_seconds": 3600, "reset_at": "2026-04-13T17:35:10.122831+00:00", "blocked": false }, { "scope": "citations", "rate": "60/min", "requests_made": 0, "requests_allowed": 60, "requests_remaining": 60, "window_duration_seconds": 60, "reset_at": null, "blocked": false } ], "historical_usage": { "2026-04-02": 7, "total": 7 }, "membership": null }Deployment
This PR should:
skip-deploy(skips everything below)skip-web-deployskip-celery-deployskip-cronjob-deployskip-daemon-deploy