mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-01 11:53:55 +00:00
🥷 feat: Add showInMenu Option to Model Specs (#14034)
Add an optional `showInMenu` flag to model specs. When set to false, the spec is dropped from the model selector menu and from the client startup config (GET /api/config), but remains resolvable server-side by name — a request that sends `spec: "<name>"` still works, since server-side resolution uses the full, unfiltered list. Unlike `showIconInMenu` (which only hides the icon), this hides the whole entry. The flag is optional and defaults to listed, so existing specs are unaffected. Adds an `excludeHiddenModelSpecs()` helper (applied before `sanitizeModelSpecs`) plus unit tests.
This commit is contained in:
parent
927a5957cb
commit
ac759ef2f7
4 changed files with 79 additions and 1 deletions
|
|
@ -6,6 +6,7 @@ const {
|
|||
resolveBuildInfo,
|
||||
resolveTitleTiming,
|
||||
sanitizeModelSpecs,
|
||||
excludeHiddenModelSpecs,
|
||||
isFileSnapshotEnabled,
|
||||
} = require('@librechat/api');
|
||||
const { EModelEndpoint, defaultSocialLogins } = require('librechat-data-provider');
|
||||
|
|
@ -265,7 +266,7 @@ router.get('/', async function (req, res) {
|
|||
endpoint: EModelEndpoint.agents,
|
||||
}),
|
||||
turnstile: appConfig?.turnstileConfig,
|
||||
modelSpecs: sanitizeModelSpecs(appConfig?.modelSpecs),
|
||||
modelSpecs: sanitizeModelSpecs(excludeHiddenModelSpecs(appConfig?.modelSpecs)),
|
||||
balance: balanceConfig,
|
||||
bundlerURL: process.env.SANDPACK_BUNDLER_URL,
|
||||
staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL,
|
||||
|
|
|
|||
49
packages/api/src/modelSpecs/excludeHiddenModelSpecs.test.ts
Normal file
49
packages/api/src/modelSpecs/excludeHiddenModelSpecs.test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TModelSpec } from 'librechat-data-provider';
|
||||
import { excludeHiddenModelSpecs } from './index';
|
||||
|
||||
const makeSpec = (name: string, showInMenu?: boolean): TModelSpec => ({
|
||||
name,
|
||||
label: name,
|
||||
...(showInMenu === undefined ? {} : { showInMenu }),
|
||||
preset: {
|
||||
endpoint: EModelEndpoint.bedrock,
|
||||
model: 'claude-sonnet-4-6',
|
||||
},
|
||||
});
|
||||
|
||||
describe('excludeHiddenModelSpecs', () => {
|
||||
it('drops specs marked showInMenu: false and keeps the rest', () => {
|
||||
const modelSpecs = {
|
||||
enforce: false,
|
||||
prioritize: true,
|
||||
list: [
|
||||
makeSpec('listed-default'),
|
||||
makeSpec('listed-explicit', true),
|
||||
makeSpec('hidden', false),
|
||||
],
|
||||
};
|
||||
|
||||
const result = excludeHiddenModelSpecs(modelSpecs);
|
||||
|
||||
expect(result.list.map((s) => s.name)).toEqual(['listed-default', 'listed-explicit']);
|
||||
});
|
||||
|
||||
it('treats an omitted showInMenu as listed (backwards compatible)', () => {
|
||||
const modelSpecs = { list: [makeSpec('no-flag')] };
|
||||
expect(excludeHiddenModelSpecs(modelSpecs).list).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not mutate the input', () => {
|
||||
const modelSpecs = { list: [makeSpec('keep'), makeSpec('hidden', false)] };
|
||||
excludeHiddenModelSpecs(modelSpecs);
|
||||
expect(modelSpecs.list).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns the config unchanged when there is no list', () => {
|
||||
expect(excludeHiddenModelSpecs(undefined)).toBeUndefined();
|
||||
expect(excludeHiddenModelSpecs(null)).toBeNull();
|
||||
const noList = { enforce: false, prioritize: true };
|
||||
expect(excludeHiddenModelSpecs(noList)).toBe(noList);
|
||||
});
|
||||
});
|
||||
|
|
@ -186,6 +186,25 @@ export function resolveModelSpecPromptPrefixVariables<T extends { promptPrefix?:
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops specs marked `showInMenu: false` so they are not advertised to clients
|
||||
* (the model selector menu and the startup config). Such specs remain resolvable
|
||||
* server-side by name, so a caller that sets `spec: "<name>"` can still use them.
|
||||
* Returns the config unchanged when there is no list. Apply before `sanitizeModelSpecs`.
|
||||
*/
|
||||
export function excludeHiddenModelSpecs<T extends Partial<TSpecsConfig> | null | undefined>(
|
||||
modelSpecs: T,
|
||||
): T {
|
||||
if (!modelSpecs?.list || !Array.isArray(modelSpecs.list)) {
|
||||
return modelSpecs;
|
||||
}
|
||||
|
||||
return {
|
||||
...modelSpecs,
|
||||
list: modelSpecs.list.filter((modelSpec) => modelSpec?.showInMenu !== false),
|
||||
} as T;
|
||||
}
|
||||
|
||||
export function sanitizeModelSpecs<T extends Partial<TSpecsConfig> | null | undefined>(
|
||||
modelSpecs: T,
|
||||
): T {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ export type TModelSpec = {
|
|||
showOnLanding?: boolean;
|
||||
/** Conversation starter prompts shown on the chat landing while this spec is active. */
|
||||
conversation_starters?: string[];
|
||||
/**
|
||||
* When false, the spec is omitted from the model selector menu and from the
|
||||
* client startup config, but remains usable when invoked explicitly by name
|
||||
* via the `spec` field (server-side resolution uses the full, unfiltered list).
|
||||
* Unlike `showIconInMenu` (which only hides the icon), this hides the whole entry.
|
||||
* Defaults to true (listed).
|
||||
*/
|
||||
showInMenu?: boolean;
|
||||
iconURL?: string | EModelEndpoint; // Allow using project-included icons
|
||||
authType?: AuthType;
|
||||
/** Hide the chat input tool badge row while this model spec is active. */
|
||||
|
|
@ -71,6 +79,7 @@ export const tModelSpecSchema = z.object({
|
|||
showIconInHeader: z.boolean().optional(),
|
||||
showOnLanding: z.boolean().optional(),
|
||||
conversation_starters: z.array(z.string()).optional(),
|
||||
showInMenu: z.boolean().optional(),
|
||||
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
|
||||
authType: authTypeSchema.optional(),
|
||||
hideBadgeRow: z.boolean().optional(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue