diff --git a/api/server/services/Config/loadConfigModels.js b/api/server/services/Config/loadConfigModels.js index 9ef8994241..840d957fa1 100644 --- a/api/server/services/Config/loadConfigModels.js +++ b/api/server/services/Config/loadConfigModels.js @@ -85,7 +85,9 @@ async function loadConfigModels(req) { } if (Array.isArray(models.default)) { - modelsConfig[name] = models.default; + modelsConfig[name] = models.default.map((model) => + typeof model === 'string' ? model : model.name, + ); } } diff --git a/api/server/services/Config/loadConfigModels.spec.js b/api/server/services/Config/loadConfigModels.spec.js index b8d577667a..1e0e8780a7 100644 --- a/api/server/services/Config/loadConfigModels.spec.js +++ b/api/server/services/Config/loadConfigModels.spec.js @@ -254,8 +254,8 @@ describe('loadConfigModels', () => { // For groq and ollama, since the apiKey is "user_provided", models should not be fetched // Depending on your implementation's behavior regarding "default" models without fetching, // you may need to adjust the following assertions: - expect(result.groq).toBe(exampleConfig.endpoints.custom[2].models.default); - expect(result.ollama).toBe(exampleConfig.endpoints.custom[3].models.default); + expect(result.groq).toEqual(exampleConfig.endpoints.custom[2].models.default); + expect(result.ollama).toEqual(exampleConfig.endpoints.custom[3].models.default); // Verifying fetchModels was not called for groq and ollama expect(fetchModels).not.toHaveBeenCalledWith( diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx index 01d96432a7..d9464182b9 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx @@ -2,7 +2,12 @@ import React, { useMemo } from 'react'; import type { ModelSelectorProps } from '~/common'; import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext'; import { ModelSelectorChatProvider } from './ModelSelectorChatContext'; -import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components'; +import { + renderModelSpecs, + renderEndpoints, + renderSearchResults, + renderCustomGroups, +} from './components'; import { getSelectedIcon, getDisplayValue } from './utils'; import { CustomMenu as Menu } from './CustomMenu'; import DialogManager from './DialogManager'; @@ -86,8 +91,15 @@ function ModelSelectorContent() { renderSearchResults(searchResults, localize, searchValue) ) : ( <> - {renderModelSpecs(modelSpecs, selectedValues.modelSpec || '')} + {/* Render ungrouped modelSpecs (no group field) */} + {renderModelSpecs( + modelSpecs?.filter((spec) => !spec.group) || [], + selectedValues.modelSpec || '', + )} + {/* Render endpoints (will include grouped specs matching endpoint names) */} {renderEndpoints(mappedEndpoints ?? [])} + {/* Render custom groups (specs with group field not matching any endpoint) */} + {renderCustomGroups(modelSpecs || [], mappedEndpoints ?? [])} > )} diff --git a/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx b/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx new file mode 100644 index 0000000000..80d049cce7 --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/components/CustomGroup.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import type { TModelSpec } from 'librechat-data-provider'; +import { CustomMenu as Menu } from '../CustomMenu'; +import { ModelSpecItem } from './ModelSpecItem'; +import { useModelSelectorContext } from '../ModelSelectorContext'; + +interface CustomGroupProps { + groupName: string; + specs: TModelSpec[]; +} + +export function CustomGroup({ groupName, specs }: CustomGroupProps) { + const { selectedValues } = useModelSelectorContext(); + const { modelSpec: selectedSpec } = selectedValues; + + if (!specs || specs.length === 0) { + return null; + } + + return ( +
+ ); +} + +export function renderCustomGroups( + modelSpecs: TModelSpec[], + mappedEndpoints: Array<{ value: string }>, +) { + // Get all endpoint values to exclude them from custom groups + const endpointValues = new Set(mappedEndpoints.map((ep) => ep.value)); + + // Group specs by their group field (excluding endpoint-matched groups and ungrouped) + 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].push(spec); + return acc; + }, + {} as Record