feat: notify admins when a user crosses an AI budget threshold (#27415)

Implements:
https://linear.app/codercom/issue/AIGOV-289/notify-users-and-admins-on-budget-warning-and-limit-reached

Notify admins when a user crosses an AI budget threshold, complementing
the user-facing notifications from
https://github.com/coder/coder/pull/27346

When a priced interception pushes a user's period spend across the
warning (85%) or limit (100%) threshold, the Owners and User Admins now
receive an admin notification naming the affected user, alongside the
user's own notification. The affected user is excluded from the admin
recipients since they already get the user-facing copy. Delivery is
best-effort: a failure to enqueue is logged and never blocks recording
the interception.

The admin templates always show the effective group the spend is
attributed to, and note when the limit comes from a per-user override
rather than the group budget.

Depends on https://github.com/coder/coder/pull/27346

## Screenshots:
<img width="1101" height="440" alt="image"
src="https://github.com/user-attachments/assets/eb731088-05c8-47bd-9d06-fc9d07f63a08"
/>

<img width="468" height="391" alt="image"
src="https://github.com/user-attachments/assets/b89b76a6-3fa8-4735-99a2-43e119a7a7e3"
/>
This commit is contained in:
Yevhenii Shcherbina
2026-07-27 17:02:28 -04:00
committed by GitHub
parent 51ac968d5a
commit 2574e6b785
13 changed files with 602 additions and 54 deletions
+4 -2
View File
@@ -72,6 +72,8 @@ var (
// AI cost control related events.
var (
TemplateAIBudgetWarningUser = uuid.MustParse("b5db9597-de2a-4dea-87e9-25cee6906b86")
TemplateAIBudgetLimitReachedUser = uuid.MustParse("cdcf2ecd-f003-4169-9800-abb2661ea522")
TemplateAIBudgetWarningUser = uuid.MustParse("b5db9597-de2a-4dea-87e9-25cee6906b86")
TemplateAIBudgetLimitReachedUser = uuid.MustParse("cdcf2ecd-f003-4169-9800-abb2661ea522")
TemplateAIBudgetWarningAdmin = uuid.MustParse("2a7b0ac1-00e1-4625-9cd5-1e5933972c77")
TemplateAIBudgetLimitReachedAdmin = uuid.MustParse("0bafe0ea-a78b-4217-ad05-1ef12e92e025")
)
@@ -1497,6 +1497,45 @@ func TestNotificationTemplates_Golden(t *testing.T) {
Data: map[string]any{},
},
},
{
name: "TemplateAIBudgetWarningAdmin",
id: notifications.TemplateAIBudgetWarningAdmin,
payload: types.MessagePayload{
UserName: "Bobby",
UserEmail: "bobby@coder.com",
UserUsername: "bobby",
Labels: map[string]string{
"username": "alice",
"threshold": "85",
"limit": "$1000.00",
"period": "monthly",
"limit_source": "group",
"effective_group_name": "Engineering",
"period_start": "July 1, 2026",
"period_end": "August 1, 2026",
},
Data: map[string]any{},
},
},
{
name: "TemplateAIBudgetLimitReachedAdmin",
id: notifications.TemplateAIBudgetLimitReachedAdmin,
payload: types.MessagePayload{
UserName: "Bobby",
UserEmail: "bobby@coder.com",
UserUsername: "bobby",
Labels: map[string]string{
"username": "alice",
"limit": "$1000.00",
"period": "monthly",
"limit_source": "user_override",
"effective_group_name": "Engineering",
"period_start": "July 1, 2026",
"period_end": "August 1, 2026",
},
Data: map[string]any{},
},
},
}
// We must have a test case for every notification_template. This is enforced below:
@@ -0,0 +1,82 @@
From: system@coder.com
To: bobby@coder.com
Subject: alice has reached their monthly AI budget limit
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
Date: Fri, 11 Oct 2024 09:03:06 +0000
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
MIME-Version: 1.0
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
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.
Effective group: Engineering
This limit is a per-user override.
AI budget period: July 1, 2026 - August 1, 2026
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!doctype html>
<html lang=3D"en">
<head>
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>alice has reached their monthly AI budget limit</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
; background: #f8fafc;">
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
n: left; font-size: 14px; line-height: 1.5;">
<div style=3D"text-align: center;">
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
alice has reached their monthly AI budget limit
</h1>
<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>
<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>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=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;">
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
ttp://test.com</a></p>
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
r: #2563eb; text-decoration: none;">Click here to manage your notification =
settings</a></p>
<p><a href=3D"http://test.com/settings/notifications?disabled=3D0ba=
fe0ea-a78b-4217-ad05-1ef12e92e025" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
@@ -0,0 +1,77 @@
From: system@coder.com
To: bobby@coder.com
Subject: alice is approaching their monthly AI budget limit
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
Date: Fri, 11 Oct 2024 09:03:06 +0000
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
MIME-Version: 1.0
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8
Hi Bobby,
User alice has used more than 85% of their monthly AI budget ($1000.00).
Effective group: Engineering
AI budget period: July 1, 2026 - August 1, 2026
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!doctype html>
<html lang=3D"en">
<head>
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>alice is approaching their monthly AI budget limit</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
; background: #f8fafc;">
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
n: left; font-size: 14px; line-height: 1.5;">
<div style=3D"text-align: center;">
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
alice is approaching their monthly AI budget limit
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
<p>User <strong>alice</strong> has used more than 85% of their mont=
hly AI budget ($1000.00).</p>
<p>Effective group: <strong>Engineering</strong></p>
<p>AI budget period: July 1, 2026 - August 1, 2026</p>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=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;">
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
ttp://test.com</a></p>
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
r: #2563eb; text-decoration: none;">Click here to manage your notification =
settings</a></p>
<p><a href=3D"http://test.com/settings/notifications?disabled=3D2a7=
b0ac1-00e1-4625-9cd5-1e5933972c77" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
@@ -0,0 +1,29 @@
{
"_version": "1.1",
"msg_id": "00000000-0000-0000-0000-000000000000",
"payload": {
"_version": "1.2",
"notification_name": "User Reached AI Budget Limit",
"notification_template_id": "00000000-0000-0000-0000-000000000000",
"user_id": "00000000-0000-0000-0000-000000000000",
"user_email": "bobby@coder.com",
"user_name": "Bobby",
"user_username": "bobby",
"actions": [],
"labels": {
"effective_group_name": "Engineering",
"limit": "$1000.00",
"limit_source": "user_override",
"period": "monthly",
"period_end": "August 1, 2026",
"period_start": "July 1, 2026",
"username": "alice"
},
"data": {},
"targets": null
},
"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"
}
@@ -0,0 +1,30 @@
{
"_version": "1.1",
"msg_id": "00000000-0000-0000-0000-000000000000",
"payload": {
"_version": "1.2",
"notification_name": "User Approaching AI Budget Limit",
"notification_template_id": "00000000-0000-0000-0000-000000000000",
"user_id": "00000000-0000-0000-0000-000000000000",
"user_email": "bobby@coder.com",
"user_name": "Bobby",
"user_username": "bobby",
"actions": [],
"labels": {
"effective_group_name": "Engineering",
"limit": "$1000.00",
"limit_source": "group",
"period": "monthly",
"period_end": "August 1, 2026",
"period_start": "July 1, 2026",
"threshold": "85",
"username": "alice"
},
"data": {},
"targets": null
},
"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"
}