feat: wires vk top-level and provider-level budgets from model configs table - #3939
Conversation
|
Warning Review limit reached
More reviews will be available in 19 minutes and 57 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughThis PR consolidates governance multi-budgeting by moving budgets into model configs via ChangesMulti-Budget Governance & Model Config Consolidation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Confidence Score: 3/5Unsafe to merge for existing deployments: the migration that moves VK-level governance into wildcard model configs is never called, leaving every pre-existing virtual key's budgets stranded and causing display breakage and potential double-enforcement after any VK edit. The missing migration call has concrete runtime consequences: GET requests against existing VKs return empty governance fields (hydrateVKGovernance nulls out vk.Budgets when no wildcard model config exists), and a VK edited via the new API ends up with both old VK-level budget rows and new model-config budget rows active simultaneously, charging each request against two independent counters. Multiple existing VKs would be affected on every deployment that upgrades from a prior version. framework/configstore/migrations.go — the triggerMigrations chain at lines 837–843 is missing the migrationMigrateVirtualKeyGovernanceToModelConfigs call that the rest of the PR depends on. Important Files Changed
Reviews (4): Last reviewed commit: "feat: wires vk top-level and provider-le..." | Re-trigger Greptile |
|
@coderabbitai full-review |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
plugins/governance/store.go (2)
3557-3569:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftAggregate status from the same model-config tiers used by enforcement.
This still only inspects the direct
(model, provider)and global model-only entries. The enforcement and usage paths now usecollectModelConfigsForplusnonGlobalModelConfigScopeChain, so wildcard configs and VK-scoped wildcard rows never contribute here. After the governance cutover,BudgetPercentUsed/ rate-limit percentages can report a safe status while an applicable wildcard config is already near or over limit.Also applies to: 3606-3619
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/governance/store.go` around lines 3557 - 3569, The aggregation currently only inspects the direct modelConfig and global entries; update the budget aggregation to collect and iterate the same model-config tiers used by enforcement by calling collectModelConfigsFor(...) and respecting nonGlobalModelConfigScopeChain so wildcard and VK-scoped wildcard configs are included; specifically, replace the direct loop over modelConfig.Budgets with iterating over budgets from all configs returned by collectModelConfigsFor (for each config, use its .Budgets and existing gs.budgets lookup and budgetBaselines logic to compute budgetPercent and update result.BudgetPercentUsed), and make the analogous change in the second block (lines ~3606-3619) so both places use the same scope chain.
3118-3160:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReconcile removed child budgets and rotated rate limits on model-config updates.
UpdateModelConfigInMemoryonly upserts the incomingBudgets/RateLimit. If an update removes a budget or changes/removes the rate-limit ID, the old entries stay ings.budgets/gs.rateLimits, andDumpBudgets/DumpRateLimitswill keep flushing those stale counters until a full reload.♻️ Suggested reconciliation shape
func (gs *LocalGovernanceStore) UpdateModelConfigInMemory(ctx context.Context, mc *configstoreTables.TableModelConfig) *configstoreTables.TableModelConfig { if mc == nil { return nil // Nothing to update } // Clone to avoid modifying the original clone := *mc + + var existing *configstoreTables.TableModelConfig + gs.modelConfigs.Range(func(_, value interface{}) bool { + if current, ok := value.(*configstoreTables.TableModelConfig); ok && current != nil && current.ID == clone.ID { + existing = current + return false + } + return true + }) // Store associated budgets, preserving existing in-memory usage per budget ID and // stamping calendar alignment from the model config (consumed by the reset path). + nextBudgetIDs := make(map[string]struct{}, len(clone.Budgets)) for i := range clone.Budgets { b := &clone.Budgets[i] + nextBudgetIDs[b.ID] = struct{}{} b.IsCalendarAligned = clone.CalendarAligned if existingBudgetValue, exists := gs.budgets.Load(b.ID); exists && existingBudgetValue != nil { if eb, ok := existingBudgetValue.(*configstoreTables.TableBudget); ok && eb != nil { b.CurrentUsage = eb.CurrentUsage b.LastReset = eb.LastReset } } gs.budgets.Store(b.ID, b) } + + if existing != nil { + for i := range existing.Budgets { + if _, ok := nextBudgetIDs[existing.Budgets[i].ID]; !ok { + gs.DeleteBudget(ctx, existing.Budgets[i].ID) + } + } + } // Store associated rate limit if exists, preserving existing in-memory usage if clone.RateLimit != nil { if existingRateLimitValue, exists := gs.rateLimits.Load(clone.RateLimit.ID); exists && existingRateLimitValue != nil { if erl, ok := existingRateLimitValue.(*configstoreTables.TableRateLimit); ok && erl != nil { clone.RateLimit.TokenCurrentUsage = erl.TokenCurrentUsage clone.RateLimit.RequestCurrentUsage = erl.RequestCurrentUsage } } gs.rateLimits.Store(clone.RateLimit.ID, clone.RateLimit) } + + if existing != nil && existing.RateLimitID != nil && + (clone.RateLimitID == nil || *existing.RateLimitID != *clone.RateLimitID) { + gs.DeleteRateLimit(ctx, *existing.RateLimitID) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/governance/store.go` around lines 3118 - 3160, UpdateModelConfigInMemory currently only upserts incoming clone.Budgets and clone.RateLimit, leaving removed/rotated children behind; reconcile by deleting stale entries: before storing the new budgets, collect the set of incoming budget IDs from clone.Budgets, iterate gs.budgets to find budgets associated with this model config (use the same association key logic you use when storing — e.g., modelConfigStoreKey/clone.ModelName or any ModelConfigID field) and gs.budgets.Delete any stored budget whose ID is not in the incoming set; for rate limits, check if an existing stored rate-limit ID for this model config differs from clone.RateLimit.ID (or is nil when clone.RateLimit is nil) and if so gs.rateLimits.Delete the old ID before storing the new/updated rate limit; ensure you still preserve usage (copy eb.CurrentUsage / erl.*) prior to deletion if you need to migrate counters, and update gs.modelConfigs via modelConfigStoreKey as you already do.framework/configstore/rdb.go (1)
1158-1184:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftLock and delete scoped model configs through a shared path.
This snapshots
providerModelConfigswithout locking them, deletes the currently preloaded budgets/rate-limits, and only then bulk-deletes the parent rows. A concurrentUpdateModelConfig/budget reconciliation can attach a new child after the preload but before the finalDELETE, which leaves an orphan when FK cascade is unavailable. Please lock the model-config rows up front and route each row through the same helper/path asDeleteModelConfiginstead of open-coding the cascade here.As per coding guidelines,
**/*.goshould apply standard Go review practices, includingrace-safe shared state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@framework/configstore/rdb.go` around lines 1158 - 1184, The current deletion code loads providerModelConfigs without locking and deletes child budgets/rate-limits directly, risking races that leave orphans; instead, first select and lock the model-config rows for this provider (using the transaction txDB with a FOR UPDATE/SELECT ... FOR UPDATE equivalent) to prevent concurrent updates, then iterate each locked TableModelConfig and call the existing DeleteModelConfig helper for each (rather than manually deleting children and bulk-deleting parents) so the same reconciliation/cleanup path is used and race conditions are avoided; ensure you still run within ctx and txDB and handle DeleteModelConfig errors per-row.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 4035-4039: Add a new migration that creates an index on
governance_budgets.model_config_id (reference: tables.TableBudget and the new
"model_config_id" column added via mig.AddColumn / checked via mig.HasColumn)
and ensure the index is created non-transactionally and concurrently on Postgres
to avoid table locks; implement the migration step so it runs only if the column
exists and uses the database-specific concurrent index creation path (i.e.,
non-transactional CREATE INDEX CONCURRENTLY for Postgres, with a safe fallback
for other DBs) and mark the migration as non-transactional so it won't be
wrapped in a transaction.
- Around line 4063-4072: The Rollback currently drops model_config_id (using
mig.HasColumn/DropColumn on tables.TableBudget) which would silently lose
ownership data for newer multi-budget rows; change the migration to be
explicitly non-rollbackable instead of performing the destructive DropColumn:
replace the Rollback implementation with a function that immediately returns a
clear non-rollbackable error (e.g., errors.New("non-rollbackable migration:
cannot restore model_config_id and budget ownership")), and remove any
DropColumn logic so the migration fails loudly when a downgrade is attempted.
- Around line 4197-4257: The current Rollback function (Rollback func(tx
*gorm.DB) error) is unsafe because it may delete any tables.TableModelConfig
with scope==ModelConfigScopeVirtualKey and model_name==ModelConfigAllModels;
either mark this migration non-rollbackable by removing/replacing the Rollback
implementation (e.g., set Rollback to nil or return a non-rollbackable error) so
it cannot run on downgrade, or implement provenance: persist a migration
identifier on created rows (e.g., add a migration_id or created_by field when
inserting into tables.TableModelConfig and related rows) and change the rollback
logic to only affect rows with that migration_id (and only restore budgets/rate
limits when a matching tables.TableVirtualKeyProviderConfig exists), ensuring
you reference tables.TableModelConfig, ScopeID, Provider, RateLimitID,
tables.TableVirtualKeyProviderConfig, and governance_budgets/provider_config
updates when coding the safe restore.
- Around line 840-844: The migration ordering is wrong:
migrationMigrateVirtualKeyGovernanceToModelConfigs runs before
migrationAddModelConfigCalendarAlignedColumn causing inserts (via
ensureVKWildcardModelConfig in
migrationMigrateVirtualKeyGovernanceToModelConfigs) to target a non-existent
CalendarAligned column; fix by reordering the calls so
migrationAddModelConfigCalendarAlignedColumn(ctx, db) is executed before
migrationMigrateVirtualKeyGovernanceToModelConfigs(ctx, db) in migrations.go
(i.e., swap the two migration function invocations).
In `@plugins/governance/store.go`:
- Around line 2139-2145: The code stamps mc.CalendarAligned onto owned budgets
but misses propagating it to model-config rate limits, causing
ResetExpiredRateLimitsInMemory to use stale rateLimit.IsCalendarAligned; update
the same places that set mc.Budgets[j].IsCalendarAligned (the for j := range
mc.Budgets loop) to also set mc.RateLimits[k].IsCalendarAligned =
mc.CalendarAligned (and store/update those rate limits where applicable), and
make the identical change in the other rebuild/live-update path mentioned
(around the 3118-3141 block) so wildcard/owned rate limits inherit the owner’s
calendar_aligned flag consistently.
In `@transports/bifrost-http/handlers/governance.go`:
- Around line 2849-2858: createModelConfig's budget validation currently checks
for negative MaxLimit and parses ResetDuration but doesn't enforce uniqueness of
ResetDuration, which allows duplicate durations to be created; update the
validation in createModelConfig (the loop over req.Budgets) to track seen reset
durations (use the parsed/normalized duration or the ResetDuration string from
req.Budgets) and if a duplicate is detected return a 400 via SendError with a
clear message like "Duplicate reset_duration not allowed: <value>" (follow the
same duplicate-rejection behavior used by the update/reconcile path that uses
configstoreTables.ParseDuration).
In `@ui/app/workspace/model-limits/views/modelLimitsTable.tsx`:
- Around line 321-341: The Scope Target badge is mouse-only and missing a stable
test id; update the interactive element inside TableCell (currently the Badge
with onClick that calls navigate using config.scope_id and shows
config.scope_name) to be a keyboard-accessible control (e.g., a semantic
<button> or <a> that supports Enter/Space and focus styles via the existing
TooltipTrigger) and add a data-testid attribute (e.g.,
data-testid="scope-target-{config.scope_id}" or similar stable id) to the
interactive element so E2E tests can target it; ensure the TooltipTrigger still
wraps the new control and use the same navigate(...) call on activation (click
and keyboard) and preserve the displayed config.scope_name and ArrowUpRight
icon.
---
Outside diff comments:
In `@framework/configstore/rdb.go`:
- Around line 1158-1184: The current deletion code loads providerModelConfigs
without locking and deletes child budgets/rate-limits directly, risking races
that leave orphans; instead, first select and lock the model-config rows for
this provider (using the transaction txDB with a FOR UPDATE/SELECT ... FOR
UPDATE equivalent) to prevent concurrent updates, then iterate each locked
TableModelConfig and call the existing DeleteModelConfig helper for each (rather
than manually deleting children and bulk-deleting parents) so the same
reconciliation/cleanup path is used and race conditions are avoided; ensure you
still run within ctx and txDB and handle DeleteModelConfig errors per-row.
In `@plugins/governance/store.go`:
- Around line 3557-3569: The aggregation currently only inspects the direct
modelConfig and global entries; update the budget aggregation to collect and
iterate the same model-config tiers used by enforcement by calling
collectModelConfigsFor(...) and respecting nonGlobalModelConfigScopeChain so
wildcard and VK-scoped wildcard configs are included; specifically, replace the
direct loop over modelConfig.Budgets with iterating over budgets from all
configs returned by collectModelConfigsFor (for each config, use its .Budgets
and existing gs.budgets lookup and budgetBaselines logic to compute
budgetPercent and update result.BudgetPercentUsed), and make the analogous
change in the second block (lines ~3606-3619) so both places use the same scope
chain.
- Around line 3118-3160: UpdateModelConfigInMemory currently only upserts
incoming clone.Budgets and clone.RateLimit, leaving removed/rotated children
behind; reconcile by deleting stale entries: before storing the new budgets,
collect the set of incoming budget IDs from clone.Budgets, iterate gs.budgets to
find budgets associated with this model config (use the same association key
logic you use when storing — e.g., modelConfigStoreKey/clone.ModelName or any
ModelConfigID field) and gs.budgets.Delete any stored budget whose ID is not in
the incoming set; for rate limits, check if an existing stored rate-limit ID for
this model config differs from clone.RateLimit.ID (or is nil when
clone.RateLimit is nil) and if so gs.rateLimits.Delete the old ID before storing
the new/updated rate limit; ensure you still preserve usage (copy
eb.CurrentUsage / erl.*) prior to deletion if you need to migrate counters, and
update gs.modelConfigs via modelConfigStoreKey as you already do.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2c3253ae-68cd-4125-9c80-ebd46c9fdb82
📒 Files selected for processing (14)
framework/configstore/migrations.goframework/configstore/rdb.goframework/configstore/tables/budget.goframework/configstore/tables/modelconfig.goplugins/governance/modelprovidergovernance_test.goplugins/governance/store.goplugins/governance/test_utils.gotransports/bifrost-http/handlers/governance.gotransports/bifrost-http/server/server.goui/app/workspace/model-limits/views/modelLimitSheet.tsxui/app/workspace/model-limits/views/modelLimitsTable.tsxui/app/workspace/virtual-keys/views/virtualKeysTable.tsxui/lib/store/apis/governanceApi.tsui/lib/types/governance.ts
f3ef789 to
d0ce4b3
Compare
a93d984 to
476a36b
Compare
d0ce4b3 to
46704bf
Compare
476a36b to
0afcab3
Compare
Merge activity
|
0afcab3 to
39828ee
Compare
| if err := migrationAddBudgetModelConfigIDColumn(ctx, db); err != nil { | ||
| return err | ||
| } | ||
| if err := migrationAddModelConfigCalendarAlignedColumn(ctx, db); err != nil { | ||
| return err | ||
| } | ||
| return nil |
There was a problem hiding this comment.
VK governance migration never runs on upgrade
migrationMigrateVirtualKeyGovernanceToModelConfigs is defined (line 4088) but never wired into triggerMigrations. On any existing deployment the migration is skipped, which causes three concrete failures:
- Existing VK-level budget rows keep
virtual_key_idset andmodel_config_id = NULL. No VK-scoped wildcard model configs are created for them. hydrateVKGovernance/hydrateVKListGovernancecallGetModelConfigwith VK scope → find nothing → executeapplyVKGovernanceFromWildcardswhich explicitly setsvk.Budgets = nilandvk.RateLimit = nil. EveryGET /api/governance/virtual-keysresponse returns empty governance for pre-existing VKs.- When a user edits an existing VK via the new API,
syncVKGovernanceToModelConfigscreates fresh wildcard model configs with new budget rows (different IDs). The old VK-level budgets are NOT deleted. BothCheckVirtualKeyBudget(readsvk.Budgets, which still contains the old rows viavirtual_key_id) andCheckVirtualKeyScopedModelBudget(reads the new model-config budgets) fire on every request, double-counting usage against two independent budget limits simultaneously.
The call must be inserted between migrationAddBudgetModelConfigIDColumn (which adds the required model_config_id column it depends on) and migrationAddModelConfigCalendarAlignedColumn.
…s table (#3939) ## Summary This PR migrates virtual key (VK) governance (budgets and rate limits) from being owned directly by VK and provider-config rows into VK-scoped all-models wildcard model configs. It also upgrades model configs from a single `budget_id` FK to a `has-many` `Budgets` relationship via `TableBudget.ModelConfigID`, enabling multiple budgets with distinct reset windows on a single model config. ## Changes - **New `governance_budgets.model_config_id` column**: Adds a `ModelConfigID` FK on `TableBudget`, making model configs the owner of budgets rather than the reverse. Three new migrations handle the column addition, backfill from the legacy `budget_id`, VK governance cutover, and a new `calendar_aligned` column on model configs. - **VK governance folded into wildcard model configs**: VK top-level budgets/rate-limits move to a `(scope=virtual_key, model_name='*', provider=NULL)` model config; per-provider-config budgets/rate-limits move to `(scope=virtual_key, model_name='*', provider=<provider>)` configs. `syncVKGovernanceToModelConfigs` and `upsertVKWildcard` handle create/update; `hydrateVKGovernance` / `hydrateVKListGovernance` reverse-map them back onto VK responses for display. - **Multi-budget enforcement**: `CheckModelBudget`, `CheckVirtualKeyScopedModelBudget`, `UpdateProviderAndModelBudgetUsageInMemory`, and `UpdateVirtualKeyScopedModelBudgetUsageInMemory` now iterate `mc.Budgets` instead of reading a single `BudgetID`. All budget checks block if any one budget is exceeded. - **`CollectApplicableGovernanceIDs` rewrite**: Uses `collectModelConfigsFor` across all four tiers (exact model+provider, model-only, all-models+provider, all-models wildcard) and the full VK scope chain, replacing the previous partial lookup. - **`DeleteModelConfig` / `DeleteVirtualKey` / `DeleteProvider` cleanup**: Now preload and delete all owned budgets (via `ModelConfigID`) in addition to the legacy single `BudgetID`. - **`UpdateModelConfig` association safety**: Uses `Omit(clause.Associations)` on save to prevent cascading saves from clobbering live budget usage counters. - **`calendar_aligned` on model configs**: Propagated from the owning VK for VK-scoped configs; stamped onto owned budgets via `AfterFind` and `rebuildInMemoryStructures` so the reset path reads the correct window. - **API shape changes**: `CreateModelConfigRequest.budget` → `budgets []CreateBudgetRequest`; `UpdateModelConfigRequest.budget` → `budgets []CreateBudgetRequest` (full desired set, reconciled server-side). `reconcileModelConfigBudgets` handles upsert/delete of the set. - **UI**: Model limit sheet replaced the single budget field with a `MultiBudgetLines` component. The model limits table now shows all budgets per config, a "Scope Target" column with a deep-link to the VK page, and calendar-alignment labels. VK table gains deep-link support via a `?vk=` query param consumed from the model limits table. - **Cache invalidation**: VK create/update/delete mutations now also invalidate `ModelConfigs`; model config mutations invalidate `VirtualKeys`; provider governance mutations invalidate `VirtualKeys`. - **Tests**: New unit tests cover multi-budget enforcement (one exceeded blocks, all within passes, all budgets bumped on usage update), no-double-count guard for VK governance budgets, and multi-budget VK-scoped model config blocking. ## Type of change - [ ] Bug fix - [x] Feature - [x] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [x] UI (React) - [ ] Docs ## How to test ```sh # Core/Transports go test ./framework/configstore/... ./plugins/governance/... ./transports/bifrost-http/... # UI cd ui pnpm i pnpm build ``` 1. Create a VK with top-level budgets and per-provider budgets via the API or UI. 2. Verify that `GET /api/governance/virtual-keys/:id` returns the budgets and rate limits hydrated from the VK-scoped wildcard model configs. 3. Verify that `GET /api/governance/model-configs` shows the VK-scoped wildcard rows with their owned budgets. 4. Make requests through the VK and confirm usage is charged to the wildcard model config budgets exactly once (not double-counted via both the VK hierarchy and scoped-model paths). 5. Delete the VK and confirm no orphaned budget or rate-limit rows remain. 6. In the UI, open Model Limits, confirm multi-budget lines render and the Scope Target column links to the correct VK. ## Breaking changes - [x] Yes - [ ] No The `CreateModelConfigRequest` and `UpdateModelConfigRequest` API shapes change: the single `budget` field is replaced by a `budgets` array. Callers using the single-budget field must migrate to the array form. Existing database rows are backfilled automatically by the migrations; the legacy `budget_id` column and `Budget` association are retained as inert for backward compatibility. ## Related issues ## Security considerations No new auth surfaces or secrets handling. Budget ownership is enforced via a `BeforeSave` hook that rejects a budget row with more than one owner FK set, preventing accidental cross-owner budget sharing. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Model configs now support multiple budgets for more granular control over API usage * Virtual-key governance is now organized within model configurations * Added deep-link support for virtual keys via URL parameter * Model limits table now displays "Scope Target" with clickable navigation * **Tests** * Added multi-budget governance test coverage <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…s table (#3939) ## Summary This PR migrates virtual key (VK) governance (budgets and rate limits) from being owned directly by VK and provider-config rows into VK-scoped all-models wildcard model configs. It also upgrades model configs from a single `budget_id` FK to a `has-many` `Budgets` relationship via `TableBudget.ModelConfigID`, enabling multiple budgets with distinct reset windows on a single model config. ## Changes - **New `governance_budgets.model_config_id` column**: Adds a `ModelConfigID` FK on `TableBudget`, making model configs the owner of budgets rather than the reverse. Three new migrations handle the column addition, backfill from the legacy `budget_id`, VK governance cutover, and a new `calendar_aligned` column on model configs. - **VK governance folded into wildcard model configs**: VK top-level budgets/rate-limits move to a `(scope=virtual_key, model_name='*', provider=NULL)` model config; per-provider-config budgets/rate-limits move to `(scope=virtual_key, model_name='*', provider=<provider>)` configs. `syncVKGovernanceToModelConfigs` and `upsertVKWildcard` handle create/update; `hydrateVKGovernance` / `hydrateVKListGovernance` reverse-map them back onto VK responses for display. - **Multi-budget enforcement**: `CheckModelBudget`, `CheckVirtualKeyScopedModelBudget`, `UpdateProviderAndModelBudgetUsageInMemory`, and `UpdateVirtualKeyScopedModelBudgetUsageInMemory` now iterate `mc.Budgets` instead of reading a single `BudgetID`. All budget checks block if any one budget is exceeded. - **`CollectApplicableGovernanceIDs` rewrite**: Uses `collectModelConfigsFor` across all four tiers (exact model+provider, model-only, all-models+provider, all-models wildcard) and the full VK scope chain, replacing the previous partial lookup. - **`DeleteModelConfig` / `DeleteVirtualKey` / `DeleteProvider` cleanup**: Now preload and delete all owned budgets (via `ModelConfigID`) in addition to the legacy single `BudgetID`. - **`UpdateModelConfig` association safety**: Uses `Omit(clause.Associations)` on save to prevent cascading saves from clobbering live budget usage counters. - **`calendar_aligned` on model configs**: Propagated from the owning VK for VK-scoped configs; stamped onto owned budgets via `AfterFind` and `rebuildInMemoryStructures` so the reset path reads the correct window. - **API shape changes**: `CreateModelConfigRequest.budget` → `budgets []CreateBudgetRequest`; `UpdateModelConfigRequest.budget` → `budgets []CreateBudgetRequest` (full desired set, reconciled server-side). `reconcileModelConfigBudgets` handles upsert/delete of the set. - **UI**: Model limit sheet replaced the single budget field with a `MultiBudgetLines` component. The model limits table now shows all budgets per config, a "Scope Target" column with a deep-link to the VK page, and calendar-alignment labels. VK table gains deep-link support via a `?vk=` query param consumed from the model limits table. - **Cache invalidation**: VK create/update/delete mutations now also invalidate `ModelConfigs`; model config mutations invalidate `VirtualKeys`; provider governance mutations invalidate `VirtualKeys`. - **Tests**: New unit tests cover multi-budget enforcement (one exceeded blocks, all within passes, all budgets bumped on usage update), no-double-count guard for VK governance budgets, and multi-budget VK-scoped model config blocking. ## Type of change - [ ] Bug fix - [x] Feature - [x] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [x] UI (React) - [ ] Docs ## How to test ```sh # Core/Transports go test ./framework/configstore/... ./plugins/governance/... ./transports/bifrost-http/... # UI cd ui pnpm i pnpm build ``` 1. Create a VK with top-level budgets and per-provider budgets via the API or UI. 2. Verify that `GET /api/governance/virtual-keys/:id` returns the budgets and rate limits hydrated from the VK-scoped wildcard model configs. 3. Verify that `GET /api/governance/model-configs` shows the VK-scoped wildcard rows with their owned budgets. 4. Make requests through the VK and confirm usage is charged to the wildcard model config budgets exactly once (not double-counted via both the VK hierarchy and scoped-model paths). 5. Delete the VK and confirm no orphaned budget or rate-limit rows remain. 6. In the UI, open Model Limits, confirm multi-budget lines render and the Scope Target column links to the correct VK. ## Breaking changes - [x] Yes - [ ] No The `CreateModelConfigRequest` and `UpdateModelConfigRequest` API shapes change: the single `budget` field is replaced by a `budgets` array. Callers using the single-budget field must migrate to the array form. Existing database rows are backfilled automatically by the migrations; the legacy `budget_id` column and `Budget` association are retained as inert for backward compatibility. ## Related issues ## Security considerations No new auth surfaces or secrets handling. Budget ownership is enforced via a `BeforeSave` hook that rejects a budget row with more than one owner FK set, preventing accidental cross-owner budget sharing. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Model configs now support multiple budgets for more granular control over API usage * Virtual-key governance is now organized within model configurations * Added deep-link support for virtual keys via URL parameter * Model limits table now displays "Scope Target" with clickable navigation * **Tests** * Added multi-budget governance test coverage <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary This PR bumps the Go toolchain version from `1.26.3` to `1.26.4` across all modules and CI workflows, and cuts a new release (`core` v1.5.17, `framework` v1.3.17, `transports` v1.5.9, `plugins/compat` v0.1.16, `plugins/governance` v1.5.17, and associated plugin versions) incorporating a large batch of features and fixes accumulated since the previous release. ## Changes - **Go 1.26.4** — Updated `go-version` in all GitHub Actions workflows (`e2e-tests`, `helm-release`, `pr-tests`, `release-cli`, `release-pipeline`, `snyk`) and all `go.mod` files (core, framework, transports, cli, all plugins, examples, and test modules). - **Core (v1.5.17)** — OpenAI compaction support, multi-customer logs and usage tracking, multiple team/business unit support, `request_headers` wildcard pattern capture for OTel and Maxim plugins, xAI `x_search` tool, fetch URL validation with SSRF hardening, `file://` pricing URL scheme, virtual key provider fan-out filtering, and a broad set of fixes including Anthropic prompt cache key, empty thinking block stripping, OpenAI stream usage event cleanup, Gemini numeric schema constraints, stale connection retries, Azure Claude diagnostic strip, and passthrough budget handling. - **Framework (v1.3.17)** — Scope-aware budgets and limits wired from model configs, provider-level governance, multiple customer budget support with `calendar_aligned` windows, paginated virtual key fetch, `config.json` source-of-truth flow, FTS index cap reduction, sync worker drift fix, cascade deletes for model configs, and high-scale virtual key flow improvements. - **Transports (v1.5.9)** — Full changelog covering all of the above plus UI improvements (log navigation, customer detail sheet, `BudgetDisplay` component, inline loading shell, materialized view alias), SCIM provisioning fields, Helm/config schema additions (`roles`, `per_user_oauth`), client IP resolution from forwarded headers, and dependency upgrades (`recharts` to 3.8.1, `golang.org/x` CVE remediation). - **Plugins** — `governance` v1.5.17 adds team budget/rate-limit exporters, ghost node reconciliation fix, and VK double usage counting fix; `logging` v1.5.17 adds wildcard header capture and file attachment rendering; `otel` v1.2.17 adds `disable_content_logging` and multiple collectors support; `maxim` v1.6.17 adds `request_headers` wildcard capture; `compat` v0.1.16 fixes `max_tokens` preservation during param filtering. ## Type of change - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [x] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [x] Providers/Integrations - [x] Plugins - [x] UI (React) - [ ] Docs ## How to test ```sh # Verify Go version go version # should report go1.26.4 # Run core tests cd core && go test ./... # Run framework tests cd framework && go test ./... # Run transports tests cd transports && go test ./... # Run plugin tests cd plugins/governance && go test ./... cd plugins/logging && go test ./... cd plugins/otel && go test ./... # UI cd ui pnpm i pnpm build pnpm test ``` ## Breaking changes - [ ] Yes - [x] No ## Related issues #4053, #4066, #4041, #4012, #3976, #3947, #3991, #4045, #3957, #3938, #3937, #3939, #3981, #3998, #3997, #4092, #4091, #4079, #4080, #4086, #3929, #3994, #4028, #3970, #3919, #3861, #3664, #3999, #4088, #4070, #4051, #4043, #4057, #4023, #3941, #3955, #4024, #3956, #3967, #3925, #3992, #3900 ## Security considerations - Fetch URL validation hardened against SSRF by tightening IP checks for private networks and link-local addresses (#4092, #3947, #3991). - Transitive `golang.org/x` dependencies (crypto, net, sys, text) bumped to address Docker Scout CVEs (#3900). ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * OpenAI compaction, multi-customer/team logstore support, request-header wildcard capture, enhanced governance (provider-level & scope-aware limits), disable-content-logging option, support for multiple OpenTelemetry collectors, SSRF hardening and URL validation. * **Chores** * Bumped Go toolchain across modules and updated component/plugin version releases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…s table (#3939) ## Summary This PR migrates virtual key (VK) governance (budgets and rate limits) from being owned directly by VK and provider-config rows into VK-scoped all-models wildcard model configs. It also upgrades model configs from a single `budget_id` FK to a `has-many` `Budgets` relationship via `TableBudget.ModelConfigID`, enabling multiple budgets with distinct reset windows on a single model config. ## Changes - **New `governance_budgets.model_config_id` column**: Adds a `ModelConfigID` FK on `TableBudget`, making model configs the owner of budgets rather than the reverse. Three new migrations handle the column addition, backfill from the legacy `budget_id`, VK governance cutover, and a new `calendar_aligned` column on model configs. - **VK governance folded into wildcard model configs**: VK top-level budgets/rate-limits move to a `(scope=virtual_key, model_name='*', provider=NULL)` model config; per-provider-config budgets/rate-limits move to `(scope=virtual_key, model_name='*', provider=<provider>)` configs. `syncVKGovernanceToModelConfigs` and `upsertVKWildcard` handle create/update; `hydrateVKGovernance` / `hydrateVKListGovernance` reverse-map them back onto VK responses for display. - **Multi-budget enforcement**: `CheckModelBudget`, `CheckVirtualKeyScopedModelBudget`, `UpdateProviderAndModelBudgetUsageInMemory`, and `UpdateVirtualKeyScopedModelBudgetUsageInMemory` now iterate `mc.Budgets` instead of reading a single `BudgetID`. All budget checks block if any one budget is exceeded. - **`CollectApplicableGovernanceIDs` rewrite**: Uses `collectModelConfigsFor` across all four tiers (exact model+provider, model-only, all-models+provider, all-models wildcard) and the full VK scope chain, replacing the previous partial lookup. - **`DeleteModelConfig` / `DeleteVirtualKey` / `DeleteProvider` cleanup**: Now preload and delete all owned budgets (via `ModelConfigID`) in addition to the legacy single `BudgetID`. - **`UpdateModelConfig` association safety**: Uses `Omit(clause.Associations)` on save to prevent cascading saves from clobbering live budget usage counters. - **`calendar_aligned` on model configs**: Propagated from the owning VK for VK-scoped configs; stamped onto owned budgets via `AfterFind` and `rebuildInMemoryStructures` so the reset path reads the correct window. - **API shape changes**: `CreateModelConfigRequest.budget` → `budgets []CreateBudgetRequest`; `UpdateModelConfigRequest.budget` → `budgets []CreateBudgetRequest` (full desired set, reconciled server-side). `reconcileModelConfigBudgets` handles upsert/delete of the set. - **UI**: Model limit sheet replaced the single budget field with a `MultiBudgetLines` component. The model limits table now shows all budgets per config, a "Scope Target" column with a deep-link to the VK page, and calendar-alignment labels. VK table gains deep-link support via a `?vk=` query param consumed from the model limits table. - **Cache invalidation**: VK create/update/delete mutations now also invalidate `ModelConfigs`; model config mutations invalidate `VirtualKeys`; provider governance mutations invalidate `VirtualKeys`. - **Tests**: New unit tests cover multi-budget enforcement (one exceeded blocks, all within passes, all budgets bumped on usage update), no-double-count guard for VK governance budgets, and multi-budget VK-scoped model config blocking. ## Type of change - [ ] Bug fix - [x] Feature - [x] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [x] UI (React) - [ ] Docs ## How to test ```sh # Core/Transports go test ./framework/configstore/... ./plugins/governance/... ./transports/bifrost-http/... # UI cd ui pnpm i pnpm build ``` 1. Create a VK with top-level budgets and per-provider budgets via the API or UI. 2. Verify that `GET /api/governance/virtual-keys/:id` returns the budgets and rate limits hydrated from the VK-scoped wildcard model configs. 3. Verify that `GET /api/governance/model-configs` shows the VK-scoped wildcard rows with their owned budgets. 4. Make requests through the VK and confirm usage is charged to the wildcard model config budgets exactly once (not double-counted via both the VK hierarchy and scoped-model paths). 5. Delete the VK and confirm no orphaned budget or rate-limit rows remain. 6. In the UI, open Model Limits, confirm multi-budget lines render and the Scope Target column links to the correct VK. ## Breaking changes - [x] Yes - [ ] No The `CreateModelConfigRequest` and `UpdateModelConfigRequest` API shapes change: the single `budget` field is replaced by a `budgets` array. Callers using the single-budget field must migrate to the array form. Existing database rows are backfilled automatically by the migrations; the legacy `budget_id` column and `Budget` association are retained as inert for backward compatibility. ## Related issues ## Security considerations No new auth surfaces or secrets handling. Budget ownership is enforced via a `BeforeSave` hook that rejects a budget row with more than one owner FK set, preventing accidental cross-owner budget sharing. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Model configs now support multiple budgets for more granular control over API usage * Virtual-key governance is now organized within model configurations * Added deep-link support for virtual keys via URL parameter * Model limits table now displays "Scope Target" with clickable navigation * **Tests** * Added multi-budget governance test coverage <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## ✨ Features - **OpenAI Compaction** — Added OpenAI conversation compaction support across core, framework, logging, and the API surface (#4053) - **Multi-Customer & Org Hierarchy** — Logs and usage tracking now support multiple customers, teams, and business units, including business unit CRUD, team assignment, and governance endpoints in the OpenAPI spec (#4066, #4041, #4082) - **Provider-Level Governance** — Budgets & limits are now scope-aware and can be applied at the virtual-key top level and per provider, wired from the model configs table, with UI filters for scope and providers (#3938, #3937, #3939, #3981, #3962) - **Customer Budgets** — Customers support multiple budgets and `calendar_aligned` budget windows (#3998, #3997) - **Virtual Key Attribution & Controls** — Added a `created_by` user attribution column and a `blacklisted_models` column for virtual key provider configs (#3672, #3653) - **Request Header Capture** — OTel and Maxim observability plugins capture `request_headers` by pattern, with wildcard support (e.g. `x-custom-*`); logging gained the same wildcard header capture (#4012, #3958) - **OTel Content Controls & Collectors** — New `disable_content_logging` option drops message/tool content from exported spans, plus support for multiple OTel collectors (#4064, #3894) - **xAI x_search** — Added xAI `x_search` tool support (#3976) - **URL Validation** — Added fetch URL validation with private-network configuration and link-local blocking (#3947, #3991) - **File Scheme Pricing URLs** — Pricing source URLs now accept the `file://` scheme for air-gapped and self-hosted deployments (#4045) - **Paginated Virtual Keys** — Virtual key fetching is paginated to handle deployments with very large numbers of keys (#3957) - **Client IP Resolution** — Resolve client IP from `X-Forwarded-For`/`X-Real-IP` headers - **SCIM Provisioning** — Added `attributeType`/`attributeValue` SCIM provisioning fields - **Helm/Config Schema** — Added `roles` RBAC governance config and `per_user_oauth` MCP auth to the Helm chart and config schema (#4004, #4009) - **Log Navigation UI** — Added a "View logs" menu item to customer, team, and virtual key tables, clickable links in log detail views, a customer detail sheet, and a reusable `BudgetDisplay` component (#4073, #4054, #4026, #4055) - **Faster First Paint** — Added an inline loading shell to `#root` before React mounts (#4063) - **Materialized View Alias** — Added an `alias` column to the materialized view with filter support (#4078) ## 🐞 Fixed - **Fetch URL IP Checks** — Hardened fetch URL IP checks against SSRF (#4092) - **Mantle Model Matching** — Broadened Mantle model matching to all `gpt` variants (#4091) - **Empty Thinking Blocks** — Strip thinking blocks when the signature is empty (#4079) - **OpenAI Stream Usage** — Removed usage from the `responses.created` event in the OpenAI stream (#4080) - **Prompt Cache Key** — Set the prompt cache key from the Anthropic integration (#4086) - **Upstream Failure Status** — Map upstream connection failures to 502 instead of 400 (#3929) (thanks [@chris-colinsky](https://github.com/chris-colinsky)!) - **Gemini Schema Constraints** — Accept numeric schema integer constraints for Gemini (#3994) (thanks [@yanhao98](https://github.com/yanhao98)!) - **Files Provider Param** — Accept the `?provider=` query param on `GET /v1/files` (#3971) (thanks [@alexef](https://github.com/alexef)!) - **Optional Batch Model** — Made the `model` field optional on `POST /v1/batches` (#3973) (thanks [@alexef](https://github.com/alexef)!) - **Helm Azure Config** — Added missing `azure_key_config` fields to the Helm schema (#3996) (thanks [@axelray-dev](https://github.com/axelray-dev)!) - **Text Completion Chunk Model** — Added the missing `Model` field to `TextCompletionChunkResponse` (#3970) (thanks [@kuishou68](https://github.com/kuishou68)!) - **MCP Inline stdio Env** — MCP stdio server configs accept inline environment variable assignments (#3861) (thanks [@Shushmitaaaa](https://github.com/Shushmitaaaa)!) - **Orphaned Tool Results** — Orphaned tool results in the OpenAI to Anthropic conversion flow are no longer rejected by the Anthropic API (#3919) - **Node Usage Reconciliation** — Added a monotonic `inc_number` log cursor so node usage reconciliation does not skip late async log writes (#3664) - **Bedrock Output Assessments** — Corrected the type of `outputAssessments` in Bedrock responses (#4028) - **Model Pool Pricing Reloads** — Preserve non-pricing model pool entries across pricing reloads (#3999) - **Ghost Node Reconciliation** — Replicate the VK hierarchy flow for ghost node reconciliation (#4088) - **VK Double Usage Counting** — Fixed double usage counting when creating a virtual key (#4070) - **Model Config Lifecycle** — Cascade deletes for model configs and removal of stale in-memory model configs (#4051, #4043) - **FTS Index Cap** — Reduced the FTS index `left()` cap from 800k to 250k chars to stay within the tsvector limit (#4057) - **Sync Worker Drift** — Reduced the sync worker ticker period to 5m to prevent threshold drift (#4023) - **Passthrough** — Fixed passthrough budgets, gated passthrough models per VK, model extraction for Azure passthrough, and restricted fallbacks/provider selection to the VK boundary (#3941, #3988, #3983, #3924) - **Provider Response Headers** — Strip provider response headers and add a content-type filter (#3955, #4024) - **Stream Handling** — Drain non-SSE stream readers and retry stale connections (#3956, #3967) - **Azure Claude** — Strip Azure diagnostic property for Claude models (#3925) - **Compat max_tokens** — Preserve chat `max_tokens` during param filtering (#3992) - **Raw Request Flag** — Removed the raw request flag from providers that don't support it (#4058) - **UI Fixes** — Standardized page container layout, virtual key model configs UI, and dashboard chart tooltips (#4046, #4052, #4044) ## 🔧 Maintenance - **Dependency Upgrades** — Bumped transitive `golang.org/x` dependencies (crypto, net, sys, text) for Docker Scout CVE remediation and `recharts` to 3.8.1; cascaded version bumps across all modules (#3900, #4003)
## Summary After governance data moved into VK-scoped wildcard model configs (introduced in PR #3939), the `getVirtualKeyQuota` endpoint and `rotateVirtualKeyByID` continued reading the now-empty direct `Budgets` and `RateLimit` relationships on the virtual key, causing both endpoints to report no governance data. This PR fixes both call sites by calling `hydrateVKGovernance` to reverse-map governance from the VK-scoped model configs back onto the response. ## Changes - `rotateVirtualKeyByID` now calls `hydrateVKGovernance` on the preloaded virtual key before returning it, ensuring rotated keys carry their governance state. - `getVirtualKeyQuota` now calls `hydrateVKGovernance` on the fetched virtual key before serializing the response, so budgets and rate limits sourced from model configs are included in the quota payload. - Added `GetModelConfig` to `mockRotateConfigStore` so existing rotate tests can exercise the hydration path. - Added `mockQuotaConfigStore` with `GetVirtualKeyQuotaByValue` and `GetModelConfig` to back new quota endpoint tests. - Added `TestGetVirtualKeyQuota_HydratesBudgetsFromModelConfigs` — the primary regression test asserting that both VK-level and per-provider budgets/rate limits are hydrated from model configs. - Added `TestGetVirtualKeyQuota_NoGovernanceReturnsEmpty` — verifies a VK with no model configs returns 200 with empty governance fields rather than stale data. - Added `TestGetVirtualKeyQuota_MissingHeaderReturns401` and `TestGetVirtualKeyQuota_NotFoundReturns401` for auth boundary coverage. - Added `TestGetVirtualKeyQuota_EndToEndWithRealStore` — a full SQLite-backed integration test that creates a VK, writes VK-scoped and per-provider model configs, hits the quota endpoint, and asserts the correct budget values are returned. ## Type of change - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./transports/bifrost-http/handlers/... -run TestGetVirtualKeyQuota -v go test ./transports/bifrost-http/handlers/... -v ``` The new tests will fail against the unpatched handler and pass with this fix applied. `TestGetVirtualKeyQuota_EndToEndWithRealStore` exercises the full path against a real SQLite store and is the definitive regression guard. ## Breaking changes - [ ] Yes - [x] No ## Related issues Regression introduced by PR #3939 (governance migration to VK-scoped model configs). ## Security considerations No changes to authentication or authorization logic. The `getVirtualKeyQuota` endpoint's existing 401 path for missing or unknown VK headers is preserved and covered by new tests. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Virtual Key APIs now populate governance data (budgets, rate limits, provider configs) across additional response paths; model-config fetch failures are logged. * **Tests** * Added comprehensive quota/governance hydration tests (unit and end-to-end), covering missing-header and not-found 401 cases and real-store E2E scenarios; extended API collection checks. * **Documentation** * Updated changelog/release notes and added rollback instructions for reverting to v1.5.8. * **Chores** * Bumped transport version to v1.5.11 and updated test module references. <!-- end of auto-generated comment: release notes by coderabbit.ai -->

Summary
This PR migrates virtual key (VK) governance (budgets and rate limits) from being owned directly by VK and provider-config rows into VK-scoped all-models wildcard model configs. It also upgrades model configs from a single
budget_idFK to ahas-manyBudgetsrelationship viaTableBudget.ModelConfigID, enabling multiple budgets with distinct reset windows on a single model config.Changes
governance_budgets.model_config_idcolumn: Adds aModelConfigIDFK onTableBudget, making model configs the owner of budgets rather than the reverse. Three new migrations handle the column addition, backfill from the legacybudget_id, VK governance cutover, and a newcalendar_alignedcolumn on model configs.(scope=virtual_key, model_name='*', provider=NULL)model config; per-provider-config budgets/rate-limits move to(scope=virtual_key, model_name='*', provider=<provider>)configs.syncVKGovernanceToModelConfigsandupsertVKWildcardhandle create/update;hydrateVKGovernance/hydrateVKListGovernancereverse-map them back onto VK responses for display.CheckModelBudget,CheckVirtualKeyScopedModelBudget,UpdateProviderAndModelBudgetUsageInMemory, andUpdateVirtualKeyScopedModelBudgetUsageInMemorynow iteratemc.Budgetsinstead of reading a singleBudgetID. All budget checks block if any one budget is exceeded.CollectApplicableGovernanceIDsrewrite: UsescollectModelConfigsForacross all four tiers (exact model+provider, model-only, all-models+provider, all-models wildcard) and the full VK scope chain, replacing the previous partial lookup.DeleteModelConfig/DeleteVirtualKey/DeleteProvidercleanup: Now preload and delete all owned budgets (viaModelConfigID) in addition to the legacy singleBudgetID.UpdateModelConfigassociation safety: UsesOmit(clause.Associations)on save to prevent cascading saves from clobbering live budget usage counters.calendar_alignedon model configs: Propagated from the owning VK for VK-scoped configs; stamped onto owned budgets viaAfterFindandrebuildInMemoryStructuresso the reset path reads the correct window.CreateModelConfigRequest.budget→budgets []CreateBudgetRequest;UpdateModelConfigRequest.budget→budgets []CreateBudgetRequest(full desired set, reconciled server-side).reconcileModelConfigBudgetshandles upsert/delete of the set.MultiBudgetLinescomponent. The model limits table now shows all budgets per config, a "Scope Target" column with a deep-link to the VK page, and calendar-alignment labels. VK table gains deep-link support via a?vk=query param consumed from the model limits table.ModelConfigs; model config mutations invalidateVirtualKeys; provider governance mutations invalidateVirtualKeys.Type of change
Affected areas
How to test
GET /api/governance/virtual-keys/:idreturns the budgets and rate limits hydrated from the VK-scoped wildcard model configs.GET /api/governance/model-configsshows the VK-scoped wildcard rows with their owned budgets.Breaking changes
The
CreateModelConfigRequestandUpdateModelConfigRequestAPI shapes change: the singlebudgetfield is replaced by abudgetsarray. Callers using the single-budget field must migrate to the array form. Existing database rows are backfilled automatically by the migrations; the legacybudget_idcolumn andBudgetassociation are retained as inert for backward compatibility.Related issues
Security considerations
No new auth surfaces or secrets handling. Budget ownership is enforced via a
BeforeSavehook that rejects a budget row with more than one owner FK set, preventing accidental cross-owner budget sharing.Checklist
docs/contributing/README.mdand followed the guidelinesSummary by CodeRabbit
Release Notes
New Features
Tests