feat(site): label AI spend as estimated (#27584)

AI spend is estimated by multiplying token usage by a snapshot of
published model prices that ships with each Coder release, so it can
differ from provider invoices.

Label spend figures as estimated and link to the [How spend is
estimated](https://coder.com/docs/@main/ai-coder/ai-gateway/cost-controls#how-spend-is-estimated)
docs section. "Estimated spend" matches the terminology already defined
in the cost controls doc.

## Changes

- Group members tooltip: "Monthly AI spend for this user." becomes
"Estimated monthly AI spend for this user.", followed by an inline docs
link.
- Groups table tooltip: "Current AI spend compared to the group's AI
budget..." becomes "Estimated AI spend compared to...", with the same
link.
- User dropdown: "(AI spend/month)" becomes "(Estimated AI
spend/month)".
- `StatusIconTooltip` now exports `SpendEstimateDocsLink`, shared by
both tooltips.

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
This commit is contained in:
Susana Ferreira
2026-08-03 14:56:36 +01:00
committed by GitHub
parent f4110122ec
commit ec9b0f04d1
6 changed files with 42 additions and 12 deletions
@@ -73,7 +73,7 @@ export const WithAISpend: Story = {
await waitFor(() =>
expect(document.body).toHaveTextContent("$819 / $1,200 USD"),
);
expect(document.body).toHaveTextContent("(AI spend/month)");
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
expect(
screen.getByRole("progressbar", { name: "AI spend usage" }),
).toHaveAttribute("aria-valuenow", "68");
@@ -97,7 +97,7 @@ export const AISpendWarning: Story = {
await waitFor(() =>
expect(document.body).toHaveTextContent("$1,080 / $1,200 USD"),
);
expect(document.body).toHaveTextContent("(AI spend/month)");
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
expect(
screen.getByRole("progressbar", { name: "AI spend usage" }),
).toHaveAttribute("aria-valuenow", "90");
@@ -144,7 +144,7 @@ export const AISpendExceeded: Story = {
await waitFor(() =>
expect(document.body).toHaveTextContent("$1,500 / $1,200 USD"),
);
expect(document.body).toHaveTextContent("(AI spend/month)");
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
expect(
screen.getByRole("progressbar", { name: "AI spend usage" }),
).toHaveAttribute("aria-valuenow", "100");
@@ -165,7 +165,7 @@ export const AISpendUnlimited: Story = {
await waitFor(() =>
expect(document.body).toHaveTextContent("$819 / Unlimited USD"),
);
expect(document.body).toHaveTextContent("(AI spend/month)");
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
expect(
screen.queryByRole("progressbar", { name: "AI spend usage" }),
).not.toBeInTheDocument();
@@ -272,7 +272,7 @@ export const AISpendHiddenOnInvalidData: Story = {
play: async ({ canvasElement, step }) => {
await step("hides AI spend on invalid data", async () => {
await openDropdown(canvasElement);
expect(screen.queryByText("(AI spend/month)")).not.toBeInTheDocument();
expect(screen.queryByText(/spend\/month/)).not.toBeInTheDocument();
});
},
};
@@ -293,7 +293,7 @@ export const AISpendHiddenOnNegativeLimit: Story = {
play: async ({ canvasElement, step }) => {
await step("hides AI spend on a negative limit", async () => {
await openDropdown(canvasElement);
expect(screen.queryByText("(AI spend/month)")).not.toBeInTheDocument();
expect(screen.queryByText(/spend\/month/)).not.toBeInTheDocument();
});
},
};
@@ -31,7 +31,7 @@ export const UserDropdownAISpend: FC<UserDropdownAISpendProps> = ({
/>
)}
<div className="mt-1 text-xs text-content-secondary">
(AI spend/month)
(Estimated AI spend/month)
</div>
</div>
);
@@ -0,0 +1,13 @@
import type { FC } from "react";
import { Link } from "#/components/Link/Link";
import { docs } from "#/utils/docs";
export const SpendEstimateDocsLink: FC = () => (
<Link
href={docs("/ai-coder/ai-gateway/cost-controls#how-spend-is-estimated")}
target="_blank"
rel="noreferrer"
>
How spend is estimated
</Link>
);
@@ -52,6 +52,7 @@ import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { isEveryoneGroup } from "#/modules/groups";
import { cn } from "#/utils/cn";
import { formatBudgetUSD } from "#/utils/currency";
import { SpendEstimateDocsLink } from "./AICostControl";
import {
effectiveBudgetGroup,
GroupMemberBudgetCells,
@@ -112,7 +113,7 @@ const GroupMembersPage: FC = () => {
}),
);
const aiBudgetNote = [
"Monthly AI spend for this user.",
"Estimated monthly AI spend for this user.",
// Spend resets at period_end, rendered in the viewer's local time.
aiSpend &&
`Resets ${dayjs(aiSpend.period_end).format("MMM D, YYYY h:mm A")}.`,
@@ -173,7 +174,13 @@ const GroupMembersPage: FC = () => {
message="AI spend couldn't be loaded, so budgets aren't shown."
/>
) : (
<StatusIconTooltip message={aiBudgetNote} />
<StatusIconTooltip
message={
<>
{aiBudgetNote} <SpendEstimateDocsLink />
</>
}
/>
)}
</div>
</TableHead>
@@ -365,7 +365,7 @@ export const WithMemberAIBudget: Story = {
);
await expect(
await body.findByText(
/^Monthly AI spend for this user\. Resets .*The group's default limit is \$7,000 per member\.$/,
/^Estimated monthly AI spend for this user\. Resets .*The group's default limit is \$7,000 per member\.$/,
),
).toBeInTheDocument();
await userEvent.click(
@@ -434,7 +434,9 @@ export const AIBudgetActionDisabledForOtherGroup: Story = {
}),
);
await expect(
await body.findByText(/^Monthly AI spend for this user\. Resets .*\.$/),
await body.findByText(
/^Estimated monthly AI spend for this user\. Resets .*\.$/,
),
).toBeInTheDocument();
await userEvent.keyboard("{Escape}");
+9 -1
View File
@@ -25,6 +25,7 @@ import {
} from "#/components/TableLoader/TableLoader";
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { docs } from "#/utils/docs";
import { SpendEstimateDocsLink } from "./AICostControl";
import { StatusIconTooltip } from "./StatusIconTooltip";
const EM_DASH = "\u2014";
@@ -94,7 +95,14 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
message="AI spend couldn't be loaded, so budgets aren't shown."
/>
) : (
<StatusIconTooltip message="Current AI spend compared to the group's AI budget for the active period." />
<StatusIconTooltip
message={
<>
Estimated AI spend compared to the group's AI budget for
the active period. <SpendEstimateDocsLink />
</>
}
/>
)}
</div>
</TableHead>