fix(web): restore the default model catalog without a reload
Restore defaults dropped the user's `models` override from the draft but the rows kept showing it, so the catalog only looked restored after closing and reopening the card. The inherited rows were read from the namespace's effective value, which still carries the stored override until the unset is applied — so dropping the override echoed it straight back. They now come from the layer beneath the user's: what the composition entry pinned, or else the schema default that resolution would supply.
This commit is contained in:
@@ -239,6 +239,18 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
|
||||
|
||||
const keyLocked = keyState?.writable === false
|
||||
|
||||
/**
|
||||
* The catalog beneath the user layer: what the composition entry pinned, or
|
||||
* else the schema default that `resolve` would supply. The effective value
|
||||
* cannot answer this — it still carries the stored override until the unset
|
||||
* is applied, so reading it would echo that override straight back the
|
||||
* moment reset drops it, leaving the rows unchanged until a reload.
|
||||
*/
|
||||
const inheritedModels = (): unknown => {
|
||||
const pinned = getPath(namespace.base, [...settingsPath, 'models'])
|
||||
return pinned ?? nodeAtPath(root, [...settingsPath, 'models'])?.meta.default
|
||||
}
|
||||
|
||||
/**
|
||||
* The curated fields of one known adapter family. Taking the narrowed
|
||||
* family as a parameter is what makes `EFFORT_FIELD` total here: an
|
||||
@@ -248,7 +260,7 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
|
||||
const effortField = EFFORT_FIELD[family]
|
||||
const customModels = getPath(draft, ['models'])
|
||||
const modelsOverridden = hasPath(draft, ['models'])
|
||||
const models = modelDrafts(modelsOverridden ? customModels : getPath(fallback, ['models']))
|
||||
const models = modelDrafts(modelsOverridden ? customModels : inheritedModels())
|
||||
const defaultContextWindow = getPath(fallback, ['defaultContextWindow'])
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -41,7 +41,22 @@ const DeepSeekConfig = Schema.object({
|
||||
name: Schema.string(),
|
||||
description: Schema.string(),
|
||||
contextWindow: Schema.number().step(1).min(1),
|
||||
})),
|
||||
// The adapter declares its catalog as a schema default rather than a
|
||||
// composition entry, which is what the restore-defaults path has to read.
|
||||
})).default([
|
||||
{
|
||||
id: 'deepseek-v4-flash',
|
||||
name: 'DeepSeek-V4-Flash',
|
||||
description: '',
|
||||
contextWindow: 1_000_000,
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v4-pro',
|
||||
name: 'DeepSeek-V4-Pro',
|
||||
description: '',
|
||||
contextWindow: 1_000_000,
|
||||
},
|
||||
]),
|
||||
})
|
||||
|
||||
const DEFAULT_DEEPSEEK_MODELS = [
|
||||
@@ -420,6 +435,48 @@ describe('ModelsSection', () => {
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['the schema default', undefined],
|
||||
['the composition entry', { models: [{ id: 'pinned-by-deployment' }] }],
|
||||
])('restores %s the moment the override is dropped, not after a reload', async (_label, base) => {
|
||||
// The regression: reset read the EFFECTIVE value, which still carries the
|
||||
// stored override until the unset is applied — so the rows did not change
|
||||
// and the catalog only looked restored after reopening the card.
|
||||
const { face } = scriptedFace()
|
||||
const stored = { models: [{ id: 'user-only-model', name: 'User Only' }] }
|
||||
const overridden: SettingsNamespaceView = {
|
||||
ns: 'llm-deepseek',
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
||||
value: { ...stored, defaultContextWindow: 1_000_000 },
|
||||
...base === undefined ? {} : { base },
|
||||
user: stored,
|
||||
applies: 'live',
|
||||
secrets: [],
|
||||
revision: 0,
|
||||
}
|
||||
const { ProviderEditor } = await import('../src/client/ProviderEditor.tsx')
|
||||
render(<ProviderEditor
|
||||
provider="deepseek-official"
|
||||
displayName="DeepSeek"
|
||||
namespace={overridden}
|
||||
settingsPath={[]}
|
||||
api={face as never}
|
||||
t={t}
|
||||
readOnly={false}
|
||||
onClose={() => {}}
|
||||
/>)
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
expect(screen.getByText(en.modelsCustomized)).toBeTruthy()
|
||||
expect(screen.getAllByLabelText(new RegExp(en.modelId)).map(input => (input as HTMLInputElement).value))
|
||||
.toEqual(['user-only-model'])
|
||||
|
||||
fireEvent.click(screen.getByText(en.resetModels))
|
||||
|
||||
expect(screen.getByText(en.modelsInherited)).toBeTruthy()
|
||||
expect(screen.getAllByLabelText(new RegExp(en.modelId)).map(input => (input as HTMLInputElement).value))
|
||||
.toEqual(base === undefined ? ['deepseek-v4-flash', 'deepseek-v4-pro'] : ['pinned-by-deployment'])
|
||||
})
|
||||
|
||||
it('renders malformed draft fallbacks without inventing catalog values', () => {
|
||||
render(<DeepSeekModelsEditor
|
||||
models={[{}]}
|
||||
|
||||
Reference in New Issue
Block a user