From 308347a7ad71212700aa65549f29644186e35dfb Mon Sep 17 00:00:00 2001 From: odrec Date: Wed, 3 Dec 2025 10:51:18 +0100 Subject: [PATCH] feat: add groupIcon property to modelSpecs for custom group icons Added the ability to define icons for custom model spec groups in the UI selector. Changes: - Added property to TModelSpec type and schema in data-provider - Created GroupIcon component to render URL or built-in endpoint icons - Updated CustomGroup component to display group icons - Added documentation and examples in librechat.example.yaml Usage: The groupIcon can be: - A built-in endpoint key (e.g., "openAI", "anthropic", "groq") - A URL to a custom icon image Only the first spec in a group needs groupIcon - all specs share the same icon. --- .../Endpoints/components/CustomGroup.tsx | 24 ++++++-- .../Menus/Endpoints/components/GroupIcon.tsx | 60 +++++++++++++++++++ librechat.example.yaml | 19 +++++- packages/data-provider/src/models.ts | 7 +++ 4 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx diff --git a/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx b/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx index 80d049cce7..a71c676f9c 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx @@ -3,13 +3,15 @@ import type { TModelSpec } from 'librechat-data-provider'; import { CustomMenu as Menu } from '../CustomMenu'; import { ModelSpecItem } from './ModelSpecItem'; import { useModelSelectorContext } from '../ModelSelectorContext'; +import GroupIcon from './GroupIcon'; interface CustomGroupProps { groupName: string; specs: TModelSpec[]; + groupIcon?: string; } -export function CustomGroup({ groupName, specs }: CustomGroupProps) { +export function CustomGroup({ groupName, specs, groupIcon }: CustomGroupProps) { const { selectedValues } = useModelSelectorContext(); const { modelSpec: selectedSpec } = selectedValues; @@ -25,6 +27,11 @@ export function CustomGroup({ groupName, specs }: CustomGroupProps) { label={
+ {groupIcon && ( +
+ +
+ )} {groupName}
@@ -45,22 +52,27 @@ export function renderCustomGroups( const endpointValues = new Set(mappedEndpoints.map((ep) => ep.value)); // Group specs by their group field (excluding endpoint-matched groups and ungrouped) + // Also track the groupIcon for each group (first spec with groupIcon wins) const customGroups = modelSpecs.reduce( (acc, spec) => { if (!spec.group || endpointValues.has(spec.group)) { return acc; } if (!acc[spec.group]) { - acc[spec.group] = []; + acc[spec.group] = { specs: [], groupIcon: undefined }; + } + acc[spec.group].specs.push(spec); + // Use the first groupIcon found for the group + if (!acc[spec.group].groupIcon && spec.groupIcon) { + acc[spec.group].groupIcon = spec.groupIcon; } - acc[spec.group].push(spec); return acc; }, - {} as Record, + {} as Record, ); // Render each custom group - return Object.entries(customGroups).map(([groupName, specs]) => ( - + return Object.entries(customGroups).map(([groupName, { specs, groupIcon }]) => ( + )); } diff --git a/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx b/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx new file mode 100644 index 0000000000..5f6fe351bb --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/components/GroupIcon.tsx @@ -0,0 +1,60 @@ +import React, { memo, useState } from 'react'; +import { AlertCircle } from 'lucide-react'; +import type { IconMapProps } from '~/common'; +import { icons } from '~/hooks/Endpoint/Icons'; + +interface GroupIconProps { + iconURL: string; + groupName: string; +} + +type IconType = (props: IconMapProps) => React.JSX.Element; + +const GroupIcon: React.FC = ({ iconURL, groupName }) => { + const [imageError, setImageError] = useState(false); + + const handleImageError = () => { + setImageError(true); + }; + + // Check if the iconURL is a URL or a built-in icon key + if (!iconURL.includes('http')) { + const Icon: IconType = (icons[iconURL] ?? icons.unknown) as IconType; + return ; + } + + if (imageError || !iconURL) { + const DefaultIcon: IconType = icons.unknown as IconType; + return ( +
+
+ +
+ {imageError && iconURL && ( +
+ +
+ )} +
+ ); + } + + return ( +
+ {groupName} +
+ ); +}; + +export default memo(GroupIcon); diff --git a/librechat.example.yaml b/librechat.example.yaml index f163f8d4ac..c545df5987 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -339,6 +339,10 @@ endpoints: # - If 'group' matches an endpoint name (e.g., "openAI", "groq"), the spec appears nested under that endpoint # - If 'group' is a custom name (doesn't match any endpoint), it creates a separate collapsible section # - If 'group' is omitted, the spec appears as a standalone item at the top level +# +# The 'groupIcon' field sets an icon for custom groups: +# - Only needs to be set on one spec per group (first one is used) +# - Can be a URL or a built-in endpoint key (e.g., "openAI", "anthropic", "groq") # modelSpecs: # list: # # Example 1: Nested under an endpoint (grouped with openAI endpoint) @@ -359,11 +363,12 @@ endpoints: # endpoint: "groq" # model: "llama3-70b-8192" # -# # Example 3: Custom group (creates a separate collapsible section) +# # Example 3: Custom group with icon (creates a separate collapsible section) # - name: "coding-assistant" # label: "Coding Assistant" # description: "Specialized for coding tasks" # group: "my-assistants" # Custom string - doesn't match any endpoint, so creates its own group +# groupIcon: "https://example.com/icons/assistants.png" # Icon URL for the group # preset: # endpoint: "openAI" # model: "gpt-4o" @@ -374,12 +379,22 @@ endpoints: # label: "Writing Assistant" # description: "Specialized for creative writing" # group: "my-assistants" # Same custom group name - both specs appear in same section +# # No need to set groupIcon again - the first spec's icon is used # preset: # endpoint: "anthropic" # model: "claude-sonnet-4" # instructions: "You are a creative writing expert..." # -# # Example 4: Standalone (no group - appears at top level) +# # Example 4: Custom group using built-in icon key +# - name: "fast-models" +# label: "Fast Response Model" +# group: "Fast Models" +# groupIcon: "groq" # Uses the built-in Groq icon +# preset: +# endpoint: "groq" +# model: "llama3-8b-8192" +# +# # Example 5: Standalone (no group - appears at top level) # - name: "general-assistant" # label: "General Assistant" # description: "General purpose assistant" diff --git a/packages/data-provider/src/models.ts b/packages/data-provider/src/models.ts index 1edca6ea37..3c3c197660 100644 --- a/packages/data-provider/src/models.ts +++ b/packages/data-provider/src/models.ts @@ -22,6 +22,12 @@ export type TModelSpec = { * - If omitted, the spec appears as a standalone item at the top level */ group?: string; + /** + * Optional icon URL for the group this spec belongs to. + * Only needs to be set on one spec per group - the first one found with a groupIcon will be used. + * Can be a URL or an endpoint name to use its icon. + */ + groupIcon?: string | EModelEndpoint; showIconInMenu?: boolean; showIconInHeader?: boolean; iconURL?: string | EModelEndpoint; // Allow using project-included icons @@ -40,6 +46,7 @@ export const tModelSpecSchema = z.object({ default: z.boolean().optional(), description: z.string().optional(), group: z.string().optional(), + groupIcon: z.union([z.string(), eModelEndpointSchema]).optional(), showIconInMenu: z.boolean().optional(), showIconInHeader: z.boolean().optional(), iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),