diff --git a/site/.knip.jsonc b/site/.knip.jsonc
index d4cedd0c75..d12174a6f9 100644
--- a/site/.knip.jsonc
+++ b/site/.knip.jsonc
@@ -11,14 +11,6 @@
"ignore": [
"**/*Generated.ts",
"src/api/chatModelOptions.ts",
- // TODO(ai-settings): aiProviders.ts queries are staged in PR 2 of the
- // AI settings stack; they are consumed by the provider pages in PR 4.
- // Remove this exclusion once those pages land.
- "src/api/queries/aiProviders.ts",
- // TODO(ai-settings): addableProviderTypes.ts is staged in PR 3 of the
- // AI settings stack; its exports are consumed by the provider pages
- // in PR 4. Remove this exclusion once those pages land.
- "src/pages/AISettingsPage/ProvidersPage/components/addableProviderTypes.ts",
// TODO(devtools): debugPanelUtils.ts is staged in PR 7; its exports are
// consumed by the Debug panel components in PRs 8 and 9. Remove this
// exclusion once the panel components land.
diff --git a/site/src/modules/management/AISettingsSidebar.tsx b/site/src/modules/management/AISettingsSidebar.tsx
new file mode 100644
index 0000000000..c01bbb5285
--- /dev/null
+++ b/site/src/modules/management/AISettingsSidebar.tsx
@@ -0,0 +1,8 @@
+import AISettingsSidebarView from "#/modules/management/AISettingsSidebarView";
+
+/**
+ * A sidebar for AI settings.
+ */
+export const AISettingsSidebar: React.FC = () => {
+ return ;
+};
diff --git a/site/src/modules/management/AISettingsSidebarView.tsx b/site/src/modules/management/AISettingsSidebarView.tsx
new file mode 100644
index 0000000000..81447b4e5c
--- /dev/null
+++ b/site/src/modules/management/AISettingsSidebarView.tsx
@@ -0,0 +1,16 @@
+import {
+ Sidebar as BaseSidebar,
+ SettingsSidebarNavItem as SidebarNavItem,
+} from "#/components/Sidebar/Sidebar";
+
+const AISettingsSidebarView: React.FC = () => {
+ return (
+
+
+ Providers
+
+
+ );
+};
+
+export default AISettingsSidebarView;
diff --git a/site/src/pages/AISettingsPage/AISettingsLayout.tsx b/site/src/pages/AISettingsPage/AISettingsLayout.tsx
new file mode 100644
index 0000000000..8a44e10352
--- /dev/null
+++ b/site/src/pages/AISettingsPage/AISettingsLayout.tsx
@@ -0,0 +1,21 @@
+import { Suspense } from "react";
+import { Outlet } from "react-router";
+import { Loader } from "#/components/Loader/Loader";
+import { AISettingsSidebar } from "#/modules/management/AISettingsSidebar";
+
+const AISettingsLayout = () => {
+ return (
+
+ );
+};
+
+export default AISettingsLayout;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPage.tsx b/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPage.tsx
new file mode 100644
index 0000000000..74df05f479
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPage.tsx
@@ -0,0 +1,49 @@
+import { ArrowLeftIcon } from "lucide-react";
+import { Link, useSearchParams } from "react-router";
+import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
+import { Button } from "#/components/Button/Button";
+import { useAuthenticated } from "#/hooks/useAuthenticated";
+import { RequirePermission } from "#/modules/permissions/RequirePermission";
+import { pageTitle } from "#/utils/page";
+import { addableProviders } from "../components/addableProviderTypes";
+import AddProviderPageView from "./AddProviderPageView";
+
+const AddProviderPage: React.FC = () => {
+ const { permissions } = useAuthenticated();
+ const hasPermission = permissions.viewAnyAIProvider;
+ const [searchParams] = useSearchParams();
+ const typeParam = searchParams.get("type");
+
+ const provider = addableProviders.find((p) => p.value === typeParam);
+ if (!provider) {
+ return (
+
+
+
+
+ Back to providers
+
+
+
+ Provider type not found
+
+ The provider type you are trying to add is not valid. Please try
+ again.
+
+
+
+ );
+ }
+
+ return (
+
+
+ {pageTitle(`New ${provider.label} Provider`, "AI Providers")}
+
+
+
+
+ );
+};
+
+export default AddProviderPage;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPageView.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPageView.stories.tsx
new file mode 100644
index 0000000000..f7bbab9761
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPageView.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { reactRouterParameters } from "storybook-addon-remix-react-router";
+import { withToaster } from "#/testHelpers/storybook";
+import { addableProviders } from "../components/addableProviderTypes";
+import AddProviderPageView from "./AddProviderPageView";
+
+const meta: Meta = {
+ title: "pages/AISettingsPage/AddProviderPage",
+ component: AddProviderPageView,
+ decorators: [withToaster],
+ parameters: {
+ reactRouter: reactRouterParameters({
+ location: { path: "/ai/settings/add" },
+ routing: [
+ { path: "/ai/settings", useStoryElement: true },
+ { path: "/ai/settings/add", useStoryElement: true },
+ ],
+ }),
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const AddAnthropic: Story = {
+ args: {
+ provider: addableProviders.find((p) => p.value === "anthropic")!,
+ },
+};
+
+export const AddOpenAI: Story = {
+ args: {
+ provider: addableProviders.find((p) => p.value === "openai")!,
+ },
+};
+
+export const AddBedrock: Story = {
+ args: {
+ provider: addableProviders.find((p) => p.value === "bedrock")!,
+ },
+};
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPageView.tsx
new file mode 100644
index 0000000000..6f21156077
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPageView.tsx
@@ -0,0 +1,81 @@
+import { ArrowLeftIcon } from "lucide-react";
+import { useMutation, useQueryClient } from "react-query";
+import { Link, useNavigate } from "react-router";
+import { toast } from "sonner";
+import { getErrorMessage } from "#/api/errors";
+import { createAIProviderMutation } from "#/api/queries/aiProviders";
+import { Avatar } from "#/components/Avatar/Avatar";
+import { Button } from "#/components/Button/Button";
+import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
+import type { AddableProvider } from "../components/addableProviderTypes";
+import { ProviderForm } from "../components/ProviderForm";
+import { getProviderIcon } from "../components/ProviderIcon";
+import { providerFormValuesToCreate } from "../components/providerFormApiMap";
+
+interface AddProviderPageViewProps {
+ provider: AddableProvider;
+}
+
+const AddProviderPageView: React.FC = ({
+ provider,
+}) => {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const createMutation = useMutation(createAIProviderMutation(queryClient));
+
+ return (
+ <>
+
+
+
+ Back to providers
+
+
+
+
+
+
{`Add a ${provider.label} provider`}
+
+
+ Configure connection details and credentials.
+
+
+
{
+ const request = providerFormValuesToCreate(values);
+ try {
+ const res = await createMutation.mutateAsync(request);
+ toast.success(
+ `Provider "${res.display_name || res.name}" added.`,
+ );
+ // Awaited so the form's submitting state stays true through
+ // navigation, keeping the unsaved-changes prompt suppressed.
+ await navigate(`/ai/settings/${res.name}`);
+ } catch (error) {
+ const name = values.name.trim();
+ toast.error(
+ getErrorMessage(
+ error,
+ name
+ ? `Failed to add provider "${name}".`
+ : "Failed to add provider.",
+ ),
+ );
+ }
+ }}
+ />
+
+
+ >
+ );
+};
+
+export default AddProviderPageView;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPage.tsx b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPage.tsx
new file mode 100644
index 0000000000..6c08c9507e
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPage.tsx
@@ -0,0 +1,28 @@
+import { useQuery } from "react-query";
+import { aiProvidersList } from "#/api/queries/aiProviders";
+import { useAuthenticated } from "#/hooks/useAuthenticated";
+import { RequirePermission } from "#/modules/permissions/RequirePermission";
+import ProvidersPageView from "#/pages/AISettingsPage/ProvidersPage/ProvidersPageView";
+import { pageTitle } from "#/utils/page";
+
+const ProvidersPage: React.FC = () => {
+ const { permissions } = useAuthenticated();
+ const hasPermission = permissions.viewAnyAIProvider;
+
+ const providersQuery = useQuery(aiProvidersList());
+
+ return (
+
+ {pageTitle("AI Providers")}
+
+
+
+ );
+};
+
+export default ProvidersPage;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.stories.tsx
new file mode 100644
index 0000000000..e92a52409b
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.stories.tsx
@@ -0,0 +1,63 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { userEvent, within } from "storybook/test";
+import { reactRouterParameters } from "storybook-addon-remix-react-router";
+import { MockAIProviders } from "#/testHelpers/entities";
+import ProvidersPageView from "./ProvidersPageView";
+
+const meta: Meta = {
+ title: "pages/AISettingsPage/ProvidersPageView",
+ component: ProvidersPageView,
+ args: {
+ isLoading: false,
+ isFetching: false,
+ error: null,
+ providers: MockAIProviders,
+ },
+ parameters: {
+ reactRouter: reactRouterParameters({
+ location: { path: "/ai/settings" },
+ routing: [
+ { path: "/ai/settings", useStoryElement: true },
+ { path: "/ai/settings/add", useStoryElement: true },
+ { path: "/ai/settings/:providerId", useStoryElement: true },
+ ],
+ }),
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Loading: Story = {
+ args: {
+ isLoading: true,
+ isFetching: true,
+ },
+};
+
+export const EmptyProviders: Story = {
+ args: {
+ providers: [],
+ },
+};
+
+export const LoadError: Story = {
+ args: {
+ isLoading: false,
+ isFetching: false,
+ error: new Error("Failed to load providers"),
+ providers: [],
+ },
+};
+
+export const AddProviderDropdownOpen: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = await canvas.findByRole("button", {
+ name: /add provider/i,
+ });
+ await userEvent.click(trigger);
+ },
+};
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx
new file mode 100644
index 0000000000..dde7c8757b
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/ProvidersPageView.tsx
@@ -0,0 +1,126 @@
+import { ChevronDownIcon, PlusIcon } from "lucide-react";
+import { useNavigate } from "react-router";
+import type { AIProvider } from "#/api/typesGenerated";
+import { ErrorAlert } from "#/components/Alert/ErrorAlert";
+import { Button } from "#/components/Button/Button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "#/components/DropdownMenu/DropdownMenu";
+import {
+ SettingsHeader,
+ SettingsHeaderDescription,
+ SettingsHeaderTitle,
+} from "#/components/SettingsHeader/SettingsHeader";
+import {
+ Table,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "#/components/Table/Table";
+import { TableEmpty } from "#/components/TableEmpty/TableEmpty";
+import { TableLoader } from "#/components/TableLoader/TableLoader";
+import { addableProviders } from "#/pages/AISettingsPage/ProvidersPage/components/addableProviderTypes";
+import { ProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon";
+import { ProviderRow } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderRow";
+
+interface ProvidersPageViewProps {
+ isLoading: boolean;
+ isFetching: boolean;
+ error: unknown;
+ providers: AIProvider[];
+}
+
+const AddProviderDropdown: React.FC<{ align?: "start" | "end" }> = ({
+ align = "end",
+}) => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+ Add provider
+
+
+
+
+
+ Select a provider
+
+ {addableProviders.map((entry) => (
+
+ void navigate(
+ `/ai/settings/add?type=${encodeURIComponent(entry.value)}`,
+ )
+ }
+ >
+
+ {entry.label}
+
+ ))}
+
+
+ );
+};
+
+const ProvidersPageView: React.FC = ({
+ isLoading,
+ isFetching,
+ error,
+ providers,
+}) => {
+ const navigate = useNavigate();
+
+ return (
+
+
}>
+
Providers
+
+ Connect third-party LLM services like OpenAI, Anthropic, or Amazon
+ Bedrock. Each provider supplies models that users can select for their
+ conversations.
+
+
+ {Boolean(error) && (
+
+
+
+ )}
+
+
+
+ Name
+ Base URL
+ Status
+
+
+
+ {isLoading || isFetching ? (
+
+ ) : providers.length === 0 ? (
+ }
+ />
+ ) : (
+ providers.map((provider) => (
+ navigate(`/ai/settings/${provider.name}`)}
+ />
+ ))
+ )}
+
+
+
+ );
+};
+
+export default ProvidersPageView;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPage.tsx b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPage.tsx
new file mode 100644
index 0000000000..0ae990311b
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPage.tsx
@@ -0,0 +1,16 @@
+import { useAuthenticated } from "#/hooks/useAuthenticated";
+import { RequirePermission } from "#/modules/permissions/RequirePermission";
+import UpdateProviderPageView from "./UpdateProviderPageView";
+
+const UpdateProviderPage: React.FC = () => {
+ const { permissions } = useAuthenticated();
+ const hasPermission = permissions.viewAnyAIProvider;
+
+ return (
+
+
+
+ );
+};
+
+export default UpdateProviderPage;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.stories.tsx
new file mode 100644
index 0000000000..15713428f9
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.stories.tsx
@@ -0,0 +1,79 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { expect, screen, userEvent, within } from "storybook/test";
+import { reactRouterParameters } from "storybook-addon-remix-react-router";
+import type { AIProvider } from "#/api/typesGenerated";
+import {
+ MockAIProviderAnthropic,
+ MockAIProviderBedrock,
+ MockAIProviderOpenAI,
+} from "#/testHelpers/entities";
+import { withToaster } from "#/testHelpers/storybook";
+import UpdateProviderPageView from "./UpdateProviderPageView";
+
+const routingFor = (path: string) =>
+ reactRouterParameters({
+ location: { path },
+ routing: [
+ { path: "/ai/settings", useStoryElement: true },
+ { path: "/ai/settings/:providerId", useStoryElement: true },
+ ],
+ });
+
+const seed = (provider: AIProvider) => ({
+ queries: [{ key: ["ai", "providers", provider.name], data: provider }],
+});
+
+const meta: Meta = {
+ title: "pages/AISettingsPage/UpdateProviderPageView",
+ component: UpdateProviderPageView,
+ decorators: [withToaster],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const OpenAI: Story = {
+ parameters: {
+ reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
+ ...seed(MockAIProviderOpenAI),
+ },
+};
+
+export const Anthropic: Story = {
+ parameters: {
+ reactRouter: routingFor(`/ai/settings/${MockAIProviderAnthropic.name}`),
+ ...seed(MockAIProviderAnthropic),
+ },
+};
+
+export const Bedrock: Story = {
+ parameters: {
+ reactRouter: routingFor(`/ai/settings/${MockAIProviderBedrock.name}`),
+ ...seed(MockAIProviderBedrock),
+ },
+};
+
+// No seeded query: the page renders the loader while useQuery fetches.
+export const Loading: Story = {
+ parameters: {
+ reactRouter: routingFor("/ai/settings/loading-provider"),
+ },
+};
+
+export const DeleteDialogOpen: Story = {
+ parameters: {
+ reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
+ ...seed(MockAIProviderOpenAI),
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const deleteButton = await canvas.findByRole("button", {
+ name: /^delete$/i,
+ });
+ await userEvent.click(deleteButton);
+ // DeleteDialog renders via Radix portal, so search the document, not
+ // just the story canvas.
+ await expect(await screen.findByRole("dialog")).toBeInTheDocument();
+ await expect(await screen.findByText(/irreversible/i)).toBeInTheDocument();
+ },
+};
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx
new file mode 100644
index 0000000000..dbeb16003d
--- /dev/null
+++ b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx
@@ -0,0 +1,248 @@
+import { isAxiosError } from "axios";
+import { ArrowLeftIcon } from "lucide-react";
+import { useState } from "react";
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { Link, Navigate, useNavigate, useParams } from "react-router";
+import { toast } from "sonner";
+import { getErrorMessage } from "#/api/errors";
+import {
+ aiProvider,
+ deleteAIProviderMutation,
+ updateAIProviderMutation,
+} from "#/api/queries/aiProviders";
+import { Avatar } from "#/components/Avatar/Avatar";
+import { Badge } from "#/components/Badge/Badge";
+import { Button } from "#/components/Button/Button";
+import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
+import { Loader } from "#/components/Loader/Loader";
+import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
+import { Switch } from "#/components/Switch/Switch";
+import { pageTitle } from "#/utils/page";
+import { ProviderForm } from "../components/ProviderForm";
+import { getProviderIcon } from "../components/ProviderIcon";
+import {
+ aiProviderToFormValues,
+ getProviderDisplayType,
+ hasBedrockStoredCredentials,
+ isBedrockProvider,
+ providerFormValuesToUpdate,
+} from "../components/providerFormApiMap";
+
+const BACK_HREF = "/ai/settings";
+
+const UpdateProviderPageView: React.FC = () => {
+ const { providerId } = useParams<{ providerId: string }>();
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+
+ const providerQuery = useQuery({
+ ...aiProvider(providerId ?? ""),
+ enabled: Boolean(providerId),
+ });
+
+ const provider = providerQuery.data;
+ const providerIsOpenAiAnthropic =
+ provider !== undefined && !isBedrockProvider(provider);
+
+ const updateMutation = useMutation(
+ updateAIProviderMutation(queryClient, providerId ?? ""),
+ );
+
+ const deleteMutation = useMutation(
+ deleteAIProviderMutation(queryClient, providerId ?? ""),
+ );
+
+ // Rendered into every non-redirect return so the document title reflects
+ // the provider as soon as we know it; falls back to a placeholder while
+ // the query is in flight.
+ const title = (
+
+ {pageTitle(
+ (provider?.display_name || provider?.name) ?? "Loading...",
+ "AI Providers",
+ )}
+
+ );
+
+ if (!providerId) {
+ return ;
+ }
+
+ if (providerQuery.isLoading) {
+ return (
+ <>
+ {title}
+
+ >
+ );
+ }
+
+ if (providerQuery.isError) {
+ const status = isAxiosError(providerQuery.error)
+ ? providerQuery.error.response?.status
+ : undefined;
+ if (status === 404) {
+ return ;
+ }
+ return (
+ <>
+ {title}
+
+
+ {getErrorMessage(providerQuery.error, "Failed to load provider.")}
+
+
+
+
+ Back to providers
+
+
+
+ >
+ );
+ }
+
+ if (!provider) {
+ return ;
+ }
+
+ const openAiAnthropicSavedApiKey =
+ providerIsOpenAiAnthropic && provider.api_keys.length > 0;
+ const openAiAnthropicMaskedApiKey = providerIsOpenAiAnthropic
+ ? provider.api_keys[0]?.masked
+ : undefined;
+
+ return (
+ <>
+ {title}
+
+
+
+
+ Back to providers
+
+
+
{
+ setDeleteDialogOpen(true);
+ }}
+ >
+ Delete
+
+
+
+
+
+
+
+ {provider.display_name || provider.name}
+
+
+ {!provider.enabled &&
Disabled }
+
+
+
+ Add or update models for this provider.{" "}
+
+ Model settings
+
+
+
+ {
+ updateMutation.mutate(
+ { enabled: checked },
+ {
+ onSuccess: (updated) => {
+ toast.success(
+ `Provider "${updated.display_name || updated.name}" ${checked ? "enabled" : "disabled"}.`,
+ );
+ },
+ },
+ );
+ }}
+ disabled={updateMutation.isPending}
+ aria-label="Provider enabled"
+ />
+ Enable
+
+
+
+
{
+ const request = providerFormValuesToUpdate(values, provider);
+ try {
+ const updated = await updateMutation.mutateAsync(request);
+ toast.success(
+ `Provider "${updated.display_name || updated.name}" updated.`,
+ );
+ } catch (error) {
+ toast.error(
+ getErrorMessage(
+ error,
+ `Failed to update provider "${provider.display_name || provider.name}".`,
+ ),
+ );
+ }
+ }}
+ />
+
+
{
+ setDeleteDialogOpen(false);
+ }}
+ onConfirm={() => {
+ deleteMutation.mutate(undefined, {
+ onSuccess: () => {
+ toast.success(
+ `Provider "${provider.display_name || provider.name}" deleted.`,
+ );
+ setDeleteDialogOpen(false);
+ void navigate(BACK_HREF, { replace: true });
+ },
+ onError: (error) => {
+ toast.error(
+ getErrorMessage(
+ error,
+ `Failed to delete provider "${provider.display_name || provider.name}".`,
+ ),
+ );
+ },
+ });
+ }}
+ />
+
+ >
+ );
+};
+
+export default UpdateProviderPageView;
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx
index feaf29f898..f84dcc8375 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderIcon.tsx
@@ -5,7 +5,6 @@ type ProviderIconProps = {
provider: string;
};
-/** @lintignore Consumed by provider pages landing in the next PR of the AI settings stack. */
export const getProviderIcon = (provider: string): string | undefined => {
switch (provider) {
case "openai":
diff --git a/site/src/router.tsx b/site/src/router.tsx
index e84bec537e..6a7b04d151 100644
--- a/site/src/router.tsx
+++ b/site/src/router.tsx
@@ -429,6 +429,25 @@ const AIBridgeSessionThreadsPage = lazy(
() => import("./pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage"),
);
+const AISettingsLayout = lazy(
+ () => import("./pages/AISettingsPage/AISettingsLayout"),
+);
+const AISettingsProvidersPage = lazy(
+ () => import("./pages/AISettingsPage/ProvidersPage/ProvidersPage"),
+);
+const AISettingsUpdateProviderPage = lazy(
+ () =>
+ import(
+ "./pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPage"
+ ),
+);
+const AISettingsAddProviderPage = lazy(
+ () =>
+ import(
+ "./pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPage"
+ ),
+);
+
const GlobalLayout = () => {
return (
}>
@@ -678,6 +697,15 @@ export const router = createBrowserRouter(
} />
+ }>
+ } />
+ } />
+ }
+ />
+
+
}>
} />
} />
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index f15d839e14..c8b8a083ba 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -5574,7 +5574,6 @@ export const MockAIProviderBedrock: TypesGen.AIProvider = {
updated_at: "2026-05-14T10:00:00Z",
};
-/** @lintignore Consumed by page stories landing in PR 4 of the AI settings stack. */
export const MockAIProviders: TypesGen.AIProvider[] = [
MockAIProviderOpenAI,
MockAIProviderAnthropic,