feat: add org-level daily conversation quota exemptions - #212
Merged
Conversation
Add migration 148 with a nullable per-user daily_conversation_limit override column on the user table and a daily_conversation_usage table for atomic per-user, per-UTC-day conversation accounting with a unique (user_id, usage_date) constraint. Register the model and add focused storage tests for the nullable override and uniqueness invariant. No enforcement or API behavior is introduced in this foundation; that lands in subsequent stacked PRs. Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Add GET /api/quota/status returning the authenticated user's effective daily limit, used count, remaining, and next UTC-midnight reset_at. Add a SaaS-only settings page at /settings/quota showing a progress bar and a live HH:MM:SS countdown to the next reset. Includes focused tests for the quota status service covering unlimited, partial, exhausted, and no-usage-today cases. Stacked on feat/daily-conversation-limit (PR #180). Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Add migration 149_org_quota with a nullable daily_conversation_limit
column on the org table. Update the quota service to resolve limits
with the precedence: user override → org override → env default → None.
Org-level exemptions use -1 to mean unlimited (NULL inherits the
deployment default). This allows paying SaaS orgs to be exempted from
daily conversation limits while still enforcing limits for other orgs.
Add PUT /api/admin/quota/orgs/{org_id}/quota admin endpoint for
setting or clearing org-level limits. 6 focused tests covering
user precedence, org override, org exemption, env fallback, and
unset/unlimited resolution.
Stacked on feat/quota-usage-page (PR #199).
Co-authored-by: openhands <openhands@all-hands.dev>
This was referenced Aug 20, 2026
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Main advanced through migrations 148/149 (budget changes), so this branch's migration reused the already-applied revision id 148. Renumber it to 150 on top of main's 149. Also drop the OH_DAILY_CONVERSATION_LIMIT backfill: stamping the deployment default into every existing user's daily_conversation_limit would take precedence over org-level limits/exemptions and future default changes, permanently pinning those users. NULL now always means 'inherit the effective default at runtime'.
Return undefined explicitly from the countdown effect's no-op branch (consistent-return alongside the interval cleanup return), apply Prettier formatting to the SaaS-only message and countdown span, and swap the unsupported pt-BR translations of the quota keys for the required Catalan (ca) entries.
The revision id '149_org_quota' broke the numeric-prefix/revision match rule and, together with the quota-request migration, created a second Alembic head off revision 148. Renumber to a linear 151 on top of the renumbered 150 so 'alembic upgrade head' resolves a single head.
|
|
The enterprise ruff config enforces single-quoted strings; this literal was the one remaining violation failing the enterprise lint job.
Sort the quota router imports, drop the unused AsyncSession and patch imports. These were previously masked in CI because the enterprise lint job failed to build its mypy environment before ruff could report.
…ions # Conflicts: # enterprise/saas_server.py # enterprise/server/routes/quota.py
Drop the unused logger import and apply ruff formatting in the quota routes and org quota test; restore the patch import the upstream merge removed while this branch's env-clearing test still uses it. These were previously masked in CI by the enterprise lint job's environment failure.
Scope quota resolution to the request's effective org, gate the admin API
through the shared permission check, validate limit values, and guard the
migration revision graph.
- Resolve the daily conversation limit against the effective org
(X-Org-Id / API-key binding) instead of user.current_org_id. The latter
is only the user's last-selected org, so a multi-org user got the wrong
org's limit -- and the wrong org's exemption -- whenever a request was
scoped elsewhere.
- Replace the hand-rolled super-role check on
PUT /api/admin/quota/orgs/{org_id}/quota with
require_permission(MANAGE_ORG_QUOTA). The inline check skipped the
API-key organization binding, letting a key bound to one org edit
another org's quota. MANAGE_ORG_QUOTA is granted only to the superadmin
super role; no org-scoped role carries it.
- Reject 0 and values below -1 on the admin API. They are not meaningful
quotas but resolve to a limit no org can satisfy, silently blocking
every member -- a mistyped "-11" for "-1" would have done the opposite
of the intended exemption. Treat -1 as "exempt" at the user level too so
the sentinel means the same thing at both levels.
- Type the path org_id as UUID so a malformed id is a 422, not a 500.
- Add test_migration_graph.py: duplicate revision ids, multiple heads,
missing parents and shared parents now fail loudly. Sequential numbering
means concurrent branches pick the same number and each passes CI alone,
so the collision otherwise only surfaces on main after the second merge.
- Cover the admin route and its authorization gate, and pin the real
user -> org -> usage query sequence in get_status.
Exempting an org from daily conversation limits is revenue-affecting, but the endpoint recorded nothing, so there was no way to answer who changed an org's quota or when. Bind the caller id that require_permission already returns and emit org_quota:set after the commit, matching the super_admins:grant / super_admins:revoke convention for instance-admin mutations.
|
🚀 Released in 1.55.0. |
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.
Adds org-level daily conversation quota support so that specific organizations can be exempted from limits or given custom limits, independent of the deployment-wide default.
What
daily_conversation_limitcolumn to theorgtabledaily_conversation_limit(int) — per-user overridedaily_conversation_limit(int) — org-specific limit, or-1to exempt the org entirelyOH_DAILY_CONVERSATION_LIMITenv var — deployment-wide defaultPUT /api/admin/quota/orgs/{org_id}/quota— superadmin-gated endpoint to set or clear org-level limitsWhy
Limits must be configurable at the org level so that paying SaaS customers can be exempted while free-tier orgs remain rate-limited. This also ensures OHE (on-prem enterprise) customers are unaffected — enforcement is gated on SaaS mode, and the env var defaults to unset (unlimited).
Stacking
This PR is stacked on #199 (usage page), which is stacked on #180 (schema).
This pull request was created by an AI agent (OpenHands) on behalf of the user.
Enterprise server image for this PR: