mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
feat: add CTAs to AI budget admin notifications (#28138)
## Problem
AI budget admin notifications tell an admin that a user crossed a budget
threshold, but give them nothing to act on: no link to the user's
budget, and no pointer to the docs.
## Changes
- Add a **Manage user AI budget** action to both admin templates,
linking to the effective group's members page filtered to the affected
user, where the per-user override is set.
- Link to the AI budget documentation from both notification bodies.
- Say when the block lifts in the limit-reached notification ("blocked
until `{{period_end}}`").
- Add an `organization_name` label, needed to build the members page
URL.
- Grant the AI Gateway daemon subject read access to organizations,
which the label lookup requires.
- Add an authorization test for the notification path, which also covers
the existing group read grant.
<details>
<summary>Screenshots</summary>
**Admin Notifications in Coder UI**
<img width="456" height="695" alt="admin_notifications"
src="https://github.com/user-attachments/assets/a579f91c-55cc-497c-87f4-235f15493725"
/>
**Admin Notification in Email**
<img width="660" height="399" alt="admin_notifications_email"
src="https://github.com/user-attachments/assets/39b876fb-f512-41e5-997f-a5269cd26ddc"
/>
</details>
Related to internal Slack threads:
* https://codercom.slack.com/archives/C096PFVBZKN/p1786051630688569
* https://codercom.slack.com/archives/C04M9CNGHGF/p1786453662306149
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
This commit is contained in:
@@ -85,6 +85,7 @@ type store interface {
|
||||
GetUserEveryoneFallbackGroup(ctx context.Context, userID uuid.UUID) (uuid.UUID, error)
|
||||
GetUserAISpendSince(ctx context.Context, arg database.GetUserAISpendSinceParams) (database.GetUserAISpendSinceRow, error)
|
||||
GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error)
|
||||
GetOrganizationByID(ctx context.Context, id uuid.UUID) (database.Organization, error)
|
||||
GetUsers(ctx context.Context, arg database.GetUsersParams) ([]database.GetUsersRow, error)
|
||||
|
||||
// MCPConfigurator-related queries.
|
||||
|
||||
@@ -2574,6 +2574,90 @@ func TestRecordTokenUsageAuthorized(t *testing.T) {
|
||||
require.Equal(t, wantCost, spend.SpendMicros, "spend micros")
|
||||
}
|
||||
|
||||
// TestBudgetNotificationAuthorized exercises the budget threshold notification
|
||||
// path end-to-end against a real database through the dbauthz layer as
|
||||
// subjectAibridged. This catches missing RBAC grants on the aibridged subject
|
||||
// and verifies the group and organization labels round-trip from storage.
|
||||
func TestBudgetNotificationAuthorized(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
logger := testutil.Logger(t)
|
||||
|
||||
rawDB, _ := dbtestutil.NewDB(t)
|
||||
authzDB := dbauthz.New(rawDB, rbac.NewStrictAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer())
|
||||
|
||||
// Seed prerequisites via the raw (unauthorized) store. The user belongs to a
|
||||
// group with a budget, so the effective group resolves to that group.
|
||||
org := dbgen.Organization(t, rawDB, database.Organization{})
|
||||
user := dbgen.User(t, rawDB, database.User{})
|
||||
dbgen.OrganizationMember(t, rawDB, database.OrganizationMember{OrganizationID: org.ID, UserID: user.ID})
|
||||
group := dbgen.Group(t, rawDB, database.Group{OrganizationID: org.ID})
|
||||
dbgen.GroupMember(t, rawDB, database.GroupMemberTable{UserID: user.ID, GroupID: group.ID})
|
||||
|
||||
// The interception below costs 1555 micros, which crosses 85% of this limit
|
||||
// (1530 micros) without reaching the limit itself.
|
||||
_, err := rawDB.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{
|
||||
GroupID: group.ID,
|
||||
SpendLimitMicros: 1_800,
|
||||
})
|
||||
require.NoError(t, err, "upsert group AI budget")
|
||||
|
||||
const provider, model = "anthropic", "claude-sonnet-4-6"
|
||||
priceSeed, err := json.Marshal([]map[string]any{{
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"input_price": 3_000_000,
|
||||
"output_price": 6_000_000,
|
||||
"cache_read_price": 300_000,
|
||||
"cache_write_price": 4_000_000,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, rawDB.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{Seed: priceSeed, Source: database.AIModelPriceSourceDefault}), "seed model prices")
|
||||
|
||||
aiProvider := dbgen.AIProvider(t, rawDB, database.AIProvider{
|
||||
Name: "anthropic-eu",
|
||||
Type: database.AIProviderTypeAnthropic,
|
||||
})
|
||||
intc := dbgen.AIBridgeInterception(t, rawDB, database.InsertAIBridgeInterceptionParams{
|
||||
InitiatorID: user.ID,
|
||||
Provider: provider,
|
||||
ProviderName: aiProvider.Name,
|
||||
Model: model,
|
||||
}, nil)
|
||||
|
||||
enq := ¬ificationstest.FakeEnqueuer{}
|
||||
srv, err := aibridgedserver.NewServer(ctx, aibridgedserver.Options{
|
||||
Store: authzDB,
|
||||
AISeatTracker: agplaiseats.Noop{},
|
||||
AccessURL: "/",
|
||||
GatewayCfg: codersdk.AIBridgeConfig{},
|
||||
Experiments: requiredExperiments,
|
||||
Enqueuer: enq,
|
||||
Logger: logger,
|
||||
Clock: quartz.NewReal(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = srv.RecordTokenUsage(ctx, &proto.RecordTokenUsageRequest{
|
||||
InterceptionId: intc.ID.String(),
|
||||
MsgId: "msg_budget_authz",
|
||||
InputTokens: 100,
|
||||
OutputTokens: 200,
|
||||
CacheReadInputTokens: 50,
|
||||
CacheWriteInputTokens: 10,
|
||||
CreatedAt: timestamppb.New(time.Date(2026, 6, 25, 14, 30, 0, 0, time.UTC)),
|
||||
})
|
||||
require.NoError(t, err, "record token usage")
|
||||
|
||||
// Notification failures are logged rather than returned, so the enqueued
|
||||
// message is the only signal that both lookups were authorized.
|
||||
sent := enq.Sent(notificationstest.WithTemplateID(notifications.TemplateAIBudgetWarningUser))
|
||||
require.Len(t, sent, 1, "expected one budget warning notification")
|
||||
require.Equal(t, group.Name, sent[0].Labels["effective_group_name"])
|
||||
require.Equal(t, org.Name, sent[0].Labels["organization_name"])
|
||||
}
|
||||
|
||||
// TestRecordTokenUsageModelPriceResolution covers which price an interception
|
||||
// snapshots when a model carries a price from the embedded book, a price set
|
||||
// through the API, or both.
|
||||
@@ -2985,11 +3069,15 @@ func TestRecordTokenUsageBudgetNotifications(t *testing.T) {
|
||||
Return(database.AIUserDailySpend{}, nil)
|
||||
db.EXPECT().GetUserAISpendSince(gomock.Any(), gomock.Any()).
|
||||
Return(database.GetUserAISpendSinceRow{SpendMicros: tc.newSpend}, nil)
|
||||
// The group and user are resolved once per interception that
|
||||
// notifies, regardless of how many thresholds it crosses.
|
||||
// The group, organization, and user are resolved once per
|
||||
// interception that notifies, regardless of how many thresholds it
|
||||
// crosses.
|
||||
if len(tc.wantTemplates) > 0 {
|
||||
orgID := uuid.New()
|
||||
db.EXPECT().GetGroupByID(gomock.Any(), groupID).
|
||||
Return(database.Group{ID: groupID, Name: "Engineering"}, nil)
|
||||
Return(database.Group{ID: groupID, Name: "Engineering", OrganizationID: orgID}, nil)
|
||||
db.EXPECT().GetOrganizationByID(gomock.Any(), orgID).
|
||||
Return(database.Organization{ID: orgID, Name: "coder"}, nil)
|
||||
db.EXPECT().GetUserByID(gomock.Any(), intc.InitiatorID).
|
||||
Return(database.User{ID: intc.InitiatorID, Username: "bob"}, nil)
|
||||
// No admins configured, so only the user is notified.
|
||||
@@ -3083,8 +3171,11 @@ func TestRecordTokenUsageBudgetNotificationAcrossPeriodBoundary(t *testing.T) {
|
||||
gotPeriodStart = p.PeriodStart
|
||||
return database.GetUserAISpendSinceRow{SpendMicros: warnAt}, nil
|
||||
})
|
||||
orgID := uuid.New()
|
||||
db.EXPECT().GetGroupByID(gomock.Any(), groupID).
|
||||
Return(database.Group{ID: groupID, Name: "Engineering"}, nil)
|
||||
Return(database.Group{ID: groupID, Name: "Engineering", OrganizationID: orgID}, nil)
|
||||
db.EXPECT().GetOrganizationByID(gomock.Any(), orgID).
|
||||
Return(database.Organization{ID: orgID, Name: "coder"}, nil)
|
||||
db.EXPECT().GetUserByID(gomock.Any(), intc.InitiatorID).
|
||||
Return(database.User{ID: intc.InitiatorID, Username: "bob"}, nil)
|
||||
// No admins configured, so only the user is notified.
|
||||
@@ -3366,8 +3457,11 @@ func TestRecordTokenUsageBudgetAdminNotification(t *testing.T) {
|
||||
Return(database.AIUserDailySpend{}, nil)
|
||||
db.EXPECT().GetUserAISpendSince(gomock.Any(), gomock.Any()).
|
||||
Return(database.GetUserAISpendSinceRow{SpendMicros: tc.newSpend}, nil)
|
||||
orgID := uuid.New()
|
||||
db.EXPECT().GetGroupByID(gomock.Any(), groupID).
|
||||
Return(database.Group{ID: groupID, Name: "Engineering"}, nil)
|
||||
Return(database.Group{ID: groupID, Name: "Engineering", OrganizationID: orgID}, nil)
|
||||
db.EXPECT().GetOrganizationByID(gomock.Any(), orgID).
|
||||
Return(database.Organization{ID: orgID, Name: "coder"}, nil)
|
||||
db.EXPECT().GetUserByID(gomock.Any(), intc.InitiatorID).
|
||||
Return(database.User{ID: intc.InitiatorID, Username: "bob"}, nil)
|
||||
db.EXPECT().GetUsers(gomock.Any(), database.GetUsersParams{
|
||||
@@ -3409,6 +3503,7 @@ func TestRecordTokenUsageBudgetAdminNotification(t *testing.T) {
|
||||
require.Equal(t, tc.wantThreshold, adminSent[0].Labels["threshold"])
|
||||
require.Equal(t, "$100.00", adminSent[0].Labels["limit"])
|
||||
require.Equal(t, "Engineering", adminSent[0].Labels["effective_group_name"])
|
||||
require.Equal(t, "coder", adminSent[0].Labels["organization_name"])
|
||||
require.Equal(t, tc.wantLimitSource, adminSent[0].Labels["limit_source"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,6 +137,10 @@ func (s *Server) notifyBudgetThresholdCrossings(ctx context.Context, crossings [
|
||||
if err != nil {
|
||||
return xerrors.Errorf("look up group %q: %w", effectiveGroupID, err)
|
||||
}
|
||||
org, err := s.store.GetOrganizationByID(ctx, group.OrganizationID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("look up organization %q: %w", group.OrganizationID, err)
|
||||
}
|
||||
user, err := s.store.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("look up user %q: %w", userID, err)
|
||||
@@ -158,6 +162,7 @@ func (s *Server) notifyBudgetThresholdCrossings(ctx context.Context, crossings [
|
||||
"limit_source": string(c.limitSource),
|
||||
"username": user.Username,
|
||||
"effective_group_name": group.Name,
|
||||
"organization_name": org.Name,
|
||||
// Both bounds carry the year so a period straddling a year boundary
|
||||
// (e.g. December 1, 2026 - January 1, 2027) is unambiguous.
|
||||
"period_start": c.periodStart.UTC().Format("January 2, 2006"),
|
||||
|
||||
@@ -725,6 +725,7 @@ var (
|
||||
rbac.ResourceAiSeat.Type: {policy.ActionCreate}, // Required for UpsertAISeatState.
|
||||
rbac.ResourceAIProvider.Type: {policy.ActionRead}, // Required to load the provider snapshot (and per-provider keys) at startup.
|
||||
rbac.ResourceGroup.Type: {policy.ActionRead}, // Required to read the effective group.
|
||||
rbac.ResourceOrganization.Type: {policy.ActionRead}, // Required to name the organization in budget notification links.
|
||||
}),
|
||||
User: []rbac.Permission{},
|
||||
ByOrgID: map[string]rbac.OrgPermissions{},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
body_template = $$User **{{.Labels.username}}** has used more than {{.Labels.threshold}}% of their {{.Labels.period}} AI budget ({{.Labels.limit}}).
|
||||
|
||||
Effective group: **{{.Labels.effective_group_name}}**
|
||||
{{- if eq .Labels.limit_source "user_override"}}
|
||||
|
||||
This limit is a per-user override.
|
||||
{{- end}}
|
||||
|
||||
AI budget period: {{.Labels.period_start}} - {{.Labels.period_end}}$$,
|
||||
actions = '[]'::jsonb
|
||||
WHERE
|
||||
id = '2a7b0ac1-00e1-4625-9cd5-1e5933972c77';
|
||||
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
body_template = $$User **{{.Labels.username}}** has reached their {{.Labels.period}} AI budget limit ({{.Labels.limit}}). Subsequent requests will be blocked.
|
||||
|
||||
Effective group: **{{.Labels.effective_group_name}}**
|
||||
{{- if eq .Labels.limit_source "user_override"}}
|
||||
|
||||
This limit is a per-user override.
|
||||
{{- end}}
|
||||
|
||||
AI budget period: {{.Labels.period_start}} - {{.Labels.period_end}}$$,
|
||||
actions = '[]'::jsonb
|
||||
WHERE
|
||||
id = '0bafe0ea-a78b-4217-ad05-1ef12e92e025';
|
||||
@@ -0,0 +1,43 @@
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
body_template = $$User **{{.Labels.username}}** has used more than {{.Labels.threshold}}% of their {{.Labels.period}} AI budget ({{.Labels.limit}}).
|
||||
|
||||
Effective group: **{{.Labels.effective_group_name}}**
|
||||
{{- if eq .Labels.limit_source "user_override"}}
|
||||
|
||||
This limit is a per-user override.
|
||||
{{- end}}
|
||||
|
||||
AI budget period: {{.Labels.period_start}} - {{.Labels.period_end}}
|
||||
|
||||
[How to configure AI budgets](https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget)$$,
|
||||
actions = '[
|
||||
{
|
||||
"label": "Manage user AI budget",
|
||||
"url": "{{base_url}}/organizations/{{.Labels.organization_name}}/groups/{{.Labels.effective_group_name}}?filter={{.Labels.username}}"
|
||||
}
|
||||
]'::jsonb
|
||||
WHERE
|
||||
id = '2a7b0ac1-00e1-4625-9cd5-1e5933972c77';
|
||||
|
||||
UPDATE notification_templates
|
||||
SET
|
||||
body_template = $$User **{{.Labels.username}}** has reached their {{.Labels.period}} AI budget limit ({{.Labels.limit}}). Subsequent requests will be blocked until {{.Labels.period_end}}.
|
||||
|
||||
Effective group: **{{.Labels.effective_group_name}}**
|
||||
{{- if eq .Labels.limit_source "user_override"}}
|
||||
|
||||
This limit is a per-user override.
|
||||
{{- end}}
|
||||
|
||||
AI budget period: {{.Labels.period_start}} - {{.Labels.period_end}}
|
||||
|
||||
Raise this user's limit to unblock them sooner. [How to configure AI budgets](https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget).$$,
|
||||
actions = '[
|
||||
{
|
||||
"label": "Manage user AI budget",
|
||||
"url": "{{base_url}}/organizations/{{.Labels.organization_name}}/groups/{{.Labels.effective_group_name}}?filter={{.Labels.username}}"
|
||||
}
|
||||
]'::jsonb
|
||||
WHERE
|
||||
id = '0bafe0ea-a78b-4217-ad05-1ef12e92e025';
|
||||
@@ -1591,6 +1591,7 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
"period": "monthly",
|
||||
"limit_source": "group",
|
||||
"effective_group_name": "Engineering",
|
||||
"organization_name": "coder",
|
||||
"period_start": "July 1, 2026",
|
||||
"period_end": "August 1, 2026",
|
||||
},
|
||||
@@ -1610,6 +1611,7 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
"period": "monthly",
|
||||
"limit_source": "user_override",
|
||||
"effective_group_name": "Engineering",
|
||||
"organization_name": "coder",
|
||||
"period_start": "July 1, 2026",
|
||||
"period_end": "August 1, 2026",
|
||||
},
|
||||
|
||||
Vendored
+20
-2
@@ -13,7 +13,7 @@ Content-Type: text/plain; charset=UTF-8
|
||||
Hi Bobby,
|
||||
|
||||
User alice has reached their monthly AI budget limit ($1000.00). Subsequent=
|
||||
requests will be blocked.
|
||||
requests will be blocked until August 1, 2026.
|
||||
|
||||
Effective group: Engineering
|
||||
|
||||
@@ -21,6 +21,12 @@ This limit is a per-user override.
|
||||
|
||||
AI budget period: July 1, 2026 - August 1, 2026
|
||||
|
||||
Raise this user's limit to unblock them sooner. How to configure AI budgets=
|
||||
(https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget).
|
||||
|
||||
|
||||
Manage user AI budget: http://test.com/organizations/coder/groups/Engineeri=
|
||||
ng?filter=3Dalice
|
||||
|
||||
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
@@ -52,16 +58,28 @@ argin: 8px 0 32px; line-height: 1.5;">
|
||||
<div style=3D"line-height: 1.5;">
|
||||
<p>Hi Bobby,</p>
|
||||
<p>User <strong>alice</strong> has reached their monthly AI budget =
|
||||
limit ($1000.00). Subsequent requests will be blocked.</p>
|
||||
limit ($1000.00). Subsequent requests will be blocked until August 1, 2026.=
|
||||
</p>
|
||||
|
||||
<p>Effective group: <strong>Engineering</strong></p>
|
||||
|
||||
<p>This limit is a per-user override.</p>
|
||||
|
||||
<p>AI budget period: July 1, 2026 - August 1, 2026</p>
|
||||
|
||||
<p>Raise this user’s limit to unblock them sooner. <a href=3D"https:/=
|
||||
/coder.com/docs/ai-coder/ai-gateway/cost-controls#budget">How to configure =
|
||||
AI budgets</a>.</p>
|
||||
</div>
|
||||
<div style=3D"text-align: center; margin-top: 32px;">
|
||||
=20
|
||||
<a href=3D"http://test.com/organizations/coder/groups/Engineering?f=
|
||||
ilter=3Dalice" style=3D"display: inline-block; padding: 13px 24px; backgrou=
|
||||
nd-color: #020617; color: #f8fafc; text-decoration: none; border-radius: 8p=
|
||||
x; margin: 0 4px;">
|
||||
Manage user AI budget
|
||||
</a>
|
||||
=20
|
||||
</div>
|
||||
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
|
||||
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
|
||||
|
||||
Vendored
+16
@@ -18,6 +18,12 @@ Effective group: Engineering
|
||||
|
||||
AI budget period: July 1, 2026 - August 1, 2026
|
||||
|
||||
How to configure AI budgets (https://coder.com/docs/ai-coder/ai-gateway/cos=
|
||||
t-controls#budget)
|
||||
|
||||
|
||||
Manage user AI budget: http://test.com/organizations/coder/groups/Engineeri=
|
||||
ng?filter=3Dalice
|
||||
|
||||
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
@@ -54,9 +60,19 @@ hly AI budget ($1000.00).</p>
|
||||
<p>Effective group: <strong>Engineering</strong></p>
|
||||
|
||||
<p>AI budget period: July 1, 2026 - August 1, 2026</p>
|
||||
|
||||
<p><a href=3D"https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budg=
|
||||
et">How to configure AI budgets</a></p>
|
||||
</div>
|
||||
<div style=3D"text-align: center; margin-top: 32px;">
|
||||
=20
|
||||
<a href=3D"http://test.com/organizations/coder/groups/Engineering?f=
|
||||
ilter=3Dalice" style=3D"display: inline-block; padding: 13px 24px; backgrou=
|
||||
nd-color: #020617; color: #f8fafc; text-decoration: none; border-radius: 8p=
|
||||
x; margin: 0 4px;">
|
||||
Manage user AI budget
|
||||
</a>
|
||||
=20
|
||||
</div>
|
||||
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
|
||||
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
|
||||
|
||||
+9
-3
@@ -9,11 +9,17 @@
|
||||
"user_email": "bobby@coder.com",
|
||||
"user_name": "Bobby",
|
||||
"user_username": "bobby",
|
||||
"actions": [],
|
||||
"actions": [
|
||||
{
|
||||
"label": "Manage user AI budget",
|
||||
"url": "http://test.com/organizations/coder/groups/Engineering?filter=alice"
|
||||
}
|
||||
],
|
||||
"labels": {
|
||||
"effective_group_name": "Engineering",
|
||||
"limit": "$1000.00",
|
||||
"limit_source": "user_override",
|
||||
"organization_name": "coder",
|
||||
"period": "monthly",
|
||||
"period_end": "August 1, 2026",
|
||||
"period_start": "July 1, 2026",
|
||||
@@ -24,6 +30,6 @@
|
||||
},
|
||||
"title": "alice has reached their monthly AI budget limit",
|
||||
"title_markdown": "alice has reached their monthly AI budget limit",
|
||||
"body": "User alice has reached their monthly AI budget limit ($1000.00). Subsequent requests will be blocked.\n\nEffective group: Engineering\n\nThis limit is a per-user override.\n\nAI budget period: July 1, 2026 - August 1, 2026",
|
||||
"body_markdown": "User **alice** has reached their monthly AI budget limit ($1000.00). Subsequent requests will be blocked.\n\nEffective group: **Engineering**\n\nThis limit is a per-user override.\n\nAI budget period: July 1, 2026 - August 1, 2026"
|
||||
"body": "User alice has reached their monthly AI budget limit ($1000.00). Subsequent requests will be blocked until August 1, 2026.\n\nEffective group: Engineering\n\nThis limit is a per-user override.\n\nAI budget period: July 1, 2026 - August 1, 2026\n\nRaise this user's limit to unblock them sooner. How to configure AI budgets (https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget).",
|
||||
"body_markdown": "User **alice** has reached their monthly AI budget limit ($1000.00). Subsequent requests will be blocked until August 1, 2026.\n\nEffective group: **Engineering**\n\nThis limit is a per-user override.\n\nAI budget period: July 1, 2026 - August 1, 2026\n\nRaise this user's limit to unblock them sooner. [How to configure AI budgets](https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget)."
|
||||
}
|
||||
Vendored
+9
-3
@@ -9,11 +9,17 @@
|
||||
"user_email": "bobby@coder.com",
|
||||
"user_name": "Bobby",
|
||||
"user_username": "bobby",
|
||||
"actions": [],
|
||||
"actions": [
|
||||
{
|
||||
"label": "Manage user AI budget",
|
||||
"url": "http://test.com/organizations/coder/groups/Engineering?filter=alice"
|
||||
}
|
||||
],
|
||||
"labels": {
|
||||
"effective_group_name": "Engineering",
|
||||
"limit": "$1000.00",
|
||||
"limit_source": "group",
|
||||
"organization_name": "coder",
|
||||
"period": "monthly",
|
||||
"period_end": "August 1, 2026",
|
||||
"period_start": "July 1, 2026",
|
||||
@@ -25,6 +31,6 @@
|
||||
},
|
||||
"title": "alice is approaching their monthly AI budget limit",
|
||||
"title_markdown": "alice is approaching their monthly AI budget limit",
|
||||
"body": "User alice has used more than 85% of their monthly AI budget ($1000.00).\n\nEffective group: Engineering\n\nAI budget period: July 1, 2026 - August 1, 2026",
|
||||
"body_markdown": "User **alice** has used more than 85% of their monthly AI budget ($1000.00).\n\nEffective group: **Engineering**\n\nAI budget period: July 1, 2026 - August 1, 2026"
|
||||
"body": "User alice has used more than 85% of their monthly AI budget ($1000.00).\n\nEffective group: Engineering\n\nAI budget period: July 1, 2026 - August 1, 2026\n\nHow to configure AI budgets (https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget)",
|
||||
"body_markdown": "User **alice** has used more than 85% of their monthly AI budget ($1000.00).\n\nEffective group: **Engineering**\n\nAI budget period: July 1, 2026 - August 1, 2026\n\n[How to configure AI budgets](https://coder.com/docs/ai-coder/ai-gateway/cost-controls#budget)"
|
||||
}
|
||||
Reference in New Issue
Block a user