mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>
|
||||
|
||||
+3
-3
@@ -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}`)
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
+18
-8
@@ -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 }) => {
|
||||
|
||||
+1
-1
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user