Give the model parameters the same treatment a node's own panel gives them

The picker for a simulator or a judge and a node's parameter panel are the
same dialog component, and the dialog has always known how to offer "Use
default" on an optional field - showUseDefault, the "Using the default" hint,
the reset. It reads one flag, defaultsWhenEmpty, which the node panels set on
every optional field and this hand-written list never set at all. So a
temperature typed here by mistake had no way back to unset: clearing the box
by hand looks the same as never having decided.

Set it on all five, and brought the rest of each field in line with what the
schema-driven panel produces for the same object: an arrow step a decimal can
actually move by, an integer step on the integers, and the tips ModelParameters
itself declares, so the same explanation appears in both places.

Temperature's max was 2 here and is 1.0 on the server, which has a comment
explaining why - the dialog was offering a value the run would be rejected
for.

Still hand-written rather than derived from the published schema: this dialog
picks a model for a run, not a node's configuration, and reaching for a block
type's schema to render five known fields would buy a network call and a way
to fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-08 15:15:10 +02:00
parent 984af08f36
commit 074c8fb763
2 changed files with 59 additions and 10 deletions

View File

@ -46,6 +46,25 @@ describe('openLLMDescriptorSettings', () => {
expect(chosen).toEqual({ provider: 'InternalOllama', model: 'llama3', parameters: { seed: 7 } });
});
it('offers the same treatment a node parameter panel gives these fields', async () => {
const { service, open } = dialog(null);
await openLLMDescriptorSettings(service, retriever(), { title: 'Simulation Settings' });
const fields: Array<Record<string, unknown>> = open.mock.calls[0][0].fields;
const parameters = fields.filter((field) => field['group'] === 'Model parameters');
expect(parameters.map((field) => field['key']))
.toEqual(['temperature', 'topP', 'topK', 'maxTokens', 'seed']);
// Every one of them is optional, so every one of them can be put back to unset - which a
// filled box cannot express on its own, and which the node panels have always offered.
expect(parameters.every((field) => field['defaultsWhenEmpty'] === true)).toBe(true);
// The bound the server actually enforces on temperature, not the 2 this dialog used to allow.
expect(parameters.find((field) => field['key'] === 'temperature')).toMatchObject({ min: 0, max: 1 });
// A decimal's arrows have somewhere sensible to go instead of jumping between the two ends.
expect(parameters.find((field) => field['key'] === 'topP')?.['stepIncrement']).toBe(0.1);
expect(parameters.find((field) => field['key'] === 'topK')?.['step']).toBe(1);
});
it('shows an inherited parameter outside the collapsed section, where it can be seen', async () => {
const { service, open } = dialog(null);

View File

@ -14,18 +14,48 @@ const PARAMETER_GROUP = 'Model parameters';
* The optional sampling knobs, behind a section that starts closed. Provider and model are what
* anyone opening this dialog came for; these are for the runs where you already know you want
* them, and shown flat they made the common case look like a five-field form.
*
* <p>These are the same five fields a node's own parameter panel shows, and they are declared to
* behave the same way: `defaultsWhenEmpty` so an empty box reads as "the provider decides" and
* there is a way back to it after typing, the bounds the server actually enforces, and an arrow
* step a decimal can move by. Hand-written rather than derived from the published schema the way
* `buildSchemaObjectDialog` does it: this dialog is picking a model for a run, not editing a node's
* configuration, and reaching for a block type's schema to render five known fields would buy a
* network call and a failure mode. The labels, tips and bounds are ModelParameters' own - keep them
* in step with it.
*/
const PARAMETER_FIELDS: NodeSettingField[] = [
{ key: 'temperature', label: 'Temperature', type: 'number', min: 0, max: 2, group: PARAMETER_GROUP,
placeholder: 'Leave empty for the default', tip: '0 makes the run as repeatable as the model allows' },
{ key: 'topP', label: 'Top P', type: 'number', min: 0, max: 1, group: PARAMETER_GROUP,
placeholder: 'Leave empty for the default' },
{ key: 'topK', label: 'Top K', type: 'number', min: 1, group: PARAMETER_GROUP,
placeholder: 'Leave empty for the default' },
{ key: 'maxTokens', label: 'Max tokens', type: 'number', min: 1, group: PARAMETER_GROUP,
placeholder: 'Leave empty for the default' },
{ key: 'seed', label: 'Seed', type: 'number', group: PARAMETER_GROUP,
placeholder: 'Leave empty for the default', tip: 'Fixes the randomness, so two runs can be compared' }
{
key: 'temperature', label: 'Temperature', type: 'number', group: PARAMETER_GROUP,
// The server caps this at 1.0; offering 2 here only produced a value it would reject.
min: 0, max: 1, stepIncrement: 0.1,
defaultsWhenEmpty: true, placeholder: 'Leave empty for the default',
tip: 'Higher values make the output more varied. 0 makes it as repeatable as the model allows.'
},
{
key: 'topP', label: 'Top P', type: 'number', group: PARAMETER_GROUP,
min: 0, max: 1, stepIncrement: 0.1,
defaultsWhenEmpty: true, placeholder: 'Leave empty for the default',
tip: 'Nucleus sampling: consider only the most likely tokens adding up to this probability.'
},
{
key: 'topK', label: 'Top K', type: 'number', group: PARAMETER_GROUP,
min: 1, step: 1, stepIncrement: 1,
defaultsWhenEmpty: true, placeholder: 'Leave empty for the default',
tip: 'Consider only this many candidate tokens at each step.'
},
{
key: 'maxTokens', label: 'Max tokens', type: 'number', group: PARAMETER_GROUP,
min: 1, step: 1, stepIncrement: 1,
defaultsWhenEmpty: true, placeholder: 'Leave empty for the default',
tip: 'Upper bound on the length of the generated answer.'
},
{
key: 'seed', label: 'Seed', type: 'number', group: PARAMETER_GROUP,
step: 1, stepIncrement: 1,
defaultsWhenEmpty: true, placeholder: 'Leave empty for the default',
tip: 'Fixes the randomness, so the same inputs give the same answer. Needed to tell a real change from model noise.'
}
];
export type LLMDescriptorSettingsRequest = {