mirror of
https://github.com/coder/coder.git
synced 2026-11-02 21:50:08 +08:00
## Description Previously, a user with no per-user override and no membership in a budgeted group had no effective group, so their AI spend was attributed nowhere and was, therefore, untracked. This change falls back to the organization's Everyone group when no override or group budget applies. Since every user in an organization is implicitly a member of that org's Everyone group, spend is now attributed and tracked for any user with organization membership. A user with no organization membership resolves to no group, so their daily spend is not incremented and a warning is logged. The fallback is unlimited, so enforcement is unaffected: only override and group budgets can block requests. For users in multiple organizations, an existing budget on any Everyone group is still chosen by the "highest" policy; when none is budgeted, the fallback prefers the default org, then orders by organization name. ## Changes - Add `ResolveUserEffectiveGroup` and the `GetUserEveryoneFallbackGroup` query: resolve override → group budget → Everyone group fallback. - Attribute token-usage spend and the user AI spend endpoint via the fallback, so unbudgeted users resolve to their Everyone group instead of null. - Update `GetGroupMembersAISpend` to surface the Everyone fallback as the effective group. - Update `GetHighestGroupAIBudgetByUser` to break ties by organization name then group name, keeping multi-org resolution deterministic and consistent with the fallback. - For multi-org users with no budget anywhere, the fallback picks the Everyone group deterministically: prefer the default org, then order by organization name. Closes https://linear.app/codercom/issue/AIGOV-509/fall-back-to-the-everyone-group-for-spend-attribution > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
113 lines
4.0 KiB
Go
113 lines
4.0 KiB
Go
// Package budget resolves the effective AI spend budget for a user. A
|
|
// per-user override always wins; otherwise the deployment budget policy selects
|
|
// a budget from the groups the user belongs to.
|
|
package budget
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
// Store is the subset of database.Store needed to resolve a user's effective
|
|
// AI budget.
|
|
type Store interface {
|
|
GetUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (database.UserAIBudgetOverride, error)
|
|
GetHighestGroupAIBudgetByUser(ctx context.Context, userID uuid.UUID) (database.GetHighestGroupAIBudgetByUserRow, error)
|
|
GetUserEveryoneFallbackGroup(ctx context.Context, userID uuid.UUID) (uuid.UUID, error)
|
|
}
|
|
|
|
// EffectiveGroup is a user's resolved effective group and, when a budget
|
|
// applies, its limit. Limit is nil for the Everyone fallback (unlimited).
|
|
type EffectiveGroup struct {
|
|
// GroupID is the group the spend is attributed to.
|
|
GroupID uuid.UUID
|
|
// Limit is the resolved spend limit, or nil for the unlimited Everyone
|
|
// fallback.
|
|
Limit *Limit
|
|
}
|
|
|
|
// Limit is an AI spend limit and the source that produced it.
|
|
type Limit struct {
|
|
// SpendLimitMicros is the spend limit in micro-units (1 unit = 1,000,000).
|
|
SpendLimitMicros int64
|
|
Source codersdk.AIBudgetLimitSource
|
|
}
|
|
|
|
// ResolveUserAIBudget returns the effective AI budget group for userID,
|
|
// resolved in order:
|
|
// 1. A per-user override, if configured.
|
|
// 2. Otherwise, a group budget selected by the deployment policy.
|
|
//
|
|
// The second return value is false when no budget is configured for the user.
|
|
// TODO(AIGOV-527): unify effective group resolution in a single place.
|
|
func ResolveUserAIBudget(ctx context.Context, db Store, userID uuid.UUID, policy codersdk.AIBudgetPolicy) (EffectiveGroup, bool, error) {
|
|
// A per-user override always wins.
|
|
override, err := db.GetUserAIBudgetOverride(ctx, userID)
|
|
if err == nil {
|
|
return EffectiveGroup{
|
|
GroupID: override.GroupID,
|
|
Limit: &Limit{
|
|
SpendLimitMicros: override.SpendLimitMicros,
|
|
Source: codersdk.AIBudgetLimitSourceUserOverride,
|
|
},
|
|
}, true, nil
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return EffectiveGroup{}, false, xerrors.Errorf("get user AI budget override: %w", err)
|
|
}
|
|
|
|
// No override: select a group budget according to the deployment policy.
|
|
switch policy {
|
|
case codersdk.AIBudgetPolicyHighest:
|
|
row, err := db.GetHighestGroupAIBudgetByUser(ctx, userID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return EffectiveGroup{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return EffectiveGroup{}, false, xerrors.Errorf("get highest group AI budget: %w", err)
|
|
}
|
|
return EffectiveGroup{
|
|
GroupID: row.GroupID,
|
|
Limit: &Limit{
|
|
SpendLimitMicros: row.SpendLimitMicros,
|
|
Source: codersdk.AIBudgetLimitSourceGroup,
|
|
},
|
|
}, true, nil
|
|
default:
|
|
return EffectiveGroup{}, false, xerrors.Errorf("unsupported AI budget policy: %q", policy)
|
|
}
|
|
}
|
|
|
|
// ResolveUserEffectiveGroup resolves the user's effective group, falling back to
|
|
// the organization's Everyone group when no override or group budget applies.
|
|
// The second return value is false when no effective group was found for the
|
|
// user.
|
|
func ResolveUserEffectiveGroup(ctx context.Context, db Store, userID uuid.UUID, policy codersdk.AIBudgetPolicy) (EffectiveGroup, bool, error) {
|
|
group, ok, err := ResolveUserAIBudget(ctx, db, userID, policy)
|
|
if err != nil {
|
|
return EffectiveGroup{}, false, err
|
|
}
|
|
if ok {
|
|
return group, true, nil
|
|
}
|
|
|
|
// No override or group budget: fall back to the Everyone group (unlimited).
|
|
groupID, err := db.GetUserEveryoneFallbackGroup(ctx, userID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// This should not happen, as a user should always be a member of an
|
|
// organization and its associated Everyone group.
|
|
return EffectiveGroup{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return EffectiveGroup{}, false, xerrors.Errorf("get everyone fallback group: %w", err)
|
|
}
|
|
return EffectiveGroup{GroupID: groupID}, true, nil
|
|
}
|