feat: add daily conversation quota schema foundation - #180
Conversation
4ebf4fb to
2a68eb1
Compare
|
|
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||
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>
2a68eb1 to
25e5064
Compare
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>
OSS-9996 Limit daily conversations to 20 with support for per-user increases
ContextThe linked Slack discussion converged on:
Slack discussion: https://allhandsai.slack.com/archives/C0A4YK70L3X/p1786657867367199 Proposed designImplement a per-user, per-UTC-day conversation-start quota enforced server-side at the canonical V1 app-conversation creation endpoint ( Policy and data model
The quota owner should be the user, not the organization: the Slack concern is power users creating excessive conversations, while the existing conversation metadata already associates every V1 conversation with both Enforcement
Admin/manual increase pathFor the first rollout, do not add a user-facing request flow or admin API. Support/engineering can set the per-user override directly in the database. The application must not impose a The initial workflow should allow support/engineering to set a user to Frontend behavior
Migration, rollout, and observability
Acceptance criteria
This issue/design was prepared by an AI agent (OpenHands) on behalf of the user. |
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'.
The enterprise ruff config enforces single-quoted strings; this literal was the one remaining violation failing the enterprise lint job.
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>
* feat: add read-only quota usage page with reset countdown 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> * fix: handle nullable daily_limit in TypeScript for quota settings Co-authored-by: openhands <openhands@all-hands.dev> * fix: resolve frontend lint and translation errors on quota page 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. * fix: satisfy enterprise ruff on quota status files 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. --------- Co-authored-by: neubig <neubig@users.noreply.github.com> Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: hieptl <hieptl.developer@gmail.com>
* feat: add daily conversation quota schema foundation 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> * fix: use single quotes in storage test to satisfy ruff Co-authored-by: openhands <openhands@all-hands.dev> * feat: add read-only quota usage page with reset countdown 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> * fix: handle nullable daily_limit in TypeScript for quota settings Co-authored-by: openhands <openhands@all-hands.dev> * feat: add org-level daily conversation quota exemptions 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> * fix: renumber quota migration to 150 and drop the user-limit backfill 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'. * fix: resolve frontend lint and translation errors on quota page 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. * fix: renumber org quota migration to 151 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. * fix: use single quotes in daily usage storage test The enterprise ruff config enforces single-quoted strings; this literal was the one remaining violation failing the enterprise lint job. * fix: satisfy enterprise ruff on quota status files 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. * fix: satisfy enterprise ruff on org quota files 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. * fix: address review findings on org-level quota exemptions 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. * fix: log org quota changes with the calling admin 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. --------- Co-authored-by: neubig <neubig@users.noreply.github.com> Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: hieptl <hieptl.developer@gmail.com>
…cation (#200) * feat: add daily conversation quota schema foundation 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> * fix: use single quotes in storage test to satisfy ruff Co-authored-by: openhands <openhands@all-hands.dev> * feat: add read-only quota usage page with reset countdown 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> * fix: handle nullable daily_limit in TypeScript for quota settings Co-authored-by: openhands <openhands@all-hands.dev> * feat: add org-level daily conversation quota exemptions 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> * feat: add work-email quota increase requests with self-service verification Add migration 149 with quota_increase_request table and work_email / work_email_verified_at columns on the user model. Implement: - Free-email domain rejection (exact set from research PR #88) - QuotaIncreaseRequestService: create, approve (idempotent), list pending - POST /api/quota/increase-request: validate work email, persist request, send signed JWS verification email (1-hour expiry), capture PostHog event and set work_email person property - GET /api/quota/verify?token=...: unauthenticated self-service approval via signed token — applies requested limit immediately - GET /api/admin/quota/requests + POST /api/admin/quota/requests/{id}/approve: admin list and approve fallback (superadmin-gated) - Frontend: increase request form on /settings/quota with work email, requested limit (capped at 10x), optional reason, pending/approved states - 14 focused tests covering free-email rejection, limit bounds, duplicates, idempotent approval, and self-service verification Stacked on feat/quota-usage-page (PR #199). Co-authored-by: openhands <openhands@all-hands.dev> * fix: renumber quota migration to 150 and drop the user-limit backfill 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'. * fix: resolve frontend lint and translation errors on quota page 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. * fix: renumber org quota migration to 151 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. * fix: address quota increase request blocking issues - Renumber the quota_increase_request migration to 152 on top of the renumbered org-quota migration 151, restoring a single linear Alembic chain (main already owns revisions 148/149). - Allowlist /api/quota/verify in SetAuthCookieMiddleware: the signed JWS token is the credential, and the link is opened from the user's work email client, usually without an app session. Previously every logged-out click got a 401 before the route ran. - Cap quota increase requests at 10x the org/deployment base default instead of 10x the user's current effective limit, so approved increases can no longer compound into unbounded self-service escalation. The baseline also now respects org-level limits and exemptions via DailyConversationQuotaService.get_default_limit. - Expire pending requests older than the verification-token TTL when the user submits a replacement, and add an admin reject endpoint, so a lost or expired verification email no longer permanently locks the user out of the flow. - Await resolve_analytics_context (it is async); the PostHog capture and work_email person property previously never fired because the sync call handed a coroutine to the analytics service and the error was swallowed. - Populate work_email and work_email_verified in /api/quota/status instead of returning hardcoded None/False. - Fix ESLint/prettier errors in the quota frontend files (define QuotaIncreaseRequestForm before use, import formatting) and replace the unsupported pt-BR translations with the required Catalan entries. * fix: use single quotes in daily usage storage test The enterprise ruff config enforces single-quoted strings; this literal was the one remaining violation failing the enterprise lint job. * fix: satisfy enterprise ruff on quota status files 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. * fix: satisfy enterprise ruff on org quota files 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. * fix: satisfy enterprise ruff on quota request files Export QuotaIncreaseRequest via __all__ (matching the module's pattern and resolving the unused-import error), sort the storage/conftest imports, wrap the long admin-router registration line, and apply ruff formatting to the free-email domain test. Previously masked in CI by the enterprise lint job's environment failure. --------- Co-authored-by: neubig <neubig@users.noreply.github.com> Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: hieptl <hieptl.developer@gmail.com>
|
🚀 Released in 1.55.0. |
Implements the database foundation for the daily conversation quota described in #179: a nullable per-user
daily_conversation_limitoverride column on the user table and adaily_conversation_usagetable for atomic per-user, per-UTC-day conversation accounting with a unique(user_id, usage_date)constraint.Migration 148 is rebased onto the latest main (which advanced through migration 147 since the original branch was opened).
No enforcement or API behavior is introduced here. This is the first of a stacked series:
Coordinates with OpenHands/OpenHands-Cloud#1100 and OpenHands/saas-deploy#639.
This pull request was created by an AI agent (OpenHands) on behalf of the user.
Enterprise server image for this PR: