diff --git a/docs/ai-coder/ai-gateway/cost-controls.md b/docs/ai-coder/ai-gateway/cost-controls.md
index 741d6e5134..1b82a5f968 100644
--- a/docs/ai-coder/ai-gateway/cost-controls.md
+++ b/docs/ai-coder/ai-gateway/cost-controls.md
@@ -77,7 +77,7 @@ member can spend $200 USD and the group has a total spend limit of $2,000 USD.
Budget values behave as follows:
-- An empty field means no limit. The field displays `unlimited`.
+- An empty field means no budget is set. The field displays `no budget`.
- `$0 USD` blocks every request routed via AI Gateway from members whose
effective group is this group.
- The maximum is `$1,000,000 USD` per member per budget period.
diff --git a/site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx
index a80aba46e5..6ab83b98c8 100644
--- a/site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx
+++ b/site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx
@@ -42,7 +42,14 @@ export const WithAIBudget: Story = {
1000,
);
const helper = canvas.getByText(/month, based on/i);
- await expect(helper).toHaveTextContent("$7,000/month, based on 7 members.");
+ await expect(helper).toHaveTextContent(
+ "This group's limit is $7,000/month, based on 7 members.",
+ );
+ await expect(
+ canvas.getByRole("link", {
+ name: /learn how budgets apply across groups/i,
+ }),
+ ).toBeInTheDocument();
},
};
@@ -55,11 +62,23 @@ export const AIBudgetUncapped: Story = {
const canvas = within(canvasElement);
await expect(
canvas.getByLabelText("Monthly limit per member"),
- ).toHaveAttribute("placeholder", "unlimited");
- await expect(canvas.getByText("unlimited budget")).toBeInTheDocument();
+ ).toHaveAttribute("placeholder", "no budget");
await expect(
- canvas.getByText("Members in this group have no spending cap."),
+ canvas.getByText("This group doesn't have a budget set."),
).toBeInTheDocument();
+ await expect(
+ canvas.getByText(/Members will fall back to another group's limit/),
+ ).toBeInTheDocument();
+ await expect(
+ canvas.getByRole("link", {
+ name: /learn how budgets apply across groups/i,
+ }),
+ ).toHaveAttribute(
+ "href",
+ expect.stringContaining(
+ "/ai-coder/ai-gateway/cost-controls#effective-group-resolution",
+ ),
+ );
},
};
@@ -74,9 +93,14 @@ export const AIBudgetDisabled: Story = {
await expect(canvas.getByLabelText("Monthly limit per member")).toHaveValue(
0,
);
- await expect(canvas.getByText("no budget")).toBeInTheDocument();
+ const summary = canvas.getByText(/This group's limit has been set to/);
+ await expect(summary).toHaveTextContent(
+ "This group's limit has been set to $0.",
+ );
await expect(
- canvas.getByText("A $0 limit disables AI access for this group."),
+ canvas.getByText(
+ /A \$0 limit blocks AI access for members that aren't in another group/,
+ ),
).toBeInTheDocument();
},
};
@@ -91,7 +115,9 @@ export const AIBudgetDecimal: Story = {
const canvas = within(canvasElement);
// Cents are kept when the amount is not a whole dollar.
const helper = canvas.getByText(/month, based on/i);
- await expect(helper).toHaveTextContent("$99.99/month, based on 1 member.");
+ await expect(helper).toHaveTextContent(
+ "This group's limit is $99.99/month, based on 1 member.",
+ );
},
};
diff --git a/site/src/pages/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.tsx
index 4bc3596e5d..e8ff2c1c77 100644
--- a/site/src/pages/GroupsPage/GroupSettingsPageView.tsx
+++ b/site/src/pages/GroupsPage/GroupSettingsPageView.tsx
@@ -2,7 +2,6 @@ import { useFormik } from "formik";
import type { FC, ReactNode } from "react";
import * as Yup from "yup";
import type { Group } from "#/api/typesGenerated";
-import { Alert } from "#/components/Alert/Alert";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { IconField } from "#/components/IconField/IconField";
@@ -13,6 +12,7 @@ import {
InputGroupInput,
} from "#/components/InputGroup/InputGroup";
import { Label } from "#/components/Label/Label";
+import { Link } from "#/components/Link/Link";
import { Spinner } from "#/components/Spinner/Spinner";
import {
aiBudgetRangeError,
@@ -20,6 +20,7 @@ import {
maxAIBudgetDollars,
} from "#/modules/groups";
import { usdBudgetFormatter } from "#/utils/currency";
+import { docs } from "#/utils/docs";
import {
getFormHelpers,
nameValidator,
@@ -31,20 +32,34 @@ type FormData = {
display_name: string;
avatar_url: string;
quota_allowance: number;
- // Per-member AI budget, in dollars. "" is unlimited; 0 disables.
+ // Per-member AI budget, in dollars. "" means no budget; 0 disables AI access.
monthly_budget_per_member: string;
};
const validationSchema = Yup.object({
name: nameValidator("Name"),
quota_allowance: Yup.number().required().min(0).integer(),
- // Optional: empty is unlimited. A value must be within the range; 0 disables.
+ // Optional: empty means no budget. A value must be within the range; 0 disables.
monthly_budget_per_member: Yup.number()
.transform((value, original) => (original === "" ? undefined : value))
.min(0, aiBudgetRangeError)
.max(maxAIBudgetDollars, aiBudgetRangeError),
});
+const BudgetDocsLink: FC = () => (
+
+ Learn how budgets apply across groups
+
+);
+
interface AIBudgetFeedbackProps {
error: boolean;
helperText?: ReactNode;
@@ -69,26 +84,34 @@ const AIBudgetFeedback: FC = ({
const budgetValue = monthlyBudgetPerMember.trim();
const budgetAmount = Number(budgetValue);
- // Empty means unlimited spend; $0 disables AI access. Both states show an
+ // Empty means no budget; $0 disables AI access. Both states show an
// explanatory alert alongside the summary line.
if (budgetValue === "" || budgetAmount === 0) {
- const { label, message } =
+ const { summary, message } =
budgetValue === ""
? {
- label: "unlimited budget",
- message: "Members in this group have no spending cap.",
+ summary: "This group doesn't have a budget set.",
+ message:
+ "Members will fall back to another group's limit, or if no budgets have been set, they will have no spend limit.",
}
: {
- label: "no budget",
- message: "A $0 limit disables AI access for this group.",
+ summary: (
+ <>
+ This group's limit has been set to{" "}
+ $0.
+ >
+ ),
+ message:
+ "A $0 limit blocks AI access for members that aren't in another group with a budget set.",
};
return (
<>
- This group has{" "}
- {label}.
+ {summary}
+
+
+ {message}
- {message}
>
);
}
@@ -96,12 +119,13 @@ const AIBudgetFeedback: FC = ({
if (Number.isFinite(budgetAmount) && budgetAmount > 0) {
return (
+ This group's limit is{" "}
{usdBudgetFormatter.format(budgetAmount * memberCount)}
/month, based on{" "}
{memberCount}{" "}
- {memberCount === 1 ? "member" : "members"}.
+ {memberCount === 1 ? "member" : "members"}.
);
}
@@ -251,7 +275,7 @@ const UpdateGroupForm: FC = ({
min="0"
max={maxAIBudgetDollars}
step="1"
- placeholder="unlimited"
+ placeholder="no budget"
aria-invalid={budgetField.error}
/>
USD