🥛 fix: Drop Stale Saved Model Defaults in Builder Forms (#15179)

This commit is contained in:
Ravi Kumar L 2026-08-25 14:18:17 +02:00 committed by GitHub
parent 4b113697b5
commit 3d2da403ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 533 additions and 79 deletions

View file

@ -227,6 +227,7 @@ export type AgentModelPanelProps = {
agent_id?: string;
providers: Option[];
models: Record<string, string[] | undefined>;
modelsReady: boolean;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};

View file

@ -10,6 +10,20 @@ import type { AgentForm } from '~/common';
// Mock toast context - define this after all mocks
let mockShowToast: jest.Mock;
let mockModelsQuery: {
data: Record<string, string[]>;
isFetchedAfterMount: boolean;
isSuccess: boolean;
isFetching?: boolean;
} = { data: {}, isFetchedAfterMount: true, isSuccess: true };
let mockAgentPanelContext = {
activePanel: 'builder',
agentsConfig: { allowedProviders: [] as string[] },
setActivePanel: jest.fn(),
endpointsConfig: {},
setCurrentAgentId: jest.fn(),
agent_id: 'agent-123' as string | undefined,
};
// Mock notification severity enum before other imports
jest.mock('~/common/types', () => ({
@ -77,7 +91,7 @@ jest.mock('@librechat/client', () => ({
// Mock other dependencies
jest.mock('librechat-data-provider/react-query', () => ({
useGetModelsQuery: () => ({ data: {} }),
useGetModelsQuery: () => mockModelsQuery,
useGetEffectivePermissionsQuery: () => ({
data: { permissionBits: 0xffffffff }, // All permissions
isLoading: false,
@ -87,6 +101,8 @@ jest.mock('librechat-data-provider/react-query', () => ({
jest.mock('~/utils', () => ({
createProviderOption: jest.fn((provider: string) => ({ value: provider, label: provider })),
getAvailableAgentSelection: jest.requireActual('~/utils/agentModelSelection')
.getAvailableAgentSelection,
getDefaultAgentFormValues: jest.fn(() => ({
id: '',
name: '',
@ -110,14 +126,7 @@ jest.mock('~/hooks/useResourcePermissions', () => ({
}));
jest.mock('~/Providers/AgentPanelContext', () => ({
useAgentPanelContext: () => ({
activePanel: 'builder',
agentsConfig: { allowedProviders: [] },
setActivePanel: jest.fn(),
endpointsConfig: {},
setCurrentAgentId: jest.fn(),
agent_id: 'agent-123',
}),
useAgentPanelContext: () => mockAgentPanelContext,
}));
jest.mock('~/common', () => ({
@ -202,7 +211,6 @@ jest.mock('react-hook-form', () => {
};
},
FormProvider: ({ children }: any) => children,
useWatch: () => 'agent-123',
};
});
@ -300,9 +308,111 @@ describe('AgentPanel - Update Agent Toast Messages', () => {
mockShowToast = jest.fn();
mockFormSubmitHandler = null;
capturedFormMethods = null;
mockModelsQuery = {
data: { openai: ['gpt-4'] },
isFetchedAfterMount: true,
isSuccess: true,
};
mockAgentPanelContext = {
activePanel: 'builder',
agentsConfig: { allowedProviders: [] },
setActivePanel: jest.fn(),
endpointsConfig: { openai: {} },
setCurrentAgentId: jest.fn(),
agent_id: 'agent-123',
};
localStorage.clear();
});
describe('AgentPanel', () => {
it('restores saved defaults from the current model catalogue', async () => {
const { mockUseGetAgentByIdQuery } = setupMocks();
mockAgentQuery(mockUseGetAgentByIdQuery, {});
mockAgentPanelContext = {
...mockAgentPanelContext,
endpointsConfig: { custom: {} },
agent_id: undefined,
};
mockModelsQuery = {
data: { custom: ['cached-model'] },
isFetchedAfterMount: true,
isSuccess: true,
isFetching: true,
};
localStorage.setItem('lastAgentProvider', 'custom');
localStorage.setItem('lastAgentModel', 'current-model');
const Wrapper = createWrapper();
const { rerender } = render(<AgentPanel />, { wrapper: Wrapper });
mockModelsQuery = {
data: { custom: ['current-model'] },
isFetchedAfterMount: true,
isSuccess: true,
isFetching: false,
};
rerender(<AgentPanel />);
await waitFor(() => {
expect(capturedFormMethods?.getValues('provider')).toEqual({
value: 'custom',
label: 'custom',
});
expect(capturedFormMethods?.getValues('model')).toBe('current-model');
});
});
it('clears unavailable saved defaults', async () => {
const { mockUseGetAgentByIdQuery } = setupMocks();
mockAgentQuery(mockUseGetAgentByIdQuery, {});
mockAgentPanelContext = {
...mockAgentPanelContext,
endpointsConfig: { custom: {} },
agent_id: undefined,
};
mockModelsQuery = {
data: { custom: ['current-model'] },
isFetchedAfterMount: true,
isSuccess: true,
};
localStorage.setItem('lastAgentProvider', 'custom');
localStorage.setItem('lastAgentModel', 'removed-model');
const Wrapper = createWrapper();
render(<AgentPanel />, { wrapper: Wrapper });
await waitFor(() => {
expect(capturedFormMethods?.getValues('provider')).toEqual({
value: 'custom',
label: 'custom',
});
expect(capturedFormMethods?.getValues('model')).toBe('');
});
expect(localStorage.getItem('lastAgentModel')).toBeNull();
});
it("preserves an existing agent's configured model", async () => {
const { mockUseGetAgentByIdQuery } = setupMocks();
mockAgentQuery(mockUseGetAgentByIdQuery, {});
mockAgentPanelContext = {
...mockAgentPanelContext,
endpointsConfig: { bedrock: {} },
};
mockModelsQuery = {
data: { bedrock: ['current-model'] },
isFetchedAfterMount: true,
isSuccess: true,
};
const Wrapper = createWrapper();
render(<AgentPanel />, { wrapper: Wrapper });
await waitFor(() => {
expect(capturedFormMethods?.getValues('provider')).toBe('openai');
expect(capturedFormMethods?.getValues('model')).toBe('gpt-4');
});
});
it('should show "no changes" toast when version does not change', async () => {
const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks();

View file

@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useRef, useState } from 'react';
import React, { useMemo, useCallback, useEffect, useRef, useState } from 'react';
import { Plus } from 'lucide-react';
import isEqual from 'lodash/isEqual';
import { Button, useToastContext } from '@librechat/client';
@ -10,6 +10,7 @@ import {
SystemRoles,
ResourceType,
EModelEndpoint,
LocalStorageKeys,
PermissionBits,
removeCodeExecutionCaller,
resolveStatefulCodeEnvironment,
@ -26,7 +27,11 @@ import {
useGetExpandedAgentByIdQuery,
useUploadAgentAvatarMutation,
} from '~/data-provider';
import { createProviderOption, getDefaultAgentFormValues } from '~/utils';
import {
createProviderOption,
getAvailableAgentSelection,
getDefaultAgentFormValues,
} from '~/utils';
import { useResourcePermissions } from '~/hooks/useResourcePermissions';
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
@ -315,6 +320,7 @@ export default function AgentPanel() {
const agentQuery = canEdit && expandedAgentQuery.data ? expandedAgentQuery : basicAgentQuery;
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
const modelsReady = modelsQuery.isFetchedAfterMount && !modelsQuery.isFetching;
const methods = useForm<AgentForm>({
defaultValues: getDefaultAgentFormValues(defaultStatefulCodeEnvironment),
mode: 'onChange',
@ -329,6 +335,7 @@ export default function AgentPanel() {
formState: { dirtyFields },
} = methods;
const [isAvatarUploadInFlight, setIsAvatarUploadInFlight] = useState(false);
const uploadAvatarMutation = useUploadAgentAvatarMutation({
onSuccess: (updatedAgent) => {
showToast({ message: localize('com_ui_upload_agent_avatar') });
@ -394,6 +401,56 @@ export default function AgentPanel() {
.map((provider) => createProviderOption(provider)),
[endpointsConfig, allowedProviders],
);
useEffect(() => {
if (endpointsConfig == null || !modelsReady || !modelsQuery.isSuccess) {
return;
}
const storedProvider = localStorage.getItem(LocalStorageKeys.LAST_AGENT_PROVIDER) ?? '';
const storedModel = localStorage.getItem(LocalStorageKeys.LAST_AGENT_MODEL) ?? '';
const storedSelection = getAvailableAgentSelection({
provider: storedProvider,
model: storedModel,
providers,
models,
});
if (storedSelection.provider !== storedProvider) {
localStorage.removeItem(LocalStorageKeys.LAST_AGENT_PROVIDER);
localStorage.removeItem(LocalStorageKeys.LAST_AGENT_MODEL);
} else if (storedSelection.model !== storedModel) {
localStorage.removeItem(LocalStorageKeys.LAST_AGENT_MODEL);
}
if (current_agent_id || dirtyFields.provider === true || dirtyFields.model === true) {
return;
}
const selectedProviderOption = getValues('provider');
const selectedProvider =
(typeof selectedProviderOption === 'string'
? selectedProviderOption
: (selectedProviderOption as StringOption | undefined)?.value) ?? '';
const selectedModel = getValues('model') ?? '';
if (storedSelection.provider !== selectedProvider) {
setValue('provider', createProviderOption(storedSelection.provider));
}
if (storedSelection.model !== selectedModel) {
setValue('model', storedSelection.model);
}
}, [
current_agent_id,
dirtyFields.model,
dirtyFields.provider,
endpointsConfig,
getValues,
models,
modelsQuery.isSuccess,
modelsReady,
providers,
setValue,
]);
/* Mutations */
const update = useUpdateAgentMutation({
@ -633,7 +690,12 @@ export default function AgentPanel() {
</div>
)}
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.model && (
<ModelPanel models={models} providers={providers} setActivePanel={setActivePanel} />
<ModelPanel
models={models}
providers={providers}
modelsReady={modelsReady}
setActivePanel={setActivePanel}
/>
)}
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.builder && (
<AgentConfig />

View file

@ -0,0 +1,167 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { fireEvent, render } from '@testing-library/react';
import type { AgentForm } from '~/common';
import ModelPanel from './ModelPanel';
jest.mock('@librechat/client', () => ({
Button: ({ children, onClick, type }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button type={type} onClick={onClick}>
{children}
</button>
),
ControlCombobox: ({
ariaLabel,
disabled,
items,
selectedValue,
setValue,
}: {
ariaLabel: string;
disabled?: boolean;
items: Array<{ label: string; value: string }>;
selectedValue: string;
setValue: (value: string) => void;
}) => (
<div>
<span data-testid={`${ariaLabel}-selected`}>{selectedValue}</span>
{items.map((item) => (
<button
key={item.value}
type="button"
disabled={disabled}
data-testid={`${ariaLabel}-${item.value}`}
onClick={() => setValue(item.value)}
>
{item.label}
</button>
))}
</div>
),
}));
jest.mock('~/components/SidePanel/Parameters/components', () => ({
componentMapping: {},
}));
jest.mock('~/data-provider', () => ({
useGetEndpointsQuery: () => ({ data: {} }),
}));
jest.mock('~/Providers', () => ({
useLiveAnnouncer: () => ({ announcePolite: jest.fn() }),
}));
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string) => key,
}));
jest.mock('~/utils', () => ({
cn: (...classes: Array<string | false | undefined>) => classes.filter(Boolean).join(' '),
}));
function TestForm({
defaultModel = '',
defaultProvider = '',
models,
modelsReady,
providers = [{ label: 'Custom', value: 'custom' }],
}: {
defaultModel?: string;
defaultProvider?: string;
models: Record<string, string[]>;
modelsReady: boolean;
providers?: Array<{ label: string; value: string }>;
}) {
const methods = useForm<AgentForm>({
defaultValues: {
provider: defaultProvider,
model: defaultModel,
model_parameters: {},
},
});
return (
<FormProvider {...methods}>
<ModelPanel
providers={providers}
models={models}
modelsReady={modelsReady}
setActivePanel={jest.fn()}
/>
</FormProvider>
);
}
describe('ModelPanel', () => {
beforeEach(() => localStorage.clear());
it('disables model selection until the model catalogue is ready', () => {
const { getByTestId } = render(
<TestForm
defaultProvider="custom"
models={{ custom: ['custom-model'] }}
modelsReady={false}
/>,
);
expect(getByTestId('com_ui_provider-custom')).toBeDisabled();
expect(getByTestId('com_ui_model-custom-model')).toBeDisabled();
});
it('selects and saves the first model when the provider changes', () => {
const providers = [
{ label: 'Original', value: 'original' },
{ label: 'Alternate', value: 'alternate' },
];
const { getByTestId } = render(
<TestForm
defaultProvider="original"
defaultModel="original-model"
models={{ original: ['original-model'], alternate: ['alternate-model'] }}
modelsReady={true}
providers={providers}
/>,
);
fireEvent.click(getByTestId('com_ui_provider-alternate'));
expect(getByTestId('com_ui_model-selected')).toHaveTextContent('alternate-model');
expect(localStorage.getItem('lastAgentProvider')).toBe('alternate');
expect(localStorage.getItem('lastAgentModel')).toBe('alternate-model');
});
it('preserves the model when the current provider is selected again', () => {
const { getByTestId } = render(
<TestForm
defaultProvider="custom"
defaultModel="second-model"
models={{ custom: ['first-model', 'second-model'] }}
modelsReady={true}
/>,
);
fireEvent.click(getByTestId('com_ui_provider-custom'));
expect(getByTestId('com_ui_model-selected')).toHaveTextContent('second-model');
});
it('saves an explicitly selected model', () => {
const { getByTestId } = render(
<TestForm
defaultProvider="custom"
defaultModel="first-model"
models={{ custom: ['first-model', 'second-model'] }}
modelsReady={true}
/>,
);
fireEvent.click(getByTestId('com_ui_model-second-model'));
expect(localStorage.getItem('lastAgentProvider')).toBe('custom');
expect(localStorage.getItem('lastAgentModel')).toBe('second-model');
});
});

View file

@ -1,4 +1,4 @@
import React, { useMemo, useEffect } from 'react';
import React, { useMemo } from 'react';
import keyBy from 'lodash/keyBy';
import { ChevronLeft, RotateCcw } from 'lucide-react';
import { Button, ControlCombobox } from '@librechat/client';
@ -25,7 +25,8 @@ export default function ModelPanel({
providers,
setActivePanel,
models: modelsData,
}: Pick<AgentModelPanelProps, 'models' | 'providers' | 'setActivePanel'>) {
modelsReady,
}: Pick<AgentModelPanelProps, 'models' | 'modelsReady' | 'providers' | 'setActivePanel'>) {
const localize = useLocalize();
const { announcePolite } = useLiveAnnouncer();
@ -47,23 +48,6 @@ export default function ModelPanel({
[modelsData, provider],
);
useEffect(() => {
const _model = model ?? '';
if (provider && _model) {
const modelExists = models.includes(_model);
if (!modelExists) {
const newModels = modelsData[provider] ?? [];
setValue('model', newModels[0] ?? '');
}
localStorage.setItem(LocalStorageKeys.LAST_AGENT_MODEL, _model);
localStorage.setItem(LocalStorageKeys.LAST_AGENT_PROVIDER, provider);
}
if (provider && !_model) {
setValue('model', models[0] ?? '');
}
}, [provider, models, modelsData, setValue, model]);
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
const bedrockRegions = useMemo(() => {
@ -150,13 +134,27 @@ export default function ModelPanel({
displayValue={alternateName[display] ?? display}
selectPlaceholder={localize('com_ui_select_provider')}
searchPlaceholder={localize('com_ui_select_search_provider')}
setValue={field.onChange}
setValue={(value) => {
if (value === provider) {
return;
}
const nextModel = modelsData[value]?.[0] ?? '';
field.onChange(value);
setValue('model', nextModel);
localStorage.setItem(LocalStorageKeys.LAST_AGENT_PROVIDER, value);
if (nextModel) {
localStorage.setItem(LocalStorageKeys.LAST_AGENT_MODEL, nextModel);
} else {
localStorage.removeItem(LocalStorageKeys.LAST_AGENT_MODEL);
}
}}
items={providers.map((provider) => ({
label: typeof provider === 'string' ? provider : provider.label,
value: typeof provider === 'string' ? provider : provider.value,
}))}
className={cn(error ? 'border-2 border-red-500' : '')}
ariaLabel={localize('com_ui_provider')}
disabled={!modelsReady}
isCollapsed={false}
showCarat={true}
/>
@ -197,12 +195,20 @@ export default function ModelPanel({
: localize('com_ui_select_provider_first')
}
searchPlaceholder={localize('com_ui_select_model')}
setValue={field.onChange}
setValue={(value) => {
field.onChange(value);
localStorage.setItem(LocalStorageKeys.LAST_AGENT_PROVIDER, provider);
if (value) {
localStorage.setItem(LocalStorageKeys.LAST_AGENT_MODEL, value);
} else {
localStorage.removeItem(LocalStorageKeys.LAST_AGENT_MODEL);
}
}}
items={models.map((model) => ({
label: model,
value: model,
}))}
disabled={!provider}
disabled={!provider || !modelsReady}
className={cn('disabled:opacity-50', error ? 'border-2 border-red-500' : '')}
ariaLabel={localize('com_ui_model')}
isCollapsed={false}

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { Spinner, useToastContext, SelectDropDown } from '@librechat/client';
import { useForm, FormProvider, Controller, useWatch } from 'react-hook-form';
@ -7,19 +7,26 @@ import {
Capabilities,
isActionTool,
ImageVisionTool,
LocalStorageKeys,
defaultAssistantFormValues,
} from 'librechat-data-provider';
import type { FunctionTool, TConfig } from 'librechat-data-provider';
import type { AssistantForm, AssistantPanelProps } from '~/common';
import type { AssistantForm, AssistantPanelProps, LastSelectedModels } from '~/common';
import {
useCreateAssistantMutation,
useUpdateAssistantMutation,
useAvailableAgentToolsQuery,
} from '~/data-provider';
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
import {
cn,
cardStyle,
defaultTextProps,
getAvailableModelSelection,
removeFocusOutlines,
} from '~/utils';
import AssistantConversationStarters from './AssistantConversationStarters';
import AssistantToolsDialog from '~/components/Tools/AssistantToolsDialog';
import { useSelectAssistant, useLocalize } from '~/hooks';
import { useSelectAssistant, useLocalize, useLocalStorage } from '~/hooks';
import { useAssistantsMapContext } from '~/Providers';
import AppendDateCheckbox from './AppendDateCheckbox';
import CapabilitiesForm from './CapabilitiesForm';
@ -50,7 +57,13 @@ export default function AssistantPanel({
assistantsConfig,
version,
}: AssistantPanelProps & { assistantsConfig?: TConfig | null }) {
const modelsQuery = useGetModelsQuery();
const modelsQuery = useGetModelsQuery({ refetchOnMount: 'always' });
const models = useMemo(() => modelsQuery.data?.[endpoint] ?? [], [endpoint, modelsQuery.data]);
const modelsReady = modelsQuery.isFetchedAfterMount && !modelsQuery.isFetching;
const [lastSelectedModels] = useLocalStorage<LastSelectedModels | undefined>(
LocalStorageKeys.LAST_MODEL,
{} as LastSelectedModels,
);
const assistantMap = useAssistantsMapContext();
const { data: allTools = [] } = useAvailableAgentToolsQuery();
@ -64,10 +77,41 @@ export default function AssistantPanel({
const [showToolDialog, setShowToolDialog] = useState(false);
const { control, handleSubmit, reset, setValue, getValues } = methods;
const {
control,
handleSubmit,
reset,
setValue,
getValues,
formState: { dirtyFields },
} = methods;
const assistant = useWatch({ control, name: 'assistant' });
const functions = useWatch({ control, name: 'functions' });
const assistant_id = useWatch({ control, name: 'id' });
const model = useWatch({ control, name: 'model' });
useEffect(() => {
if (!modelsReady || !modelsQuery.isSuccess || current_assistant_id || assistant_id) {
return;
}
const candidate = dirtyFields.model === true ? model : (lastSelectedModels?.[endpoint] ?? '');
const nextModel = getAvailableModelSelection(candidate, models);
if (nextModel !== model) {
setValue('model', nextModel, { shouldDirty: false });
}
}, [
assistant_id,
current_assistant_id,
dirtyFields.model,
endpoint,
lastSelectedModels,
model,
models,
modelsQuery.isSuccess,
modelsReady,
setValue,
]);
const activeModel = useMemo(() => {
return assistantMap?.[endpoint]?.[assistant_id]?.model;
@ -363,7 +407,8 @@ export default function AssistantPanel({
emptyTitle={true}
value={field.value}
setValue={field.onChange}
availableValues={modelsQuery.data?.[endpoint] ?? []}
availableValues={models}
disabled={!modelsReady}
showAbove={false}
showLabel={false}
className={cn(

View file

@ -6,7 +6,6 @@ import {
FileSources,
Capabilities,
EModelEndpoint,
LocalStorageKeys,
isImageVisionTool,
defaultAssistantFormValues,
} from 'librechat-data-provider';
@ -19,17 +18,11 @@ import type {
} from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type { UseFormReset } from 'react-hook-form';
import type {
Actions,
ExtendedFile,
AssistantForm,
TAssistantOption,
LastSelectedModels,
} from '~/common';
import type { Actions, ExtendedFile, AssistantForm, TAssistantOption } from '~/common';
import { useListAssistantsQuery } from '~/data-provider';
import { useLocalize, useLocalStorage } from '~/hooks';
import { cn, createDropdownSetter } from '~/utils';
import { useFileMapContext } from '~/Providers';
import { useLocalize } from '~/hooks';
const keys = new Set([
'name',
@ -63,10 +56,6 @@ export default function AssistantSelect({
const localize = useLocalize();
const fileMap = useFileMapContext();
const lastSelectedAssistant = useRef<string | null>(null);
const [lastSelectedModels] = useLocalStorage<LastSelectedModels | undefined>(
LocalStorageKeys.LAST_MODEL,
{} as LastSelectedModels,
);
const toolkits = useMemo(
() => new Set(allTools?.filter((tool) => tool.toolkit === true).map((tool) => tool.pluginKey)),
@ -152,10 +141,7 @@ export default function AssistantSelect({
createMutation.reset();
if (!assistant) {
setCurrentAssistantId(undefined);
return reset({
...defaultAssistantFormValues,
model: lastSelectedModels?.[endpoint] ?? '',
});
return reset(defaultAssistantFormValues);
}
const update = {
@ -231,15 +217,7 @@ export default function AssistantSelect({
reset(formValues);
setCurrentAssistantId(assistant.id);
},
[
query.data,
reset,
setCurrentAssistantId,
createMutation,
endpoint,
lastSelectedModels,
toolkits,
],
[query.data, reset, setCurrentAssistantId, createMutation, toolkits],
);
useEffect(() => {

View file

@ -0,0 +1,62 @@
import { getAvailableAgentSelection, getAvailableModelSelection } from './agentModelSelection';
describe('getAvailableModelSelection', () => {
it('returns an empty value when a saved model is unavailable', () => {
expect(getAvailableModelSelection('gpt-removed', ['gpt-4.1'])).toBe('');
});
});
describe('getAvailableAgentSelection', () => {
const providers = [
{ label: 'Anthropic', value: 'anthropic' },
{ label: 'Bedrock', value: 'bedrock' },
];
const models = {
anthropic: ['claude-sonnet-4'],
bedrock: ['claude-sonnet-4', 'claude-haiku-3'],
};
it('keeps an available provider and model', () => {
expect(
getAvailableAgentSelection({
provider: 'bedrock',
model: 'claude-sonnet-4',
providers,
models,
}),
).toEqual({ provider: 'bedrock', model: 'claude-sonnet-4' });
});
it('returns an empty selection when the provider is unavailable', () => {
expect(
getAvailableAgentSelection({
provider: 'openAI',
model: 'gpt-5',
providers,
models,
}),
).toEqual({ provider: '', model: '' });
});
it('returns an empty selection when the provider has no model catalogue', () => {
expect(
getAvailableAgentSelection({
provider: 'anthropic',
model: 'claude-sonnet-4',
providers,
models: { bedrock: models.bedrock },
}),
).toEqual({ provider: '', model: '' });
});
it('keeps the provider but clears an unavailable model', () => {
expect(
getAvailableAgentSelection({
provider: 'bedrock',
model: 'claude-opus-3',
providers,
models,
}),
).toEqual({ provider: 'bedrock', model: '' });
});
});

View file

@ -0,0 +1,32 @@
type ProviderOption = string | { value?: string | number | null };
export function getAvailableModelSelection(model: string, models: readonly string[]): string {
return models.includes(model) ? model : '';
}
export function getAvailableAgentSelection({
provider,
model,
providers,
models,
}: {
provider: string;
model: string;
providers: readonly ProviderOption[];
models: Record<string, string[] | undefined>;
}): { provider: string; model: string } {
const providerExists =
models[provider] != null &&
providers.some((option) =>
typeof option === 'string' ? option === provider : option.value === provider,
);
if (!providerExists) {
return { provider: '', model: '' };
}
return {
provider,
model: getAvailableModelSelection(model, models[provider] ?? []),
};
}

View file

@ -1,10 +1,6 @@
import { getDefaultAgentFormValues } from './forms';
describe('getDefaultAgentFormValues', () => {
beforeEach(() => {
localStorage.clear();
});
it('uses the scalable user workspace by default', () => {
expect(getDefaultAgentFormValues().stateful_code_environment).toBe('user');
});

View file

@ -4,7 +4,6 @@ import {
alternateName,
EModelEndpoint,
EToolResources,
LocalStorageKeys,
defaultAgentFormValues,
} from 'librechat-data-provider';
import type { Agent, TFile, StatefulCodeEnvironment } from 'librechat-data-provider';
@ -44,17 +43,12 @@ export const createProviderOption = (provider: string) => ({
value: provider,
});
/**
* Gets default agent form values with localStorage values for model and provider.
* This is used to initialize agent forms with the last used model and provider.
**/
/** Gets default agent form values. */
export const getDefaultAgentFormValues = (
statefulCodeEnvironment: StatefulCodeEnvironment = 'user',
) => ({
...defaultAgentFormValues,
stateful_code_environment: statefulCodeEnvironment,
model: localStorage.getItem(LocalStorageKeys.LAST_AGENT_MODEL) ?? '',
provider: createProviderOption(localStorage.getItem(LocalStorageKeys.LAST_AGENT_PROVIDER) ?? ''),
avatar_file: null,
avatar_preview: '',
avatar_action: null,

View file

@ -44,6 +44,7 @@ export * from './favoritesError';
export * from './approval';
export * from './steer';
export * from './activityLabels';
export * from './agentModelSelection';
export * from './runStepDuration';
export * from './toolCallPhase';
export * from './documentTitle';