From 3544cd972a8e235df49624de79b45d4ef624fa23 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 27 May 2026 22:01:58 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20Harden=20Model?= =?UTF-8?q?=20Spec=20Icon=20Rendering=20(#13356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Harden model spec icon rendering * docs: Clarify model spec icon fallback --- .../Menus/Endpoints/components/SpecIcon.tsx | 2 +- .../components/__tests__/SpecIcon.test.tsx | 68 +++++++++++++++++++ .../__tests__/getModelSpecIconURL.test.ts | 53 +++++++++++++++ client/src/utils/endpoints.ts | 7 +- 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 client/src/components/Chat/Menus/Endpoints/components/__tests__/SpecIcon.test.tsx create mode 100644 client/src/utils/__tests__/getModelSpecIconURL.test.ts diff --git a/client/src/components/Chat/Menus/Endpoints/components/SpecIcon.tsx b/client/src/components/Chat/Menus/Endpoints/components/SpecIcon.tsx index 1a3d9ca480..6eef7656c3 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/SpecIcon.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/SpecIcon.tsx @@ -15,7 +15,7 @@ type IconType = (props: IconMapProps) => React.JSX.Element; const SpecIcon: React.FC = ({ currentSpec, endpointsConfig }) => { const iconURL = getModelSpecIconURL(currentSpec); - const { endpoint } = currentSpec.preset; + const endpoint = currentSpec.preset?.endpoint; const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL'); const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL }); let Icon: IconType; diff --git a/client/src/components/Chat/Menus/Endpoints/components/__tests__/SpecIcon.test.tsx b/client/src/components/Chat/Menus/Endpoints/components/__tests__/SpecIcon.test.tsx new file mode 100644 index 0000000000..2549d8c5e3 --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/components/__tests__/SpecIcon.test.tsx @@ -0,0 +1,68 @@ +import { render, screen } from '@testing-library/react'; +import { EModelEndpoint } from 'librechat-data-provider'; +import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider'; +import SpecIcon from '../SpecIcon'; + +jest.mock('~/hooks/Endpoint/Icons', () => { + const React = jest.requireActual('react'); + const createIcon = + (iconKey: string) => + ({ endpoint, iconURL }: { endpoint?: string | null; iconURL?: string }) => + React.createElement('span', { + 'data-testid': 'endpoint-icon', + 'data-icon-key': iconKey, + 'data-endpoint': endpoint ?? '', + 'data-icon-url': iconURL ?? '', + }); + + return { + icons: { + google: createIcon('google'), + openAI: createIcon('openAI'), + unknown: createIcon('unknown'), + }, + }; +}); + +jest.mock('~/components/Endpoints/URLIcon', () => { + const React = jest.requireActual('react'); + return { + URLIcon: ({ iconURL, endpoint }: { iconURL: string; endpoint?: string }) => + React.createElement('span', { + 'data-testid': 'url-icon', + 'data-icon-url': iconURL, + 'data-endpoint': endpoint ?? '', + }), + }; +}); + +describe('SpecIcon', () => { + const endpointsConfig = {} as TEndpointsConfig; + + it('renders the explicit spec icon when runtime spec data is missing preset', () => { + const currentSpec = { + name: 'gemini-test', + label: 'Gemini Test', + iconURL: EModelEndpoint.google, + } as TModelSpec; + + render(); + + expect(screen.getByTestId('endpoint-icon')).toHaveAttribute( + 'data-icon-key', + EModelEndpoint.google, + ); + expect(screen.getByTestId('endpoint-icon')).toHaveAttribute('data-endpoint', ''); + }); + + it('falls back to the unknown icon when runtime spec data has no icon or preset', () => { + const currentSpec = { + name: 'gemini-test', + label: 'Gemini Test', + } as TModelSpec; + + render(); + + expect(screen.getByTestId('endpoint-icon')).toHaveAttribute('data-icon-key', 'unknown'); + }); +}); diff --git a/client/src/utils/__tests__/getModelSpecIconURL.test.ts b/client/src/utils/__tests__/getModelSpecIconURL.test.ts new file mode 100644 index 0000000000..fdab800e92 --- /dev/null +++ b/client/src/utils/__tests__/getModelSpecIconURL.test.ts @@ -0,0 +1,53 @@ +import { EModelEndpoint } from 'librechat-data-provider'; +import type { TModelSpec } from 'librechat-data-provider'; +import { getModelSpecIconURL } from '../endpoints'; + +describe('getModelSpecIconURL', () => { + it('returns the explicit model spec icon before preset values', () => { + const modelSpec = { + name: 'gemini-test', + label: 'Gemini Test', + iconURL: EModelEndpoint.google, + preset: { + endpoint: EModelEndpoint.openAI, + iconURL: EModelEndpoint.anthropic, + }, + } as TModelSpec; + + expect(getModelSpecIconURL(modelSpec)).toBe(EModelEndpoint.google); + }); + + it('falls back to the preset icon URL when no spec icon is defined', () => { + const modelSpec = { + name: 'gemini-test', + label: 'Gemini Test', + preset: { + endpoint: EModelEndpoint.google, + iconURL: EModelEndpoint.openAI, + }, + } as TModelSpec; + + expect(getModelSpecIconURL(modelSpec)).toBe(EModelEndpoint.openAI); + }); + + it('falls back to the preset endpoint when no icon URL is defined', () => { + const modelSpec = { + name: 'gemini-test', + label: 'Gemini Test', + preset: { + endpoint: EModelEndpoint.google, + }, + } as TModelSpec; + + expect(getModelSpecIconURL(modelSpec)).toBe(EModelEndpoint.google); + }); + + it('returns an empty icon when a runtime model spec is missing preset data', () => { + const modelSpec = { + name: 'gemini-test', + label: 'Gemini Test', + } as TModelSpec; + + expect(getModelSpecIconURL(modelSpec)).toBe(''); + }); +}); diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index a27f71b8e9..1d922744eb 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -335,12 +335,9 @@ export function mergeQuerySettingsWithSpec( }; } -/** Gets the default spec iconURL by order or definition. - * - * First, the admin defined default, then last selected spec, followed by first spec - */ +/** Gets the model spec iconURL by explicit icon, preset icon, then preset endpoint. */ export function getModelSpecIconURL(modelSpec: t.TModelSpec) { - return modelSpec.iconURL ?? modelSpec.preset.iconURL ?? modelSpec.preset.endpoint ?? ''; + return modelSpec.iconURL ?? modelSpec.preset?.iconURL ?? modelSpec.preset?.endpoint ?? ''; } /** Gets the default frontend-facing endpoint, dependent on iconURL definition.