diff --git a/MODEL_SPEC_FOLDERS.md b/MODEL_SPEC_FOLDERS.md new file mode 100644 index 0000000000..ef1a909899 --- /dev/null +++ b/MODEL_SPEC_FOLDERS.md @@ -0,0 +1,147 @@ +# Model Spec Subfolder Support + +This enhancement adds the ability to organize model specs into subfolders/categories for better organization and user experience. + +## Feature Overview + +Model specs can now be grouped into folders by adding an optional `folder` field to each spec. This helps organize related models together, making it easier for users to find and select the appropriate model for their needs. + +## Configuration + +### Basic Usage + +Add a `folder` field to any model spec in your `librechat.yaml`: + +```yaml +modelSpecs: + list: + - name: "gpt4_turbo" + label: "GPT-4 Turbo" + folder: "OpenAI Models" # This spec will appear under "OpenAI Models" folder + preset: + endpoint: "openAI" + model: "gpt-4-turbo-preview" +``` + +### Folder Structure + +- **With Folder**: Model specs with the `folder` field will be grouped under that folder name +- **Without Folder**: Model specs without the `folder` field appear at the root level +- **Multiple Folders**: You can create as many folders as needed to organize your models +- **Alphabetical Sorting**: Folders are sorted alphabetically, and specs within folders are sorted by their `order` field or label + +### Example Configuration + +```yaml +modelSpecs: + list: + # OpenAI Models Category + - name: "gpt4_turbo" + label: "GPT-4 Turbo" + folder: "OpenAI Models" + preset: + endpoint: "openAI" + model: "gpt-4-turbo-preview" + + - name: "gpt35_turbo" + label: "GPT-3.5 Turbo" + folder: "OpenAI Models" + preset: + endpoint: "openAI" + model: "gpt-3.5-turbo" + + # Anthropic Models Category + - name: "claude3_opus" + label: "Claude 3 Opus" + folder: "Anthropic Models" + preset: + endpoint: "anthropic" + model: "claude-3-opus-20240229" + + # Root level model (no folder) + - name: "quick_chat" + label: "Quick Chat" + preset: + endpoint: "openAI" + model: "gpt-3.5-turbo" +``` + +## UI Features + +### Folder Display +- Folders are displayed with expand/collapse functionality +- Folder icons change between open/closed states +- Indentation shows the hierarchy clearly + +### Search Integration +- When searching for models, the folder path is shown for context +- Search works across all models regardless of folder structure + +### User Experience +- Folders start expanded by default for easy access +- Click on folder header to expand/collapse +- Selected model is highlighted with a checkmark +- Folder state is preserved during the session + +## Benefits + +1. **Better Organization**: Group related models together (e.g., by provider, capability, or use case) +2. **Improved Navigation**: Users can quickly find models in organized categories +3. **Scalability**: Handles large numbers of model specs without overwhelming the UI +4. **Backward Compatible**: Existing configurations without folders continue to work +5. **Flexible Structure**: Mix foldered and non-foldered specs as needed + +## Use Cases + +### By Provider +```yaml +folder: "OpenAI Models" +folder: "Anthropic Models" +folder: "Google Models" +``` + +### By Capability +```yaml +folder: "Vision Models" +folder: "Code Models" +folder: "Creative Writing" +``` + +### By Performance Tier +```yaml +folder: "Premium Models" +folder: "Standard Models" +folder: "Budget Models" +``` + +### By Department/Team +```yaml +folder: "Engineering Team" +folder: "Marketing Team" +folder: "Research Team" +``` + +## Implementation Details + +### Type Changes +- Added optional `folder?: string` field to `TModelSpec` type +- Updated `tModelSpecSchema` to include the folder field validation + +### Components +- Created `ModelSpecFolder` component for rendering folder structure +- Updated `ModelSelector` to use folder-aware rendering +- Enhanced search results to show folder context + +### Behavior +- Folders are collapsible with state management +- Models are sorted within folders by order/label +- Root-level models appear after all folders + +## Migration + +No migration needed - the feature is fully backward compatible. Existing model specs without the `folder` field will continue to work and appear at the root level. + +## See Also + +- `librechat.example.subfolder.yaml` - Complete example configuration +- GitHub Issue #9165 - Original feature request \ No newline at end of file diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx index 01d96432a7..234194bf23 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx @@ -2,7 +2,7 @@ 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 { renderModelSpecsWithFolders, renderEndpoints, renderSearchResults } from './components'; import { getSelectedIcon, getDisplayValue } from './utils'; import { CustomMenu as Menu } from './CustomMenu'; import DialogManager from './DialogManager'; @@ -86,7 +86,7 @@ function ModelSelectorContent() { renderSearchResults(searchResults, localize, searchValue) ) : ( <> - {renderModelSpecs(modelSpecs, selectedValues.modelSpec || '')} + {renderModelSpecsWithFolders(modelSpecs, selectedValues.modelSpec || '')} {renderEndpoints(mappedEndpoints ?? [])} )} diff --git a/client/src/components/Chat/Menus/Endpoints/components/ModelSpecFolder.tsx b/client/src/components/Chat/Menus/Endpoints/components/ModelSpecFolder.tsx new file mode 100644 index 0000000000..7b49ad6524 --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/components/ModelSpecFolder.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react'; +import type { TModelSpec } from 'librechat-data-provider'; +import { ModelSpecItem } from './ModelSpecItem'; +import { cn } from '~/utils'; + +interface ModelSpecFolderProps { + folderName: string; + specs: TModelSpec[]; + selectedSpec: string; + level?: number; +} + +export function ModelSpecFolder({ + folderName, + specs, + selectedSpec, + level = 0 +}: ModelSpecFolderProps) { + const [isExpanded, setIsExpanded] = useState(true); + + const handleToggle = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsExpanded(!isExpanded); + }; + + const indent = level * 16; + + return ( +
+ + {isExpanded && ( +
+ {specs.map((spec) => ( +
+ +
+ ))} +
+ )} +
+ ); +} + +interface GroupedSpecs { + [folder: string]: TModelSpec[]; +} + +export function renderModelSpecsWithFolders(specs: TModelSpec[], selectedSpec: string) { + if (!specs || specs.length === 0) { + return null; + } + + // Group specs by folder + const grouped: GroupedSpecs = {}; + const rootSpecs: TModelSpec[] = []; + + specs.forEach((spec) => { + if (spec.folder) { + if (!grouped[spec.folder]) { + grouped[spec.folder] = []; + } + grouped[spec.folder].push(spec); + } else { + rootSpecs.push(spec); + } + }); + + // Sort folders alphabetically + const sortedFolders = Object.keys(grouped).sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()) + ); + + // Sort specs within each folder by order or label + sortedFolders.forEach(folder => { + grouped[folder].sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) { + return a.order - b.order; + } + return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); + }); + }); + + // Sort root specs + rootSpecs.sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) { + return a.order - b.order; + } + return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); + }); + + return ( + <> + {/* Render folders first */} + {sortedFolders.map((folder) => ( + + ))} + {/* Render root level specs */} + {rootSpecs.map((spec) => ( + + ))} + + ); +} \ No newline at end of file diff --git a/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx b/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx index ffefbc44d4..7838bb54a4 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx @@ -67,7 +67,12 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP )}
- {spec.label} + + {spec.folder && ( + {spec.folder} / + )} + {spec.label} + {spec.description && ( {spec.description} )} diff --git a/client/src/components/Chat/Menus/Endpoints/components/index.ts b/client/src/components/Chat/Menus/Endpoints/components/index.ts index d39ad4276f..a04e713885 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/index.ts +++ b/client/src/components/Chat/Menus/Endpoints/components/index.ts @@ -1,4 +1,5 @@ export * from './ModelSpecItem'; +export * from './ModelSpecFolder'; export * from './EndpointModelItem'; export * from './EndpointItem'; export * from './SearchResults'; diff --git a/librechat.example.subfolder.yaml b/librechat.example.subfolder.yaml new file mode 100644 index 0000000000..da0fafe17d --- /dev/null +++ b/librechat.example.subfolder.yaml @@ -0,0 +1,149 @@ +# Example configuration demonstrating model spec subfolder/category support +# This shows how to organize model specs into folders for better organization + +version: 1.1.7 + +modelSpecs: + enforce: false + prioritize: true + list: + # OpenAI Models Category + - name: "gpt4_turbo" + label: "GPT-4 Turbo" + folder: "OpenAI Models" # This spec will appear under "OpenAI Models" folder + preset: + endpoint: "openAI" + model: "gpt-4-turbo-preview" + temperature: 0.7 + description: "Latest GPT-4 Turbo model with enhanced capabilities" + iconURL: "openAI" + order: 1 + + - name: "gpt4_vision" + label: "GPT-4 Vision" + folder: "OpenAI Models" # Same folder as above + preset: + endpoint: "openAI" + model: "gpt-4-vision-preview" + temperature: 0.7 + description: "GPT-4 with vision capabilities" + iconURL: "openAI" + order: 2 + + - name: "gpt35_turbo" + label: "GPT-3.5 Turbo" + folder: "OpenAI Models" + preset: + endpoint: "openAI" + model: "gpt-3.5-turbo" + temperature: 0.7 + description: "Fast and efficient model for most tasks" + iconURL: "openAI" + order: 3 + + # Anthropic Models Category + - name: "claude3_opus" + label: "Claude 3 Opus" + folder: "Anthropic Models" # Different folder + preset: + endpoint: "anthropic" + model: "claude-3-opus-20240229" + temperature: 0.7 + description: "Most capable Claude model" + iconURL: "anthropic" + order: 1 + + - name: "claude3_sonnet" + label: "Claude 3 Sonnet" + folder: "Anthropic Models" + preset: + endpoint: "anthropic" + model: "claude-3-sonnet-20240229" + temperature: 0.7 + description: "Balanced performance and cost" + iconURL: "anthropic" + order: 2 + + - name: "claude3_haiku" + label: "Claude 3 Haiku" + folder: "Anthropic Models" + preset: + endpoint: "anthropic" + model: "claude-3-haiku-20240307" + temperature: 0.7 + description: "Fast and affordable" + iconURL: "anthropic" + order: 3 + + # Specialized Models Category + - name: "code_assistant" + label: "Code Assistant" + folder: "Specialized Models" + preset: + endpoint: "openAI" + model: "gpt-4-turbo-preview" + temperature: 0.2 + systemMessage: "You are an expert programmer. Provide clear, well-commented code solutions." + description: "Optimized for coding tasks" + iconURL: "openAI" + + - name: "creative_writer" + label: "Creative Writer" + folder: "Specialized Models" + preset: + endpoint: "anthropic" + model: "claude-3-opus-20240229" + temperature: 0.9 + systemMessage: "You are a creative writer. Generate engaging and imaginative content." + description: "For creative writing tasks" + iconURL: "anthropic" + + - name: "research_analyst" + label: "Research Analyst" + folder: "Specialized Models" + preset: + endpoint: "openAI" + model: "gpt-4-turbo-preview" + temperature: 0.3 + systemMessage: "You are a research analyst. Provide thorough, fact-based analysis." + description: "For research and analysis" + iconURL: "openAI" + + # Models without folders (appear at root level) + - name: "quick_chat" + label: "Quick Chat" + preset: + endpoint: "openAI" + model: "gpt-3.5-turbo" + temperature: 0.7 + description: "Fast general-purpose chat" + iconURL: "openAI" + default: true # This is the default model + + - name: "advanced_reasoning" + label: "Advanced Reasoning" + preset: + endpoint: "anthropic" + model: "claude-3-opus-20240229" + temperature: 0.5 + description: "For complex reasoning tasks" + iconURL: "anthropic" + +# Interface configuration +interface: + endpointsMenu: false # Hide endpoints menu when using model specs + modelSelect: false # Hide traditional model selector + parameters: false # Hide parameter controls (using presets) + presets: false # Hide preset selector (using model specs) + +# Endpoints configuration (required for the model specs to work) +endpoints: + openAI: + apiKey: "${OPENAI_API_KEY}" + models: + default: ["gpt-3.5-turbo", "gpt-4-turbo-preview", "gpt-4-vision-preview"] + + anthropic: + apiKey: "${ANTHROPIC_API_KEY}" + models: + default: ["claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"] \ No newline at end of file diff --git a/packages/data-provider/src/models.ts b/packages/data-provider/src/models.ts index c925781bff..199c08b406 100644 --- a/packages/data-provider/src/models.ts +++ b/packages/data-provider/src/models.ts @@ -19,6 +19,7 @@ export type TModelSpec = { showIconInHeader?: boolean; iconURL?: string | EModelEndpoint; // Allow using project-included icons authType?: AuthType; + folder?: string; // Optional folder/category for grouping model specs }; export const tModelSpecSchema = z.object({ @@ -32,6 +33,7 @@ export const tModelSpecSchema = z.object({ showIconInHeader: z.boolean().optional(), iconURL: z.union([z.string(), eModelEndpointSchema]).optional(), authType: authTypeSchema.optional(), + folder: z.string().optional(), }); export const specsConfigSchema = z.object({