Skip to content

feat(quota): enforce aggregate user limits - #263

Merged
astaxie merged 13 commits into
mainfrom
codex/issue-259-user-quota
Aug 21, 2026
Merged

feat(quota): enforce aggregate user limits#263
astaxie merged 13 commits into
mainfrom
codex/issue-259-user-quota

Conversation

@astaxie

@astaxie astaxie commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds aggregate user-scoped quota policies so one user's request, token, cost, rate, and concurrency limits apply across all API keys attributed to that user. This closes the per-key bypass caused by multiple keys and key rotation, while preserving ownership history when a key changes owner within one billing period.

Related Issue

Closes #259

Changes

  • Add scope: "user" quota policies with API-key owner, key creator, and project owner attribution fallback.
  • Enforce aggregate daily/monthly request, token, cost, RPM/TPM, and concurrency limits across keys, including atomic PostgreSQL multi-instance admission.
  • Store API-key quota totals in canonical rows and owner-specific history in attribution rows, so owner transfers preserve each user's usage.
  • Migrate legacy quota buckets to an explicit unattributed canonical marker; never assign unverifiable history to the current owner.
  • Commit image admission, concurrency leases, and image job creation in one database transaction; persist response/image admission state for recovery.
  • Preserve settlement and reservation correctness for buffered, streaming, image, and background Responses calls.
  • Extend the admin API and console with user selection, current request/token/cost usage, lifecycle controls, and team-leader authorization.
  • Update English, Simplified Chinese, and Japanese administrator documentation and UI translations.

Type of Change

  • Bug fix
  • New feature
  • Refactor or maintenance
  • Documentation
  • Deployment or configuration

Verification

  • gofmt -w on changed Go files
  • go test ./...
  • go vet ./...
  • npm run lint
  • npm run typecheck
  • npm test
  • npm run build
  • npm run test:e2e
  • node --test tools/*.test.mjs
  • node tools/check-doc-translations.mjs --base origin/main --head HEAD
  • node tools/check-ui-translations.mjs
  • node tools/check-env-contract.mjs
  • node tools/check-source-lines.mjs
  • git diff --check
  • Added regression coverage for same-bucket owner transfers, legacy-safe attribution, persisted image admission rollback, and transactional background admission.
  • SDK smoke tests were skipped because no compatible backend credentials/environment were configured; deployment checks were skipped because this PR does not change deployment files.

Compatibility, Security, and Operations

The existing OpenAI-compatible /v1 contract remains unchanged; user-scoped policies are additive and existing scopes retain their strictest-limit behavior. This change updates the frozen database adoption baseline: quota_buckets now uses (key_id, scope, bucket, attributed_user_id) as its primary key, legacy rows are retained under an explicit unattributed marker rather than guessed ownership, and image/response admission recovery fields are persistent. Pre-ledger databases are upgraded under the shared schema lock before verification. The attribution column defaults to the canonical unattributed marker so verified N-1 releases can continue writing during the rollback window; operators should still preserve the database and take the usual backup before rollout.

Image admission and job creation now commit together, so a crash cannot leave a reservation or concurrency lease without a recoverable job. User metrics and quota errors expose only bounded scope labels, never raw user IDs or secrets. Team leaders can configure only user policies within their own teams; platform administrators retain global access. Disabling or deleting a policy provides an application-level rollback without changing stored API keys.

Checklist

  • The PR title and body are written in English.
  • Tests were added or updated for behavior changes, including cross-key, owner-transfer, concurrency, settlement, lifecycle, migration, and PostgreSQL coverage.
  • No credentials, local .env files, databases, backups, or runtime logs are included.
  • Environment variable changes are synchronized across examples, Compose, start.sh, and deployment documentation where applicable.
  • Shared user-facing behavior is documented consistently in English, Simplified Chinese, and Japanese where applicable.
  • data/model-catalog.yaml remains tracked and catalog changes were reviewed where applicable.
  • git diff --check passes.

Copilot AI balanced review requested due to automatic review settings August 17, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the complete current diff at head cde69dd. Local full Go tests and go vet, frontend lint/typecheck/tests/build, all 122 repository tests, translation/UI/environment/source-line gates, and git diff --check passed. The required Backend and aggregate CI checks are still failing because four newly added helper functions are unused. I also reproduced one blocking attribution-continuity issue inline, so this is a comment-only review.

Comment thread backend/internal/server/store_backup_quota.go Outdated
Copilot AI review requested due to automatic review settings August 20, 2026 03:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete diff at a20bd517. All current GitHub checks are green, and local backend tests/vet, frontend lint/typecheck/tests/build, 125 repository tests, explicit translation/UI/environment/source-line gates, and git diff --check pass. The prior attribution finding is only partially fixed (I replied on that thread with a failing same-period transfer reproduction), and the following additional issues still block approval.

[P1] Make image admission atomic with durable image-job persistence (backend/internal/server/image_generation.go:95-109,241-255, backend/internal/server/store_routing_calls.go:450-462). startImageCall commits quota reservations and concurrency leases before CreateImageJob runs in a separate transaction. A process crash between those calls leaves no image-job row for FailUnfinishedImageJobs to discover, so request/token reservations remain charged and leases have no durable owner to recover. Persist the image job and its admission in one transaction, as the background Responses path does, and add a crash/restart regression.

[P1] Backfill existing current-period quota buckets safely (backend/internal/server/store_backup_quota.go:395-425,479-486). AutoMigrate adds attributed_user_id, but existing rows start blank. The first post-upgrade request stamps the whole pre-existing day/month row with the current owner, silently assigning all earlier requests, tokens, and cost to that user; untouched rows are ignored by user aggregation. Reconstruct attribution from request-time records or define an explicit conservative rollout path, with upgrade coverage for ownership changes.

[P2] Correct the PR's database and rollback impact. The Compatibility section says no database migrations are required, but this head adds persisted columns to quota_buckets, image_jobs, and response_jobs, and changes key/project deletion to retain attributed quota history (backend/internal/server/store.go:18-23, backend/internal/server/types.go:518-588, backend/internal/server/store_projects.go:187-196,684-696). Document the automatic schema changes, retained user-attribution data, upgrade/backfill behavior, and rollback compatibility.

[P2] Document team-leader authorization in all three administrator guides (docs/administrator-guide.md:73 and locale counterparts). The docs say platform administrators configure user quotas, while the frontend exposes quota policies to team leaders and the backend lets them manage policies for users in their own team (frontend/features/admin/core/navigation.tsx:210-216, backend/internal/server/admin_authorization.go:1034-1108). Describe that scoped capability and its restrictions consistently, or remove the access.

Copilot AI review requested due to automatic review settings August 20, 2026 05:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 20, 2026 05:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one remaining startup blocker in the PostgreSQL upgrade path:

[P1] Add the new column before the PostgreSQL backfill (backend/internal/server/store_database.go:378-380). migrate() invokes ensureQuotaBucketAttributionSchema before db.AutoMigrate (:214-232). On a database created by the base revision, quota_buckets has only the original (key_id, scope, bucket) columns, so this branch executes UPDATE quota_buckets SET attributed_user_id ... against a column that does not exist and aborts startup before AutoMigrate can add it. The SQLite branch handles the missing column explicitly, but the PostgreSQL branch does not. Please add/guard ALTER TABLE quota_buckets ADD COLUMN attributed_user_id ... (and make the backfill/PK replacement safe for both legacy and partially upgraded schemas), plus a PostgreSQL migration regression test.

Copilot AI review requested due to automatic review settings August 20, 2026 07:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original legacy-PostgreSQL startup blocker is fixed at c34adddb, but two issues still block approval:

[P1] Run the new PostgreSQL regressions in CI (.github/workflows/ci.yml:100-103). Both TestQuotaBucketMigrationPostgresAddsAttributionColumnBeforeBackfill and TestUserQuotaIsAtomicAcrossPostgresInstances are behind the integration build tag, while the PostgreSQL job filters execution to exactly ^TestMultiInstancePostgresE2E$; default go test ./... excludes them as well. The current green checks therefore execute neither the migration regression added by this commit nor the PR's direct cross-instance user-quota test. Expand the filter or add an explicit invocation so these tests actually run on every PR.

[P2] Scope the PostgreSQL primary-key metadata query to the active schema (backend/internal/server/store_database.go:390). It currently selects every primary key named on a quota_buckets table across all schemas. In a database containing public.quota_buckets plus a TokenHub schema (the new migration test creates exactly this shape), pkColumns combines both schemas, len(pkColumns) is never 4, and every startup unnecessarily drops and recreates the active table's primary key under an exclusive lock. Add tc.table_schema = current_schema() (or equivalent) and make the second migration assertion verify that the PK was not rebuilt.

Copilot AI review requested due to automatic review settings August 20, 2026 07:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at exact head d636b4dd. No remaining code, standards, or spec findings were found. All previously reported blockers are fixed, and exact-head CI is fully green, including the legacy PostgreSQL quota-bucket migration, primary-key idempotence, and cross-instance aggregate user-quota tests. Focused local quota/image/migration tests, go vet ./..., all 125 repository tests, explicit translation/UI/environment/source-line gates, and git diff --check pass. GitHub rejected the attempted APPROVE review because the authenticated astaxie account is also this PR's author; a non-author reviewer must approve if approval is required. Not merged.

Copilot AI review requested due to automatic review settings August 20, 2026 11:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 20, 2026 11:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 20, 2026 11:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at exact head 4739b3e. One new quota-settlement blocker remains.

[P1] finishCallTransaction wraps minute-token reconciliation, attributed key usage, and all user day/month reservation settlement inside the successful lookup of the live API key row at store_routing_calls.go:655-743. If an administrator deletes a key after StartCall admits a request but before FinishCall runs, the request and usage logs are still written, but the user reservation is never reconciled to actual tokens and the attributed quota buckets are not updated. This can under-enforce when actual usage exceeds the reservation or over-enforce when the reservation exceeds actual usage, breaking continuity across key deletion and rotation.

Please settle the immutable CallContext quota data even when the key row no longer exists, keep only key-dependent alerting or model data conditional, and add a regression that deletes the key between admission and completion.

All GitHub checks are green. The authenticated astaxie account is also the PR author, so GitHub cannot accept an APPROVE review from this account.

Copilot AI review requested due to automatic review settings August 21, 2026 03:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread backend/internal/server/store_routing_calls.go Outdated
Copilot AI review requested due to automatic review settings August 21, 2026 04:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread backend/internal/server/store_call_admission.go
Copilot AI review requested due to automatic review settings August 21, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie
astaxie merged commit 292a503 into main Aug 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(quota): add per-user aggregate usage limits

2 participants