refactor: make user AI budget and spend endpoints consistent (#27611)

## Description

Makes the user AI cost control endpoints consistent.

## Changes

- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
This commit is contained in:
Susana Ferreira
2026-07-29 14:01:32 +01:00
committed by GitHub
parent e71249a821
commit d6a5c8e9f8
14 changed files with 194 additions and 181 deletions
+11 -13
View File
@@ -32,28 +32,26 @@ const (
AIBudgetLimitSourceGroup AIBudgetLimitSource = "group"
)
// AIGroupBudget is an AI spend limit and the tier that produced it. Both
// AIBudgetLimit is an AI spend limit and the tier that produced it. Both
// fields are always populated together.
type AIGroupBudget struct {
type AIBudgetLimit struct {
SpendLimitMicros int64 `json:"spend_limit_micros"`
LimitSource AIBudgetLimitSource `json:"limit_source"`
}
// UserAIBudgetSummary is the effective AI budget for a user. When no budget
// applies, the effective group falls back to the Everyone group with a null
// limit and source.
// budget.
type UserAIBudgetSummary struct {
UserID uuid.UUID `json:"user_id" format:"uuid"`
// EffectiveGroupID is the group the spend is attributed to, falling back to
// the Everyone group when no budget applies. Null only when the user has no
// organization membership.
EffectiveGroupID *uuid.UUID `json:"effective_group_id" format:"uuid"`
// SpendLimitMicros is the effective spend limit in micro-units.
// Null when no budget applies to the user (unlimited).
SpendLimitMicros *int64 `json:"spend_limit_micros"`
// LimitSource identifies which tier produced the limit. Null when no
// budget applies.
LimitSource *AIBudgetLimitSource `json:"limit_source"`
// EffectiveBudget is the spend limit that applies to the user, whether it
// came from a group budget or a user override. Null when no budget
// applies, leaving the user's spend unlimited.
EffectiveBudget *AIBudgetLimit `json:"effective_budget"`
}
// AISpendPeriodWindow is the [Start, End) window over which AI spend is
@@ -126,7 +124,7 @@ type GroupMemberAISpend struct {
// GroupBudget is the budget when the queried group is this user's
// effective budget source. Null when the user's budget resolves to another
// group or no budget applies to the user.
GroupBudget *AIGroupBudget `json:"group_budget"`
GroupBudget *AIBudgetLimit `json:"group_budget"`
// GroupSpendMicros is the user's spend attributed to the queried group
// over the current budget period.
GroupSpendMicros int64 `json:"group_spend_micros"`
@@ -498,7 +496,7 @@ type UpsertUserAIBudgetOverrideRequest struct {
// UserAIBudgetOverride returns the AI spend budget override configured for the given user.
func (c *Client) UserAIBudgetOverride(ctx context.Context, user uuid.UUID) (UserAIBudgetOverride, error) {
res, err := c.Request(ctx, http.MethodGet,
fmt.Sprintf("/api/v2/users/%s/ai/budget", user.String()),
fmt.Sprintf("/api/v2/users/%s/ai/budget/override", user.String()),
nil,
)
if err != nil {
@@ -516,7 +514,7 @@ func (c *Client) UserAIBudgetOverride(ctx context.Context, user uuid.UUID) (User
// UpsertUserAIBudgetOverride creates or updates the AI spend budget override for the given user.
func (c *Client) UpsertUserAIBudgetOverride(ctx context.Context, user uuid.UUID, req UpsertUserAIBudgetOverrideRequest) (UserAIBudgetOverride, error) {
res, err := c.Request(ctx, http.MethodPut,
fmt.Sprintf("/api/v2/users/%s/ai/budget", user.String()),
fmt.Sprintf("/api/v2/users/%s/ai/budget/override", user.String()),
req,
)
if err != nil {
@@ -534,7 +532,7 @@ func (c *Client) UpsertUserAIBudgetOverride(ctx context.Context, user uuid.UUID,
// DeleteUserAIBudgetOverride removes the AI spend budget override for the given user.
func (c *Client) DeleteUserAIBudgetOverride(ctx context.Context, user uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete,
fmt.Sprintf("/api/v2/users/%s/ai/budget", user.String()),
fmt.Sprintf("/api/v2/users/%s/ai/budget/override", user.String()),
nil,
)
if err != nil {