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.