diff --git a/site/src/modules/aiModels/providerStates.test.ts b/site/src/modules/aiModels/providerStates.test.ts index d0da0d1008d4d..c4c260ba229d4 100644 --- a/site/src/modules/aiModels/providerStates.test.ts +++ b/site/src/modules/aiModels/providerStates.test.ts @@ -27,30 +27,38 @@ const baseProviderState: ProviderState = { }; describe("deriveProviderStates", () => { - it("orders provider configs first, then catalog-only providers", () => { + it("orders providers alphabetically by display label", () => { const providerConfigs = [ { ...MockChatProviderConfig, - id: "prov-anthropic", - provider: "anthropic", - display_name: "Anthropic", + id: "prov-openai", + provider: "openai", + display_name: "OpenAI", }, ]; const catalog: TypesGen.ChatModelsResponse = { providers: [ - { ...MockChatModelProvider, provider: "anthropic" }, { ...MockChatModelProvider, provider: "google" }, + { ...MockChatModelProvider, provider: "anthropic" }, ], unsupported_providers: [], }; const states = deriveProviderStates([], providerConfigs, catalog); - expect(states.map((s) => s.provider)).toEqual(["anthropic", "google"]); - expect(states[0].key).toBe("prov-anthropic"); - expect(states[1].key).toBe("google"); + expect(states.map((s) => s.provider)).toEqual([ + "anthropic", + "google", + "openai", + ]); + expect(states.map((s) => s.label)).toEqual([ + "Anthropic", + "Google", + "OpenAI", + ]); expect(states[0].hasEffectiveAPIKey).toBe(true); expect(states[1].hasEffectiveAPIKey).toBe(true); + expect(states[2].hasEffectiveAPIKey).toBe(true); }); it("matches model configs to provider configs by ai_provider_id", () => { diff --git a/site/src/modules/aiModels/providerStates.ts b/site/src/modules/aiModels/providerStates.ts index ce06c9063fc12..c0f09ce1b810e 100644 --- a/site/src/modules/aiModels/providerStates.ts +++ b/site/src/modules/aiModels/providerStates.ts @@ -84,6 +84,7 @@ type ProviderEntry = { provider: string; }; +// Returns provider states ordered alphabetically by display label. export const deriveProviderStates = ( modelConfigs: readonly TypesGen.ChatModelConfig[], providerConfigs: TypesGen.ChatProviderConfig[] | null | undefined, @@ -144,7 +145,7 @@ export const deriveProviderStates = ( } } - return orderedEntries.map(({ key, provider }) => { + const states = orderedEntries.map(({ key, provider }) => { const providerConfigEntry = providerConfigsByKey.get(key); const providerConfigSource = providerConfigEntry?.source; const providerConfig = isDatabaseProviderConfig( @@ -188,6 +189,8 @@ export const deriveProviderStates = ( baseURL: getProviderBaseURL(providerConfigEntry), }; }); + + return states.toSorted((a, b) => a.label.localeCompare(b.label)); }; export const canManageProviderModels = ( diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx index 86e388dd2d6e6..2ca7758306dbd 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx @@ -68,11 +68,18 @@ export const Default: Story = { canvas.getByText("Claude Sonnet 4.5 (Bedrock)"), ).toBeInTheDocument(); await expect(canvas.getByText("AWS Bedrock")).toBeInTheDocument(); - // The Bedrock model config should render the Bedrock provider icon. - await expect(canvas.getByAltText("AWS Bedrock")).toBeInTheDocument(); + // The provider icon is decorative (alt=""), so its name comes from the + // visible label asserted above rather than the image alt text. await expect(canvas.getAllByText("Enabled").length).toBeGreaterThan(0); await expect(canvas.getByText("Default")).toBeInTheDocument(); await expect(canvas.getByText("Disabled")).toBeInTheDocument(); + + // The Add model menu lists each provider by exact accessible name; a + // regressed icon would turn a name into "Anthropic Anthropic". + await userEvent.click(canvas.getByRole("button", { name: /add model/i })); + const menu = await within(document.body).findByRole("menu"); + await within(menu).findByRole("menuitem", { name: "Anthropic" }); + await userEvent.keyboard("{Escape}"); }, }; @@ -94,7 +101,8 @@ export const FilterByProvider: Story = { name: /filter by provider/i, }); await userEvent.click(providerFilter); - const anthropicOption = await within(document.body).findByRole("option", { + const listbox = await within(document.body).findByRole("listbox"); + const anthropicOption = await within(listbox).findByRole("option", { name: "Anthropic", }); await userEvent.click(anthropicOption); diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx index 80426c630a29c..2b24b010d6859 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx @@ -214,7 +214,7 @@ const ModelsPageView: FC = ({