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 (
+
+

+
+ );
+};
+
+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(),