Skip to content

feat: adds rate limits to customers and teams#1619

Merged
akshaydeo merged 1 commit intomainfrom
feature/02-12-feat_adds_rate_limits_to_customers_and_teams
Feb 12, 2026
Merged

feat: adds rate limits to customers and teams#1619
akshaydeo merged 1 commit intomainfrom
feature/02-12-feat_adds_rate_limits_to_customers_and_teams

Conversation

@danpiths
Copy link
Copy Markdown
Collaborator

Summary

Added rate limit support to teams and customers, allowing for hierarchical rate limiting at the organization level in addition to the existing virtual key level rate limits.

Changes

  • Added rate_limit_id column to both governance_teams and governance_customers tables
  • Implemented database migration to add the new columns
  • Updated CRUD operations for teams and customers to handle rate limits
  • Enhanced the governance store to track and enforce rate limits at team and customer levels
  • Updated UI components to display and manage rate limits for teams and customers
  • Added proper cleanup of rate limits when teams or customers are deleted
  • Implemented hierarchical rate limit checking that considers limits at VK, team, and customer levels

Type of change

  • Feature

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Plugins
  • UI (Next.js)

How to test

  1. Create a new team or customer with rate limits:
# Create a team with token and request rate limits
curl -X POST http://localhost:8000/api/governance/teams \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Team","rate_limit":{"token_max_limit":1000,"token_reset_duration":"1h","request_max_limit":100,"request_reset_duration":"1h"}}'

# Create a customer with rate limits
curl -X POST http://localhost:8000/api/governance/customers \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Customer","rate_limit":{"token_max_limit":5000,"token_reset_duration":"1d","request_max_limit":500,"request_reset_duration":"1d"}}'
  1. Test rate limit enforcement by making requests through a virtual key associated with the team or customer
  2. Verify rate limit usage is tracked and displayed correctly in the UI

Screenshots/Recordings

N/A

Breaking changes

  • No

Related issues

N/A

Security considerations

Rate limits provide an additional layer of security by preventing abuse of the API.

Checklist

  • I added/updated tests where appropriate
  • I verified builds succeed (Go and UI)

@danpiths danpiths requested a review from akshaydeo February 12, 2026 06:53
Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Suite Available

This PR can be tested by a repository admin.

Run tests for PR #1619

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added rate limiting for teams and customers (token/request limits with reset durations); create/update flows, validations, and public request fields updated.
    • UI: new inputs, current-usage displays, progress bars, tooltips and per-row “Limit Reached” states in teams/customers tables and dialogs.
    • Public store APIs extended to support transactional rate limit/budget operations; DB migration adds rate limit linkage.
  • Bug Fixes

    • Preserve in-memory usage on updates and cascade cleanup of orphaned budgets/rate limits on deletes.

Walkthrough

Adds Team and Customer rate-limit support across schema, DB models, store APIs, governance in-memory synchronization, HTTP handlers, and UI (dialogs/tables), including a migration, transactional delete/get helpers, and UI fields/visualizations.

Changes

Cohort / File(s) Summary
Database Migration
framework/configstore/migrations.go
New migration migrationAddRateLimitToTeamsAndCustomers adds rate_limit_id columns to governance_teams and governance_customers and is invoked in the migration sequence.
DB Models / Tables
framework/configstore/tables/team.go, framework/configstore/tables/customer.go
Added RateLimitID *string (indexed) and RateLimit *TableRateLimit relation to Team and Customer structs; comments updated.
Store / RDB API
framework/configstore/rdb.go, framework/configstore/store.go, transports/bifrost-http/lib/config_test.go
GetRateLimit now accepts optional tx; added DeleteRateLimit and DeleteBudget (transaction-aware). Many getters now preload RateLimit; delete flows cascade cleanup of RateLimit/Budget. Mock store/test helpers updated.
Governance Plugin (in-memory sync & tracker)
plugins/governance/store.go, plugins/governance/tracker.go
In-memory dump/collect/update/delete flows extended to include Team/Customer-level rate limits and budgets; UpdateUsage guard widened to consider team/customer rate limits; references propagated across VKs, providers, teams, customers.
HTTP Handlers & Schema
transports/bifrost-http/handlers/governance.go, transports/config.schema.json
Create/Update Team and Customer request types accept rate_limit; handlers validate/create/update rate limits within transactions and set rate_limit_id. JSON schema adds optional rate_limit_id for teams/customers.
UI Types
ui/lib/types/governance.ts
Public types updated: Team and Customer gain rate_limit_id? and rate_limit?; create/update request interfaces accept optional rate limit payloads.
UI — Dialogs / Forms
ui/app/workspace/user-groups/views/teamDialog.tsx, ui/app/workspace/user-groups/views/customerDialog.tsx
Form state switched to string inputs for budget/rate fields; added token/request max limit and reset duration fields; parsing/validation and submission include rate_limit when present; current-usage UI expanded.
UI — Tables / Displays
ui/app/workspace/user-groups/views/teamsTable.tsx, ui/app/workspace/user-groups/views/customerTable.tsx
Budget column replaced with combined budget + rate-limit visuals (progress bars, tooltips, reset durations, exhaustion badges); CustomersTable now exposes CustomersTableProps.
Minor UI / Tests
ui/components/formFooter.tsx, ui/app/workspace/model-limits/views/modelLimitsTable.tsx
Cancel button set to type="button"; action visibility responds to focus as well as hover. Test schema mocks updated to exclude rate_limit relations where needed.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as HTTP Handler
    participant DB as Database (Tx)
    participant Store as Governance Store

    Client->>API: POST /teams (body includes rate_limit)
    activate API
    API->>API: validate request & rate_limit
    API->>DB: BEGIN TX
    activate DB
    API->>DB: INSERT team
    API->>DB: INSERT rate_limit
    DB-->>API: return rate_limit_id
    API->>DB: UPDATE team SET rate_limit_id
    API->>DB: COMMIT
    deactivate DB
    API->>Store: sync team + rate_limit into in-memory structures
    activate Store
    Store->>Store: propagate rate_limit/budget references (VKs, teams, customers)
    Store-->>API: sync complete
    API-->>Client: 200 OK (team with rate_limit)
    deactivate API
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰 A rabbit hops through rows and code,

I bind rate limits on every node,
Budgets, tokens, requests come in line,
Migrations run and UIs shine,
I thump my paw — the systems purr and bode!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: adding rate limits to customers and teams, which is the primary change across the pull request.
Description check ✅ Passed The pull request description is comprehensive and follows the template structure with all major sections completed, including summary, changes, type of change, affected areas, test instructions, and checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/02-12-feat_adds_rate_limits_to_customers_and_teams

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
ui/lib/types/governance.ts (1)

175-223: ⚠️ Potential issue | 🟠 Major

UpdateRateLimitRequest supports null clears — make sure UI emits those when clearing fields.

The | null shapes are great for “clear/unset” semantics, but the dialogs currently only omit fields (or omit the entire rate_limit) when users clear inputs, which won’t clear server-side state in most APIs.

ui/app/workspace/user-groups/views/teamDialog.tsx (2)

76-209: ⚠️ Potential issue | 🟠 Major

Update flow can’t clear rate limits; dirty-tracking can allow “no-op saves”.

Two related issues:

  1. If a team already has token/request limits, clearing the inputs results in rate_limit being omitted (Line 165), so the server never receives a “clear” instruction even though UpdateRateLimitRequest supports null clears.
  2. Reset-duration changes can make the form dirty even when the corresponding max limit is empty, but those changes are dropped from the submission payload — leading to “saved successfully” with no actual change.

Please confirm backend update semantics for “clear” (null vs empty object vs omission) in the team update handler/store layer before merging.

Proposed fix (emit nulls on clear + align dirty tracking)
 useEffect(() => {
   const currentData = {
     name: formData.name,
     customerId: formData.customerId,
     budgetMaxLimit: formData.budgetMaxLimit,
-    budgetResetDuration: formData.budgetResetDuration,
+    budgetResetDuration: formData.budgetMaxLimit ? formData.budgetResetDuration : initialState.budgetResetDuration,
     tokenMaxLimit: formData.tokenMaxLimit,
-    tokenResetDuration: formData.tokenResetDuration,
+    tokenResetDuration: formData.tokenMaxLimit ? formData.tokenResetDuration : initialState.tokenResetDuration,
     requestMaxLimit: formData.requestMaxLimit,
-    requestResetDuration: formData.requestResetDuration,
+    requestResetDuration: formData.requestMaxLimit ? formData.requestResetDuration : initialState.requestResetDuration,
   };
   setFormData((prev) => ({
     ...prev,
     isDirty: !isEqual(initialState, currentData),
   }));
 }, [..., initialState]);

 ...

 // Add rate limit if enabled (token or request limits)
-if (tokenMaxLimitNum || requestMaxLimitNum) {
-  updateData.rate_limit = {
-    token_max_limit: tokenMaxLimitNum,
-    token_reset_duration: tokenMaxLimitNum ? formData.tokenResetDuration : undefined,
-    request_max_limit: requestMaxLimitNum,
-    request_reset_duration: requestMaxLimitNum ? formData.requestResetDuration : undefined,
-  };
-}
+const rateLimitTouched =
+  formData.tokenMaxLimit !== initialState.tokenMaxLimit ||
+  (formData.tokenMaxLimit && formData.tokenResetDuration !== initialState.tokenResetDuration) ||
+  formData.requestMaxLimit !== initialState.requestMaxLimit ||
+  (formData.requestMaxLimit && formData.requestResetDuration !== initialState.requestResetDuration);
+
+if (rateLimitTouched) {
+  updateData.rate_limit = {
+    token_max_limit: formData.tokenMaxLimit ? tokenMaxLimitNum : null,
+    token_reset_duration: formData.tokenMaxLimit ? formData.tokenResetDuration : null,
+    request_max_limit: formData.requestMaxLimit ? requestMaxLimitNum : null,
+    request_reset_duration: formData.requestMaxLimit ? formData.requestResetDuration : null,
+  };
+}

151-155: ⚠️ Potential issue | 🟠 Major

Inconsistent customer_id handling between create and update—empty string can persist via update.

Create prevents sending empty customer_id (uses || undefined), but update sends the raw value directly, allowing an empty string to reach the backend. The backend has no validation to reject or convert empty customer_id to null, so it persists the invalid state to the database. Align update to match create by converting empty strings to undefined:

customer_id: formData.customerId || undefined,

Also applies to: lines 179–182

ui/app/workspace/user-groups/views/customerDialog.tsx (1)

71-201: ⚠️ Potential issue | 🟠 Major

Customer rate limits cannot be cleared, and dirty-tracking allows orphaned duration changes.

Same pattern as TeamDialog:

  • Clearing token/request inputs omits rate_limit entirely (line 158), making it impossible to remove existing limits even though UpdateRateLimitRequest supports null values in the type definition.
  • Resetting duration inputs can mark the form dirty when their max limit is empty, but will never be submitted because the condition checks are at submission time (line 158).

Additionally, the backend updateCustomer handler lacks the deletion logic present in other handlers (ModelConfig, Provider), so even if the UI sends null values, the rate limit relationship would not be cleared.

Align UI dirty-tracking to only include reset-duration when its paired max-limit is set, and ensure the backend properly handles null rate_limit fields by setting RateLimitID = nil and deleting orphaned rate limit records.

🤖 Fix all issues with AI agents
In `@framework/configstore/store.go`:
- Around line 105-110: MockConfigStore has mismatched/missing method signatures:
update MockConfigStore's GetRateLimit to accept the variadic tx ...*gorm.DB
parameter, and add implementations for DeleteRateLimit and DeleteBudget that
match the ConfigStore interface signatures (e.g., GetRateLimit(ctx
context.Context, id string, tx ...*gorm.DB) (*tables.TableRateLimit, error),
DeleteRateLimit(ctx context.Context, id string, tx ...*gorm.DB) error,
DeleteBudget(ctx context.Context, id string, tx ...*gorm.DB) error); ensure
method receivers and return types follow the interface and that tests/uses
calling these methods (e.g., places that pass tx) compile cleanly.

In `@plugins/governance/store.go`:
- Around line 227-244: The build fails because tables.TableTeam is missing the
RateLimitID (and possibly RateLimit) fields referenced when cloning a team in
the iteration (the code uses clone.RateLimitID, clone.RateLimit, gs.rateLimits
and assigns to teams[key.(string)] = &clone). Fix by adding the missing fields
to the TableTeam struct (e.g., RateLimitID *<appropriate-type> and RateLimit
*configstoreTables.TableRateLimit) or by regenerating the generated tables code
so TableTeam includes RateLimitID/RateLimit, then recompile to ensure the clone
assignments compile against the updated struct.

In `@transports/bifrost-http/handlers/governance.go`:
- Around line 1267-1311: The update branch for rate limits currently only
overwrites non-nil fields and skips validateRateLimit, preventing callers from
clearing fields and allowing invalid values; change the update flow in the
handler so that when req.RateLimit != nil you assign every RateLimit field from
req (setting fields to nil when req has nil to clear them) instead of
conditional updates, call validateRateLimit(rateLimit) before persisting, and
then call h.configStore.UpdateRateLimit(ctx, rateLimit, tx); mirror the same
“assign all then validate then persist” pattern used in
updateModelConfig/updateProviderGovernance and keep the create path’s
validateRateLimit call intact.
- Around line 1579-1623: The handler currently ignores nil fields when updating
an existing rate limit; change the update logic for req.RateLimit so you perform
full assignment of each nullable field onto the fetched rateLimit (assigning nil
to clear values) instead of skipping on nil, then call
validateRateLimit(rateLimit) before calling h.configStore.UpdateRateLimit(ctx,
rateLimit, tx) to prevent persisting invalid values; do the same validation flow
used when creating a new rate limit (use validateRateLimit and then
h.configStore.CreateRateLimit) and keep the same assignments to
customer.RateLimitID and customer.RateLimit so callers see the updated/cleared
state.

In `@ui/app/workspace/user-groups/views/customerTable.tsx`:
- Around line 312-326: The action buttons are hidden via "opacity-0 ...
group-hover:opacity-100", which prevents keyboard users from revealing them;
update the container that wraps the actions (the div with className "opacity-0
transition-opacity group-hover:opacity-100") to also include the focus-visible
variant used in this repo (e.g., add "group-focus-within:opacity-100" or
equivalent) so the actions become visible when any child (like the
TooltipTrigger/Button that uses handleEditCustomer) receives keyboard focus;
apply the same change to the matching container in teamsTable.tsx to ensure
keyboard accessibility across both tables.

In `@ui/app/workspace/user-groups/views/teamsTable.tsx`:
- Around line 303-317: The action buttons container in teamsTable.tsx is hidden
with opacity-0 and only becomes visible on hover; update the container's class
(the div wrapping the Tooltip/Button in the row) to also include
group-focus-within:opacity-100 so keyboard focus reveals the actions (e.g.,
change the div className to include "group-focus-within:opacity-100"); ensure
the same update is applied to the equivalent containers in customerTable.tsx,
providerGovernanceTable.tsx, modelLimitsTable.tsx, and devProfiler.tsx so
keyboard users can access the handleEditTeam/Edit Button and other action
buttons via tab focus.
🧹 Nitpick comments (7)
framework/configstore/tables/customer.go (1)

5-24: RateLimit relationship wiring matches existing Budget pattern.

RateLimitID + RateLimit relationship is consistent with how BudgetID/Budget is modeled, so preloading/JSON should stay predictable across Customer/Team/VK.

If you expect DB-enforced one-to-one ownership (no sharing), consider adding a UNIQUE constraint on rate_limit_id (and similarly budget_id) at the migration level. This aligns with the stated “1:1 ownership” invariant but does require schema changes.

framework/configstore/tables/team.go (1)

11-23: Team RateLimit fields look consistent; consider enforcing 1:1 at DB layer.

The RateLimitID/RateLimit additions mirror Customer and should compose cleanly with existing GORM hooks.

Same as Customer: if rate limits are strictly owned and never shared, a UNIQUE constraint on governance_teams.rate_limit_id (nullable unique) would prevent accidental reuse.

ui/app/workspace/user-groups/views/teamsTable.tsx (2)

30-33: DRY: extract formatResetDuration (duplicated in customers table).

This helper is repeated in customerTable.tsx; consider moving it to ui/lib/utils/governance.ts next to formatCurrency for consistency.


210-281: Avoid defaulting missing reset durations to "1h" in display.

token_reset_duration || "1h" (and request) can silently misrepresent data if the backend returns null/missing durations. Prefer showing “—”/“Unknown” (or render only when duration exists) rather than inventing a value.

Please double-check backend guarantees around *_reset_duration when *_max_limit is set; if it’s guaranteed, then drop the fallback entirely.

Proposed tweak (display “—” instead of inventing “1h”)
- {formatResetDuration(team.rate_limit.token_reset_duration || "1h")}
+ {formatResetDuration(team.rate_limit.token_reset_duration ?? "—")}
...
- Resets {formatResetDuration(team.rate_limit.token_reset_duration || "1h")}
+ Resets {formatResetDuration(team.rate_limit.token_reset_duration ?? "—")}
ui/app/workspace/user-groups/views/customerTable.tsx (2)

30-33: DRY: formatResetDuration duplicated with teams table.

Recommend centralizing this helper (and possibly the “limits cell” UI) to keep Teams/Customers tables consistent.


219-290: Avoid defaulting missing reset durations to "1h" in display.

Same concern as TeamsTable: the fallback can mask missing/invalid data and show the wrong reset cadence.

Please confirm backend guarantees for *_reset_duration when *_max_limit is present; if guaranteed, remove fallback; otherwise show “—/Unknown”.

framework/configstore/migrations.go (1)

3330-3377: Add indexes for the new rate_limit_id columns.
gorm:"index" tags won’t backfill indexes on existing DBs via AddColumn, so consider explicit index creation for query performance and parity with new installs.

💡 Suggested migration enhancement
 			// Add rate_limit_id to governance_teams table
 			if !migrator.HasColumn(&tables.TableTeam{}, "rate_limit_id") {
 				if err := migrator.AddColumn(&tables.TableTeam{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to add rate_limit_id column to teams: %w", err)
 				}
 			}
+			if !migrator.HasIndex(&tables.TableTeam{}, "idx_team_rate_limit_id") {
+				if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_team_rate_limit_id ON governance_teams (rate_limit_id)").Error; err != nil {
+					return fmt.Errorf("failed to create rate_limit_id index on teams: %w", err)
+				}
+			}

 			// Add rate_limit_id to governance_customers table
 			if !migrator.HasColumn(&tables.TableCustomer{}, "rate_limit_id") {
 				if err := migrator.AddColumn(&tables.TableCustomer{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to add rate_limit_id column to customers: %w", err)
 				}
 			}
+			if !migrator.HasIndex(&tables.TableCustomer{}, "idx_customer_rate_limit_id") {
+				if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_customer_rate_limit_id ON governance_customers (rate_limit_id)").Error; err != nil {
+					return fmt.Errorf("failed to create rate_limit_id index on customers: %w", err)
+				}
+			}
 			if migrator.HasColumn(&tables.TableTeam{}, "rate_limit_id") {
 				if err := migrator.DropColumn(&tables.TableTeam{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to drop rate_limit_id column from teams: %w", err)
 				}
 			}
+			if err := tx.Exec("DROP INDEX IF EXISTS idx_team_rate_limit_id").Error; err != nil {
+				return fmt.Errorf("failed to drop team rate_limit_id index: %w", err)
+			}

 			if migrator.HasColumn(&tables.TableCustomer{}, "rate_limit_id") {
 				if err := migrator.DropColumn(&tables.TableCustomer{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to drop rate_limit_id column from customers: %w", err)
 				}
 			}
+			if err := tx.Exec("DROP INDEX IF EXISTS idx_customer_rate_limit_id").Error; err != nil {
+				return fmt.Errorf("failed to drop customer rate_limit_id index: %w", err)
+			}

Comment thread framework/configstore/store.go
Comment thread plugins/governance/store.go
Comment thread transports/bifrost-http/handlers/governance.go
Comment thread transports/bifrost-http/handlers/governance.go
Comment thread ui/app/workspace/user-groups/views/customerTable.tsx Outdated
Comment thread ui/app/workspace/user-groups/views/teamsTable.tsx Outdated
@danpiths danpiths force-pushed the feature/02-12-feat_adds_rate_limits_to_customers_and_teams branch from 1148916 to ae9e103 Compare February 12, 2026 07:54
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ui/app/workspace/user-groups/views/teamDialog.tsx (1)

156-199: ⚠️ Potential issue | 🟠 Major

Clearing budget/rate_limit fields doesn't remove existing limits; both teamDialog and customerDialog have this parity gap.

When users clear the budget or rate_limit fields and submit, the form omits these fields from the request payload (since the parsed values are undefined), rather than sending explicit null or deletion signals. The backend treats omitted optional fields as "no change," leaving existing limits intact on the server. Although DeleteBudget and DeleteRateLimit operations exist in the handlers, they're never invoked by this UI pattern. Consider explicitly sending nulls or a deletion flag when fields are cleared to enable proper removal.

ui/app/workspace/user-groups/views/customerDialog.tsx (1)

150-191: ⚠️ Potential issue | 🟠 Major

Clearing limits won't remove existing budget/rate-limit from customers and teams.

When editing a customer or team, cleared max-limit inputs omit budget / rate_limit from the update payload entirely (not sent as null). The backend handlers skip these omitted fields without attempting deletion, leaving existing BudgetID and RateLimitID references unchanged. This creates orphaned limits that remain active.

The correct pattern already exists in the codebase for ModelConfig and Provider handlers: check if req.Budget.MaxLimit == nil and explicitly set the ID field to nil to trigger deletion. Apply this same logic to updateCustomer and updateTeam handlers for parity.

🧹 Nitpick comments (6)
framework/configstore/migrations.go (1)

3330-3377: Consider explicit indexes for new rate_limit_id columns.

AddColumn may not create the gorm:"index" indexes automatically, so adding explicit indexes here would keep team/customer lookups consistent with other governance tables.

🛠️ Suggested index additions
 			// Add rate_limit_id to governance_teams table
 			if !migrator.HasColumn(&tables.TableTeam{}, "rate_limit_id") {
 				if err := migrator.AddColumn(&tables.TableTeam{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to add rate_limit_id column to teams: %w", err)
 				}
 			}
+			if !migrator.HasIndex(&tables.TableTeam{}, "idx_team_rate_limit") {
+				if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_team_rate_limit ON governance_teams (rate_limit_id)").Error; err != nil {
+					return fmt.Errorf("failed to create rate_limit_id index for teams: %w", err)
+				}
+			}
 
 			// Add rate_limit_id to governance_customers table
 			if !migrator.HasColumn(&tables.TableCustomer{}, "rate_limit_id") {
 				if err := migrator.AddColumn(&tables.TableCustomer{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to add rate_limit_id column to customers: %w", err)
 				}
 			}
+			if !migrator.HasIndex(&tables.TableCustomer{}, "idx_customer_rate_limit") {
+				if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_customer_rate_limit ON governance_customers (rate_limit_id)").Error; err != nil {
+					return fmt.Errorf("failed to create rate_limit_id index for customers: %w", err)
+				}
+			}
 		Rollback: func(tx *gorm.DB) error {
 			tx = tx.WithContext(ctx)
 			migrator := tx.Migrator()
 
+			if err := tx.Exec("DROP INDEX IF EXISTS idx_team_rate_limit").Error; err != nil {
+				return fmt.Errorf("failed to drop rate_limit_id index for teams: %w", err)
+			}
+			if err := tx.Exec("DROP INDEX IF EXISTS idx_customer_rate_limit").Error; err != nil {
+				return fmt.Errorf("failed to drop rate_limit_id index for customers: %w", err)
+			}
+
 			if migrator.HasColumn(&tables.TableTeam{}, "rate_limit_id") {
 				if err := migrator.DropColumn(&tables.TableTeam{}, "rate_limit_id"); err != nil {
 					return fmt.Errorf("failed to drop rate_limit_id column from teams: %w", err)
 				}
 			}
Does GORM Migrator.AddColumn create indexes defined via `gorm:"index"` tags, or must indexes be created explicitly (v1.31.1)?
transports/bifrost-http/lib/config_test.go (1)

575-581: Consider mutating mock governance state on DeleteRateLimit/DeleteBudget.
Right now these are no-ops; if future tests depend on deletions, the mock will mask bugs. Consider trimming m.governanceConfig to reflect removals.

♻️ Proposed update
 func (m *MockConfigStore) DeleteRateLimit(ctx context.Context, id string, tx ...*gorm.DB) error {
-	return nil
+	if m.governanceConfig == nil {
+		return nil
+	}
+	filtered := m.governanceConfig.RateLimits[:0]
+	for _, rl := range m.governanceConfig.RateLimits {
+		if rl.ID != id {
+			filtered = append(filtered, rl)
+		}
+	}
+	m.governanceConfig.RateLimits = filtered
+	return nil
 }
 
 func (m *MockConfigStore) DeleteBudget(ctx context.Context, id string, tx ...*gorm.DB) error {
-	return nil
+	if m.governanceConfig == nil {
+		return nil
+	}
+	filtered := m.governanceConfig.Budgets[:0]
+	for _, b := range m.governanceConfig.Budgets {
+		if b.ID != id {
+			filtered = append(filtered, b)
+		}
+	}
+	m.governanceConfig.Budgets = filtered
+	return nil
 }
ui/app/workspace/user-groups/views/teamsTable.tsx (1)

16-33: Consider centralizing formatResetDuration to avoid drift across the stack.

This helper is duplicated in ui/app/workspace/user-groups/views/customerTable.tsx; extracting it to a shared util keeps team/customer tables aligned.

framework/configstore/rdb.go (1)

2078-2090: Consider returning ErrNotFound when delete affects zero rows.

Keeps delete semantics consistent with other Delete* helpers and avoids silent success on unknown IDs.

Suggested pattern (apply similarly to DeleteBudget)
-	if err := txDB.WithContext(ctx).Delete(&tables.TableRateLimit{}, "id = ?", id).Error; err != nil {
-		return s.parseGormError(err)
-	}
-	return nil
+	result := txDB.WithContext(ctx).Delete(&tables.TableRateLimit{}, "id = ?", id)
+	if result.Error != nil {
+		return s.parseGormError(result.Error)
+	}
+	if result.RowsAffected == 0 {
+		return ErrNotFound
+	}
+	return nil

Also applies to: 2163-2175

plugins/governance/store.go (2)

1916-1992: Consider de-duplicating rate limits during hierarchy traversal.

If future configs allow overlapping references (or transient data inconsistencies), duplicate checks can produce repeated violations. A small seen set avoids that.

♻️ Optional deduplication helper
 func (gs *LocalGovernanceStore) collectRateLimitsFromHierarchy(vk *configstoreTables.TableVirtualKey, requestedProvider schemas.ModelProvider) ([]*configstoreTables.TableRateLimit, []string) {
 	if vk == nil {
 		return nil, nil
 	}

 	var rateLimits []*configstoreTables.TableRateLimit
 	var rateLimitNames []string
+	seen := make(map[string]struct{})
+	add := func(rl *configstoreTables.TableRateLimit, name string) {
+		if rl == nil {
+			return
+		}
+		if _, ok := seen[rl.ID]; ok {
+			return
+		}
+		seen[rl.ID] = struct{}{}
+		rateLimits = append(rateLimits, rl)
+		rateLimitNames = append(rateLimitNames, name)
+	}

 	for _, pc := range vk.ProviderConfigs {
 		if pc.RateLimitID != nil && pc.Provider == string(requestedProvider) {
 			if rateLimitValue, exists := gs.rateLimits.Load(*pc.RateLimitID); exists && rateLimitValue != nil {
 				if rateLimit, ok := rateLimitValue.(*configstoreTables.TableRateLimit); ok && rateLimit != nil {
-					rateLimits = append(rateLimits, rateLimit)
-					rateLimitNames = append(rateLimitNames, pc.Provider)
+					add(rateLimit, pc.Provider)
 				}
 			}
 		}
 	}
 ...
-					rateLimits = append(rateLimits, rateLimit)
-					rateLimitNames = append(rateLimitNames, "Team")
+					add(rateLimit, "Team")
 ...
-										rateLimits = append(rateLimits, rateLimit)
-										rateLimitNames = append(rateLimitNames, "Customer")
+										add(rateLimit, "Customer")
 ...
-							rateLimits = append(rateLimits, rateLimit)
-							rateLimitNames = append(rateLimitNames, "Customer")
+							add(rateLimit, "Customer")

2365-2375: Optional: clean LastDBUsagesRateLimits entries on delete.

This prevents stale usage maps from growing across deletes.

♻️ Optional cleanup
 			if team.RateLimitID != nil {
-				gs.rateLimits.Delete(*team.RateLimitID)
+				rateLimitID := *team.RateLimitID
+				gs.rateLimits.Delete(rateLimitID)
+				gs.LastDBUsagesRateLimitsRequestsMu.Lock()
+				delete(gs.LastDBUsagesRequestsRateLimits, rateLimitID)
+				gs.LastDBUsagesRateLimitsRequestsMu.Unlock()
+				gs.LastDBUsagesRateLimitsTokensMu.Lock()
+				delete(gs.LastDBUsagesTokensRateLimits, rateLimitID)
+				gs.LastDBUsagesRateLimitsTokensMu.Unlock()
 			}
 			if customer.RateLimitID != nil {
-				gs.rateLimits.Delete(*customer.RateLimitID)
+				rateLimitID := *customer.RateLimitID
+				gs.rateLimits.Delete(rateLimitID)
+				gs.LastDBUsagesRateLimitsRequestsMu.Lock()
+				delete(gs.LastDBUsagesRequestsRateLimits, rateLimitID)
+				gs.LastDBUsagesRateLimitsRequestsMu.Unlock()
+				gs.LastDBUsagesRateLimitsTokensMu.Lock()
+				delete(gs.LastDBUsagesTokensRateLimits, rateLimitID)
+				gs.LastDBUsagesRateLimitsTokensMu.Unlock()
 			}

Also applies to: 2477-2487

@danpiths danpiths force-pushed the feature/02-12-feat_adds_rate_limits_to_customers_and_teams branch from ae9e103 to 40c695f Compare February 12, 2026 08:47
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@plugins/governance/store.go`:
- Around line 1945-1991: The code appends the same customer rate limit twice
when vk.TeamID points to a team whose CustomerID equals vk.CustomerID; update
the logic in the block around gs.teams / gs.customers resolution (the code that
fills rateLimits and rateLimitNames for team/customer) to dedupe before
appending: track seen customer IDs (or seen rateLimit IDs) in a local set, check
collectRateLimitIDsFromMemory / UpdateVirtualKeyRateLimitUsageInMemory callers'
expectations, and only append a customer rate limit if its customer ID (or
rateLimit.ID) has not already been added; reference vk, gs.teams, gs.customers,
gs.rateLimits, rateLimits, and rateLimitNames when applying the check.

In `@transports/bifrost-http/lib/config_test.go`:
- Around line 571-581: The mock deletion methods
(MockConfigStore.DeleteRateLimit and MockConfigStore.DeleteBudget) currently
no-op and should mutate the in-memory governance state so tests can observe
deletions; update each method to remove the entry with the matching id from the
corresponding slice on MockConfigStore (e.g., RateLimits, Budgets or whatever
slice fields hold the data), handle optional tx param as before, and maintain
any existing concurrency protection (locks) used by MockConfigStore so removal
is safe; also ensure DeleteRateLimit returns an error when the id is not found
if the real store would do so.
🧹 Nitpick comments (4)
plugins/governance/store.go (1)

2732-2792: Comment mentions “users” but no user updates occur here.
Consider removing “users” to match actual behavior and avoid confusion.

ui/app/workspace/user-groups/views/teamsTable.tsx (1)

30-33: Consider centralizing formatResetDuration for the stacked views.

If customer/other tables in the stack use the same formatting, a shared util would prevent drift.

ui/app/workspace/user-groups/views/customerDialog.tsx (1)

88-91: Consider using parseFloat for token/request limits to handle edge cases.

Using parseInt for tokenMaxLimitNum and requestMaxLimitNum will silently truncate decimal inputs (e.g., "100.5" becomes 100). While the UI likely constrains to integers, this could cause confusion if a user enters a decimal value. The budget correctly uses parseFloat.

This is minor since rate limits are typically integers, but worth noting for consistency.

ui/app/workspace/user-groups/views/customerTable.tsx (1)

30-33: Consider extracting formatResetDuration to a shared utility.

This helper function is duplicated across customerTable.tsx and likely teamsTable.tsx. Consider moving it to @/lib/utils/governance.ts alongside formatCurrency for better reusability and consistency.

Comment thread plugins/governance/store.go
Comment thread transports/bifrost-http/lib/config_test.go
@danpiths danpiths force-pushed the feature/02-12-feat_adds_rate_limits_to_customers_and_teams branch 2 times, most recently from f317171 to 16da591 Compare February 12, 2026 10:45
@danpiths danpiths force-pushed the feature/02-12-feat_adds_rate_limits_to_customers_and_teams branch from 16da591 to 95af7f5 Compare February 12, 2026 11:46
@akshaydeo akshaydeo merged commit 11381ae into main Feb 12, 2026
8 checks passed
@akshaydeo akshaydeo deleted the feature/02-12-feat_adds_rate_limits_to_customers_and_teams branch February 12, 2026 11:47
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.

2 participants