fix!: prevent AI provider name collision with static settings routes (#26688)

Move the providers routes into a dedicated providers sub-tree: `/ai/settings/providers`, `/ai/settings/providers/add`, and `/ai/settings/providers/:providerId`.

The old `/ai/settings/:providerId` and `/ai/settings/add` URLs are
removed without backward-compatibility redirects. Bookmarked or shared
links to these paths now return a 404. Creating a provider with id `models` (although unlikely) made it impossible to edit it due to a conflict with the static models route.
This commit is contained in:
Danielle Maywood
2026-06-25 11:09:58 +01:00
committed by GitHub
parent a7f3ea50b7
commit 1ae96fcf8a
16 changed files with 55 additions and 39 deletions
+2 -2
View File
@@ -30,8 +30,8 @@ handling, and how to monitor providers.
> configuration that is ineffectual.**
>
> The environment variables can be safely removed once seeding has
> completed. Visit `https://<your-coder-host>/ai/settings` to see which
> providers have been seeded.
> completed. Visit `https://<your-coder-host>/ai/settings/providers` to see
> which providers have been seeded.
After seeding, manage providers through the dashboard or API. A provider
that has been edited or removed there is not recreated or overwritten
@@ -184,7 +184,7 @@ updates, or change administrator expectations:
| Tasks is the primary AI coding workflow. | Coder Agents is the long-term replacement, and Tasks is supported through the 2.34 ESR window (into 2026). | Plan migration from the Tasks API to the Chats API and Coder Agents. See [Migrating from the Tasks API to the Chats API](../../ai-coder/agents/tasks-to-chats-migration.md). |
| AI Gateway injected MCP tools can be used for tool exposure. | Injected MCP tools are deprecated. | Move new integrations toward Coder Agents MCP server configuration or the MCP server flow. See [AI Gateway MCP](../../ai-coder/ai-gateway/mcp.md) and [MCP servers](../../ai-coder/agents/platform-controls/mcp-servers.md). |
| AI Bridge is opt-in via `CODER_AIBRIDGE_ENABLED` (default `false`). | The toggle is renamed to `CODER_AI_GATEWAY_ENABLED` and now defaults to `true`. | The in-memory AI Gateway now starts on every deployment. Set `CODER_AI_GATEWAY_ENABLED=false`, or the deprecated `CODER_AIBRIDGE_ENABLED` alias which still works, to keep the old behavior. |
| AI Gateway providers are configured with `CODER_AIBRIDGE_PROVIDER_*` or `CODER_AI_GATEWAY_PROVIDER_*` env vars. | Provider configuration is stored in the database. Env vars seed the database once on first startup, then are deprecated. | After upgrade, visit `/ai/settings` to verify seeded providers, then remove the env vars. Coderd fails to start if env vars drift from the seeded database row. See [AI Gateway providers](../../ai-coder/ai-gateway/providers.md). |
| AI Gateway providers are configured with `CODER_AIBRIDGE_PROVIDER_*` or `CODER_AI_GATEWAY_PROVIDER_*` env vars. | Provider configuration is stored in the database. Env vars seed the database once on first startup, then are deprecated. | After upgrade, visit `/ai/settings/providers` to verify seeded providers, then remove the env vars. Coderd fails to start if env vars drift from the seeded database row. See [AI Gateway providers](../../ai-coder/ai-gateway/providers.md). |
| Regular users can read their own AI Gateway interceptions. | Only owners and auditors can read AI Gateway interception data. | Update dashboards, scripts, or user workflows that expected self-service interception reads. This intentionally narrows the RBAC surface. |
| `coder groups list -o json` returns the old command output shape. | `coder groups list -o json` returns a flat structure matching other list commands. | Update scripts that parse this command output. |
| `coder tokens rm` deletes token records by default. | `coder tokens rm` expires tokens by default and keeps records for auditability. | Use `coder tokens rm --delete` only when the token record must be deleted. Update scripts that expect removed tokens to disappear from token history. |
@@ -257,7 +257,7 @@ The Coder team recommends taking the following steps when performing the upgrade
starts with changed parameters.
- **Audit AI Gateway integrations:** Update experimental API routes, check
permissions for interception/session data, migrate provider configuration
from env vars to the database via `/ai/settings`, verify proxy mode behavior,
from env vars to the database via `/ai/settings/providers`, verify proxy mode behavior,
and review any injected MCP usage.
- **Plan the Tasks to Agents migration:** Tasks remains available during the
support window, but new automation should use Coder Agents and the Chats API.
@@ -28,7 +28,7 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
</SidebarNavItem>
)}
{permissions.viewAnyAIProvider && (
<SidebarNavItem href="/ai/settings" end>
<SidebarNavItem href="/ai/settings/providers">
Providers
</SidebarNavItem>
)}
@@ -18,7 +18,7 @@ const AddProviderPage: React.FC = () => {
if (!provider) {
return (
<div className="flex flex-col items-start gap-4 pt-4 px-6">
<Link to="/ai/settings">
<Link to="/ai/settings/providers">
<Button variant="subtle">
<ArrowLeftIcon />
<span>Back to providers</span>
@@ -11,10 +11,10 @@ const meta: Meta<typeof AddProviderPageView> = {
decorators: [withToaster],
parameters: {
reactRouter: reactRouterParameters({
location: { path: "/ai/settings/add" },
location: { path: "/ai/settings/providers/add" },
routing: [
{ path: "/ai/settings", useStoryElement: true },
{ path: "/ai/settings/add", useStoryElement: true },
{ path: "/ai/settings/providers", useStoryElement: true },
{ path: "/ai/settings/providers/add", useStoryElement: true },
],
}),
},
@@ -28,7 +28,7 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
return (
<>
<Link to="/ai/settings" className="-ml-3">
<Link to="/ai/settings/providers" className="-ml-3">
<Button variant="subtle">
<ArrowLeftIcon />
<span>Back to providers</span>
@@ -63,7 +63,7 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
);
// Awaited so the form's submitting state stays true through
// navigation, keeping the unsaved-changes prompt suppressed.
await navigate(`/ai/settings/${res.name}`);
await navigate(`/ai/settings/providers/${res.name}`);
} catch (error) {
const name = values.name.trim();
toast.error(
@@ -15,11 +15,11 @@ const meta: Meta<typeof ProvidersPageView> = {
},
parameters: {
reactRouter: reactRouterParameters({
location: { path: "/ai/settings" },
location: { path: "/ai/settings/providers" },
routing: [
{ path: "/ai/settings", useStoryElement: true },
{ path: "/ai/settings/add", useStoryElement: true },
{ path: "/ai/settings/:providerId", useStoryElement: true },
{ path: "/ai/settings/providers", useStoryElement: true },
{ path: "/ai/settings/providers/add", useStoryElement: true },
{ path: "/ai/settings/providers/:providerId", useStoryElement: true },
],
}),
},
@@ -58,7 +58,7 @@ const AddProviderDropdown: React.FC<{ align?: "start" | "end" }> = ({
key={entry.value}
onSelect={() =>
void navigate(
`/ai/settings/add?type=${encodeURIComponent(entry.value)}`,
`/ai/settings/providers/add?type=${encodeURIComponent(entry.value)}`,
)
}
>
@@ -119,7 +119,9 @@ const ProvidersPageView: React.FC<ProvidersPageViewProps> = ({
<ProviderRow
key={provider.name}
provider={provider}
onClick={() => navigate(`/ai/settings/${provider.name}`)}
onClick={() =>
navigate(`/ai/settings/providers/${provider.name}`)
}
/>
))
)}
@@ -15,8 +15,8 @@ const routingFor = (path: string) =>
reactRouterParameters({
location: { path },
routing: [
{ path: "/ai/settings", useStoryElement: true },
{ path: "/ai/settings/:providerId", useStoryElement: true },
{ path: "/ai/settings/providers", useStoryElement: true },
{ path: "/ai/settings/providers/:providerId", useStoryElement: true },
],
});
@@ -35,21 +35,27 @@ type Story = StoryObj<typeof UpdateProviderPageView>;
export const OpenAI: Story = {
parameters: {
reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
reactRouter: routingFor(
`/ai/settings/providers/${MockAIProviderOpenAI.name}`,
),
...seed(MockAIProviderOpenAI),
},
};
export const Anthropic: Story = {
parameters: {
reactRouter: routingFor(`/ai/settings/${MockAIProviderAnthropic.name}`),
reactRouter: routingFor(
`/ai/settings/providers/${MockAIProviderAnthropic.name}`,
),
...seed(MockAIProviderAnthropic),
},
};
export const Bedrock: Story = {
parameters: {
reactRouter: routingFor(`/ai/settings/${MockAIProviderBedrock.name}`),
reactRouter: routingFor(
`/ai/settings/providers/${MockAIProviderBedrock.name}`,
),
...seed(MockAIProviderBedrock),
},
};
@@ -58,7 +64,9 @@ export const Bedrock: Story = {
// field and keeps the immutable name disabled.
export const Copilot: Story = {
parameters: {
reactRouter: routingFor(`/ai/settings/${MockAIProviderCopilot.name}`),
reactRouter: routingFor(
`/ai/settings/providers/${MockAIProviderCopilot.name}`,
),
...seed(MockAIProviderCopilot),
},
play: async ({ canvasElement }) => {
@@ -72,13 +80,15 @@ export const Copilot: Story = {
// No seeded query: the page renders the loader while useQuery fetches.
export const Loading: Story = {
parameters: {
reactRouter: routingFor("/ai/settings/loading-provider"),
reactRouter: routingFor("/ai/settings/providers/loading-provider"),
},
};
export const DeleteDialogOpen: Story = {
parameters: {
reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
reactRouter: routingFor(
`/ai/settings/providers/${MockAIProviderOpenAI.name}`,
),
...seed(MockAIProviderOpenAI),
},
play: async ({ canvasElement }) => {
@@ -29,7 +29,7 @@ import {
providerFormValuesToUpdate,
} from "../components/providerFormApiMap";
const BACK_HREF = "/ai/settings";
const BACK_HREF = "/ai/settings/providers";
const UpdateProviderPageView: React.FC = () => {
const { providerId } = useParams<{ providerId: string }>();
@@ -542,7 +542,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
)}
<div className="flex justify-end gap-4">
<Link to="/ai/settings">
<Link to="/ai/settings/providers">
<Button variant="outline" type="button">
Cancel
</Button>
@@ -544,7 +544,7 @@ export const MissingProviderAndModelSetup: Story = {
});
expect(canvas.getByRole("link", { name: "provider" })).toHaveAttribute(
"href",
"/ai/settings",
"/ai/settings/providers",
);
expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute(
"href",
@@ -607,7 +607,7 @@ export const MissingProviderSetup: Story = {
});
expect(canvas.getByRole("link", { name: "provider" })).toHaveAttribute(
"href",
"/ai/settings",
"/ai/settings/providers",
);
},
};
@@ -490,7 +490,7 @@ export const MissingProviderAndModelSetup: Story = {
});
expect(canvas.getByRole("link", { name: "provider" })).toHaveAttribute(
"href",
"/ai/settings",
"/ai/settings/providers",
);
expect(canvas.getByRole("link", { name: "model" })).toHaveAttribute(
"href",
@@ -35,7 +35,7 @@ export const AgentSetupNotice: FC<AgentSetupNoticeProps> = ({
<NoticeContainer>
To chat with Coder Agents, set up a{" "}
<Link
to="/ai/settings"
to="/ai/settings/providers"
className="text-content-link transition-colors hover:text-content-link/80"
>
provider
@@ -167,7 +167,7 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
icon={PlugIcon}
label="Providers"
active={false}
to="/ai/settings"
to="/ai/settings/providers"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
+11 -7
View File
@@ -472,11 +472,11 @@ const AISettingsUpdateMCPServerPage = lazy(
),
);
const AISettingsIndexPage = () => {
const AISettingsIndexRedirect = () => {
const { permissions } = useAuthenticated();
if (permissions.viewAnyAIProvider) {
return <AISettingsProvidersPage />;
return <Navigate to="/ai/settings/providers" replace />;
}
if (permissions.viewAIGatewayKeys) {
@@ -487,7 +487,7 @@ const AISettingsIndexPage = () => {
return <Navigate to="/ai/settings/models" replace />;
}
return <AISettingsProvidersPage />;
return <Navigate to="/ai/settings/providers" replace />;
};
const GlobalLayout = () => {
@@ -770,7 +770,7 @@ export const router = createBrowserRouter(
path="gateway-keys"
element={<AISettingsGatewayKeysPage />}
/>
<Route index element={<AISettingsIndexPage />} />
<Route index element={<AISettingsIndexRedirect />} />
<Route path="models" element={<AISettingsModelsPage />} />
<Route
path="instructions"
@@ -792,9 +792,13 @@ export const router = createBrowserRouter(
path="mcp-servers/:serverId"
element={<AISettingsUpdateMCPServerPage />}
/>
<Route path="add" element={<AISettingsAddProviderPage />} />
<Route path="providers" element={<AISettingsProvidersPage />} />
<Route
path=":providerId"
path="providers/add"
element={<AISettingsAddProviderPage />}
/>
<Route
path="providers/:providerId"
element={<AISettingsUpdateProviderPage />}
/>
</Route>
@@ -882,7 +886,7 @@ export const router = createBrowserRouter(
<Route path="api-keys" element={<AgentSettingsAPIKeysPage />} />
<Route
path="providers"
element={<Navigate to="/ai/settings" replace />}
element={<Navigate to="/ai/settings/providers" replace />}
/>
<Route
path="models"