Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions docs/features/governance/budget-and-limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,30 @@ Budgets and rate limits support flexible reset durations:

By default, a budget **rolls**: after `reset_duration` elapses since `last_reset`, usage resets. With **`calendar_aligned`: `true`**, the budget resets at the **start of each calendar period in UTC** instead (same instant for every customer of that configuration).

**Supported `reset_duration` suffixes:** only day (`d`), week (`w`), month (`M`), quarter (`Q`), and year (`Y`). Examples: `1d` → midnight UTC each day; `1w` → Monday 00:00 UTC each week; `1M` → first day of each month; `1Q` → first day of the fiscal quarter; `1Y` → January 1 each year. Sub-day durations (for example `1h`, `30m`) **cannot** use calendar alignment; the API rejects invalid combinations.
**Supported `reset_duration` suffixes:** only day (`d`), week (`w`), month (`M`), quarter (`Q`), and year (`Y`). Examples: `1d` → midnight UTC each day; `1w` → Monday 00:00 UTC each week; `1M` → first day of each month; `1Q` → first day of the fiscal quarter; `1Y` → January 1 each year. Sub-day durations (for example `1h`, `30m`) have no calendar boundary to snap to. Setting `calendar_aligned` alongside one is **accepted rather than rejected**, and simply has no effect: that window keeps resetting on its rolling schedule.

Calendar alignment applies to budgets on **customers**, **teams**, **virtual keys**, and **per–provider-config** budgets. You can set it when creating a budget (`calendar_aligned` on create) or toggle it on update (`calendar_aligned` on the budget in `PUT` requests). Turning calendar alignment **on** for an existing budget resets **current usage to zero** and snaps **`last_reset`** to the current period start.
Calendar alignment applies to budgets on **customers**, **teams**, **virtual keys**, and **per-provider-config** budgets. `calendar_aligned` is an **owner-level** field, not a per-budget one: send it at the top level of the customer, team, virtual key, or provider-governance request body, on both `POST` (create) and `PUT` (update). Omitting it on a `PUT` leaves the current setting unchanged.

The flag applies to **everything that owner resets**, not to budgets alone. That owner's [rate limits](#rate-limiting) follow the same rule, so its token and request counters align too.

It sets the alignment **mode**, not a shared reset instant. An owner holds one window per budget plus one each for its token and request counters, and every one of them keeps its own `reset_duration` and its own `last_reset`. Each aligns to **its own** boundary: on an aligned owner, a `1M` budget resets on the 1st while a `1d` token limit resets at midnight. They do not reset together.

<Note>
Alignment only applies to durations that have a calendar boundary: `1d`, `1w`, `1M`, `1Q` and `1Y`. A **sub-day** window such as a `1h` request limit stays on its rolling schedule even when its owner is aligned, and the flag changes nothing for it.
</Note>

Turning calendar alignment **on** for an existing owner never moves `last_reset` backwards, so an already-open window is not re-dated. What happens next is decided **per window**, against that window's own boundary:

- **Window opened after its most recent boundary** - nothing resets now. It keeps its start and its accumulated usage, and its first aligned reset happens at its next boundary.
- **Window opened before its most recent boundary** - it is **already due** under the new rule, so the next reset evaluation resets it and **clears its current usage**: spend for a budget, the token or request counter for a rate limit. For a monthly window switched on partway through a month, this is the common case.

A reset evaluation is not only the background sweep. The same due-or-not check runs again on the request path, just before a request's usage is recorded, so an already-due window is cleared by the next request even if no sweep has fired yet.

Because each window is judged separately, one switch can leave a `1d` limit untouched while clearing a `1M` budget on the same owner.

<Warning>
Enabling alignment on an owner with accumulated usage can therefore clear that usage almost immediately, and which of its windows are affected depends on each one's own duration and start. There is generally no moment when every window is freshly reset, so to preserve them all set `calendar_aligned` at **create** time and avoid the switch-over entirely. To protect one specific window, enable alignment just after **that** window resets.
</Warning>

### Quarterly budgets and fiscal quarters

Expand Down
2 changes: 1 addition & 1 deletion docs/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -82990,7 +82990,7 @@
"boolean",
"null"
],
"description": "Set to true or false to enable or disable calendar-aligned resets. Only valid with reset durations that use day, week, month, quarter, or year suffixes (`d`, `w`, `M`, `Q`, `Y`); sub-day durations (e.g. `1h`, `30m`) are invalid with calendar alignment and the API rejects that combination. When enabling on an existing budget, current usage is reset to zero and last_reset snaps to the current period start. Changing quarter_start_month on a calendar-aligned quarterly budget does not move last_reset directly; the new definition shifts where the current window starts, so the budget converges onto the new fiscal boundary on the next reset tick.\n"
"description": "Set to true or false to enable or disable calendar-aligned resets. Only valid with reset durations that use day, week, month, quarter, or year suffixes (`d`, `w`, `M`, `Q`, `Y`); sub-day durations (e.g. `1h`, `30m`) are invalid with calendar alignment and the API rejects that combination. When enabling on an existing budget, alignment takes effect from the next period: the current window keeps its start and its accumulated usage, and the first aligned reset happens at the next boundary. Changing quarter_start_month on a calendar-aligned quarterly budget does not move last_reset directly; the new definition shifts where the current window starts, so the budget converges onto the new fiscal boundary on the next reset tick.\n"
}
}
},
Expand Down
5 changes: 3 additions & 2 deletions docs/openapi/schemas/management/governance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,9 @@ UpdateBudgetRequest:
Set to true or false to enable or disable calendar-aligned resets. Only valid with reset durations
that use day, week, month, quarter, or year suffixes (`d`, `w`, `M`, `Q`, `Y`); sub-day durations
(e.g. `1h`, `30m`) are invalid with calendar alignment and the API rejects that combination. When
enabling on an existing budget, current usage is reset to zero and last_reset snaps to the current
period start. Changing quarter_start_month on a calendar-aligned quarterly budget does not move
enabling on an existing budget, alignment takes effect from the next period: the current window
keeps its start and its accumulated usage, and the first aligned reset happens at the next
boundary. Changing quarter_start_month on a calendar-aligned quarterly budget does not move
last_reset directly; the new definition shifts where the current window starts, so the budget
converges onto the new fiscal boundary on the next reset tick.

Expand Down
163 changes: 163 additions & 0 deletions plugins/governance/budgetcycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,3 +841,166 @@ func TestDumpBudgetsCannotUndoOperatorReset(t *testing.T) {
assert.Equal(t, 7.5, persisted.CurrentUsage,
"the dump after a reset must resume persisting usage, or a reset would stop accounting for good")
}

// TestEnablingCalendarAlignmentCanResetAtTheBoundaryAlreadyPassed records what
// enabling alignment actually does today, which is not what this feature's docs
// describe. It is a characterization test, not an endorsement.
//
// budgetResetTarget returns WindowStart(now) whenever that is after LastReset, and
// for an aligned budget WindowStart is the most recent calendar boundary. So a
// budget whose window opened before that boundary is already due the moment
// alignment is switched on, and the next sweep clears its usage.
//
// The documented promise is that alignment applies from the next period and the
// current window keeps its usage. That holds only when LastReset is newer than the
// most recent boundary, which is the case the existing coverage exercises: it
// creates the budget at test time, so LastReset is always inside the current
// period. Both cases are pinned below so the difference is visible.
//
// Closing the gap needs either an activation marker or a dedicated write path,
// because UpdateBudget copies last_reset back from the stored row on every config
// write. Tracked as follow-up work; until then the docs describe this behaviour.
func TestEnablingCalendarAlignmentCanResetAtTheBoundaryAlreadyPassed(t *testing.T) {
store := newStandaloneStore(t)
now := time.Date(2026, time.February, 5, 12, 0, 0, 0, time.UTC)
monthStart := time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC)

alignedBudget := func(lastReset time.Time) *configstoreTables.TableBudget {
return &configstoreTables.TableBudget{
ID: "align-boundary-budget",
MaxLimit: 100,
CurrentUsage: 42,
ResetDuration: "1M",
IsCalendarAligned: true,
CreatedAt: lastReset,
LastReset: lastReset,
}
}

t.Run("a window opened before the boundary is due at once", func(t *testing.T) {
target := store.budgetResetTarget(alignedBudget(time.Date(2026, time.January, 10, 9, 0, 0, 0, time.UTC)), now)
require.NotNil(t, target,
"current behaviour: the budget is due immediately, so its $42 of usage is cleared on the next reset evaluation - the sweep or the next request, whichever lands first")
assert.True(t, target.Equal(monthStart),
"the reset lands on the boundary that already passed, not on the next one")
})

// Being due is evaluated on two independent paths, not one. The subtest above
// asks budgetResetTarget the question the 10s ticker asks; BumpBudgetUsage asks
// the identical question on the request path and resets before recording the
// new cost. So an already-due window is cleared by the very next request even
// on a node whose sweep has not fired yet, which is why the transition is
// documented as the next reset *evaluation* rather than the next sweep.
t.Run("the request path clears an already-due window without any sweep", func(t *testing.T) {
ctx := context.Background()
budget := alignedBudget(time.Date(2026, time.January, 10, 9, 0, 0, 0, time.UTC))
budget.ID = "align-boundary-request-path-budget"
openedAt := budget.LastReset
store.budgets.Store(budget.ID, budget)

// No sweepBudgetAt call anywhere in this subtest: the bump is the only
// thing that touches the budget.
require.NoError(t, store.BumpBudgetUsage(ctx, budget.ID, 2.5))

bumped := store.LoadBudget(ctx, budget.ID)
require.NotNil(t, bumped, "expected the budget to remain loaded after the bump")
assert.Equal(t, 2.5, bumped.CurrentUsage,
"the 42 already accumulated was cleared by the request itself rather than carried forward, so the new window holds only this request's cost")
assert.True(t, bumped.LastReset.After(openedAt),
"the request path advanced the window to its boundary, exactly as a sweep would")
})

t.Run("a window opened after the boundary is left alone", func(t *testing.T) {
target := store.budgetResetTarget(alignedBudget(time.Date(2026, time.February, 3, 9, 0, 0, 0, time.UTC)), now)
assert.Nil(t, target,
"this is the case the docs describe, and the only one existing coverage reaches")
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// calendar_aligned is an owner-level flag: the same switch drives the owner's
// rate limits, and rateLimitResetTarget applies the identical
// "boundary after lastReset" rule. Pinned here so the documented transition
// cannot claim to cover rate limits while only budgets are actually checked -
// and so the eventual behaviour fix is reminded it owes them the same rule.
t.Run("rate limits carry the same transition", func(t *testing.T) {
duration := "1M"

before := store.rateLimitResetTarget(&duration, true,
time.Time{}, time.Date(2026, time.January, 10, 9, 0, 0, 0, time.UTC), now)
require.NotNil(t, before,
"a rate-limit window opened before the boundary is due at once, exactly like a budget")
assert.True(t, before.Equal(monthStart),
"the reset lands on the boundary that already passed")

after := store.rateLimitResetTarget(&duration, true,
time.Time{}, time.Date(2026, time.February, 3, 9, 0, 0, 0, time.UTC), now)
assert.Nil(t, after,
"a window opened after the boundary is left alone, exactly like a budget")
})

// One owner holds several windows on independent cadences: every budget has its
// own ResetDuration, and a rate limit has two more in TokenResetDuration and
// RequestResetDuration, each with its own LastReset. The owner-level flag picks
// the alignment *mode*; it does not give them a shared boundary. Pinned because
// the documented behaviour is easy to state as "everything snaps together",
// which is wrong in both directions below.
t.Run("each window aligns on its own duration", func(t *testing.T) {
lastReset := time.Date(2026, time.January, 10, 9, 0, 0, 0, time.UTC)
monthly, daily := "1M", "1d"

monthlyTarget := store.rateLimitResetTarget(&monthly, true, time.Time{}, lastReset, now)
dailyTarget := store.rateLimitResetTarget(&daily, true, time.Time{}, lastReset, now)
require.NotNil(t, monthlyTarget)
require.NotNil(t, dailyTarget)
assert.True(t, monthlyTarget.Equal(monthStart),
"a monthly window aligns to the month boundary")
assert.True(t, dailyTarget.Equal(time.Date(2026, time.February, 5, 0, 0, 0, 0, time.UTC)),
"a daily window aligns to midnight, not to the month boundary the budget uses")
assert.False(t, monthlyTarget.Equal(*dailyTarget),
"two windows on one aligned owner do not share a boundary")
})

// A sub-day counter has no calendar boundary to snap to, so rateLimitResetTarget
// falls through to the rolling branch and the owner-level flag changes nothing
// for it. Documenting alignment as owner-wide without this exception promises a
// behaviour the code does not have.
t.Run("sub-day durations stay rolling even when aligned", func(t *testing.T) {
hourly := "1h"
anchor := time.Date(2026, time.February, 5, 9, 30, 0, 0, time.UTC)

aligned := store.rateLimitResetTarget(&hourly, true, anchor, anchor, now)
rolling := store.rateLimitResetTarget(&hourly, false, anchor, anchor, now)
require.NotNil(t, aligned)
require.NotNil(t, rolling)
assert.True(t, aligned.Equal(*rolling),
"calendar_aligned is inert on a sub-day window: it resets on its rolling anchor either way")
assert.False(t, aligned.Equal(time.Date(2026, time.February, 5, 0, 0, 0, 0, time.UTC)),
"and specifically it does not snap to midnight")
})

// The combination is accepted, not refused. BeforeSave validates owner count,
// duration format, a positive duration, max_limit, the override fields and
// reset_config - and nothing ties alignment to the duration, at the table layer
// or in the handlers. So "sub-day plus aligned" persists happily and is simply
// ignored at reset time, which is what the docs have to say.
t.Run("a sub-day aligned budget is accepted, not rejected", func(t *testing.T) {
ctx := context.Background()
logger := NewMockLogger()
configStore, err := configstore.NewConfigStore(ctx, &configstore.Config{
Enabled: true,
Type: configstore.ConfigStoreTypeSQLite,
Config: &configstore.SQLiteConfig{Path: t.TempDir() + "/subdayaligned.db"},
}, logger)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, configStore.Close(ctx)) })

budget := buildBudgetWithUsage("sub-day-aligned-budget", 100, 0, "1h")
budget.IsCalendarAligned = true
require.NoError(t, configStore.CreateBudget(ctx, budget),
"nothing validates alignment against the duration, so this must save")

stored, err := configStore.GetBudget(ctx, budget.ID)
require.NoError(t, err)
assert.Equal(t, "1h", stored.ResetDuration,
"the sub-day duration is kept as written rather than corrected or refused")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
}
77 changes: 77 additions & 0 deletions tests/governance/customerbudget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package governance
import (
"strconv"
"testing"
"time"
)

// TestCustomerBudgetExceededWithMultipleVKs tests that customer level budgets are enforced across multiple VKs
Expand Down Expand Up @@ -335,3 +336,79 @@ func TestCustomerBudgetExceededWithMultipleTeams(t *testing.T) {
t.Fatalf("Made %d requests but never hit customer budget limit (consumed $%.6f / $%.2f) - budget not being enforced",
requestNum-1, consumedBudget, customerBudget)
}

// TestCustomerBudgetCalendarAlignmentAppliesFromNextPeriod mirrors the team case
// for the customer snap site.
//
// The three sites - team, customer and provider governance - were identical in
// shape and all discarded, but they persist through different store methods, so
// each is pinned separately rather than by analogy.
func TestCustomerBudgetCalendarAlignmentAppliesFromNextPeriod(t *testing.T) {
testData := NewGlobalTestData()
defer testData.Cleanup(t)

createResp := MakeRequest(t, APIRequest{
Method: "POST",
Path: "/api/governance/customers",
Body: CreateCustomerRequest{
Name: "test-customer-calendar-align-" + generateRandomID(),
Budgets: []BudgetRequest{{
MaxLimit: 100,
ResetDuration: "1M",
}},
},
})
if createResp.StatusCode != 200 {
t.Fatalf("Failed to create customer: status %d, body %v", createResp.StatusCode, createResp.Body)
}
customerID := ExtractIDFromResponse(t, createResp)
testData.AddCustomer(customerID)

before := customerBudgetLastReset(t, customerID)

aligned := true
updateResp := MakeRequest(t, APIRequest{
Method: "PUT",
Path: "/api/governance/customers/" + customerID,
Body: UpdateCustomerRequest{CalendarAligned: &aligned},
})
if updateResp.StatusCode != 200 {
t.Fatalf("Failed to enable calendar alignment: status %d, body %v", updateResp.StatusCode, updateResp.Body)
}

after := customerBudgetLastReset(t, customerID)
if !after.Equal(before) {
t.Errorf("enabling calendar alignment moved last_reset from %s to %s; the current window must be left alone",
before.Format(time.RFC3339), after.Format(time.RFC3339))
}
}

// customerBudgetLastReset reads the first budget's last_reset off a customer.
func customerBudgetLastReset(t *testing.T, customerID string) time.Time {
t.Helper()
resp := MakeRequest(t, APIRequest{Method: "GET", Path: "/api/governance/customers/" + customerID})
if resp.StatusCode != 200 {
t.Fatalf("Failed to read customer %s: status %d, body %v", customerID, resp.StatusCode, resp.Body)
}
customer, ok := resp.Body["customer"].(map[string]interface{})
if !ok {
customer = resp.Body
}
budgets, ok := customer["budgets"].([]interface{})
if !ok || len(budgets) == 0 {
t.Fatalf("customer %s has no budgets: %v", customerID, customer)
}
budget, ok := budgets[0].(map[string]interface{})
if !ok {
t.Fatalf("customer %s first budget is not an object: %v", customerID, budgets[0])
}
raw, ok := budget["last_reset"].(string)
if !ok {
t.Fatalf("customer %s budget has no last_reset string: %v", customerID, budget)
}
parsed, err := time.Parse(time.RFC3339, raw)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if err != nil {
t.Fatalf("could not parse last_reset %q: %v", raw, err)
}
return parsed.UTC()
}
Loading
Loading