From 37f8c0cb6f1dc748ffc94e9b3b121e5db1da2898 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 11 Aug 2026 15:24:13 -0700 Subject: [PATCH 1/5] fix(studio): scope anonymizer sampling params to LLM roles [ASTD-417] The builder exposed temperature, max tokens, and top P for every Anonymizer role and sent max_tokens to all of them, including the GLiNER entity detector. GLiNER is a token-classification NIM driven by labels and a threshold, and the library's default models.yaml gives it only max_parallel_requests and timeout. Hide the params dropdown for the detector role and strip sampling keys from its request params. For the LLM roles, seed the defaults the library uses for gpt-oss-120b (temperature 0.3, top_p 0.95, max_tokens 16384) and raise the max-tokens slider ceiling so the dropdown reflects what is actually sent. Signed-off-by: mschwab --- .../ModelSelectV2/InferenceParameters.tsx | 20 ++++++--- .../ModelSelectV2/ParamsDropdown.tsx | 8 +++- .../components/ModelSettingsSection.test.tsx | 43 +++++++++++++++++++ .../components/ModelSettingsSection.tsx | 27 ++++++++---- .../AnonymizerBuilderRoute/constants.ts | 16 +++++++ .../AnonymizerBuilderRoute/schema.test.ts | 43 ++++++++++++++----- .../routes/AnonymizerBuilderRoute/schema.ts | 34 ++++++++++++--- 7 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx diff --git a/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx b/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx index cc696610daf..2a63c253df3 100644 --- a/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx +++ b/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx @@ -2,29 +2,39 @@ // SPDX-License-Identifier: Apache-2.0 import { SliderWithTextInput } from '@nemo/common/src/components/SliderWithTextInput'; +import type { HyperparameterFieldMetadata } from '@nemo/common/src/components/TrainingParameterSlider/types'; import { INFERENCE_HYPERPARAMETER_FIELD_METADATA } from '@nemo/common/src/constants/inferenceParameters'; import type { InferenceParams } from '@nemo/sdk/generated/platform/schema'; import { Stack } from '@nvidia/foundations-react-core'; import { type FC } from 'react'; +const FIELDS = ['temperature', 'max_tokens', 'top_p'] as const satisfies Array< + keyof typeof INFERENCE_HYPERPARAMETER_FIELD_METADATA +>; + +export type InferenceParameterField = (typeof FIELDS)[number]; + +type FieldMetadata = HyperparameterFieldMetadata< + Record +>[InferenceParameterField]; + export interface InferenceParametersProps { value: Partial; onChange: (value: Partial) => void; disabled?: boolean; + /** Per-field overrides of the shared bounds and defaults, for callers whose backend differs. */ + fieldMetadata?: Partial>>; } -const FIELDS = ['temperature', 'max_tokens', 'top_p'] as const satisfies Array< - keyof typeof INFERENCE_HYPERPARAMETER_FIELD_METADATA ->; - export const InferenceParameters: FC = ({ value, onChange, disabled, + fieldMetadata, }) => ( {FIELDS.map((key) => { - const param = INFERENCE_HYPERPARAMETER_FIELD_METADATA[key]; + const param = { ...INFERENCE_HYPERPARAMETER_FIELD_METADATA[key], ...fieldMetadata?.[key] }; return ( void; inferenceParams?: Partial; onInferenceParamsChange?: (params: Partial) => void; + fieldMetadata?: InferenceParametersProps['fieldMetadata']; } export const ParamsDropdown: FC = ({ @@ -29,6 +33,7 @@ export const ParamsDropdown: FC = ({ onOpenChange, inferenceParams = {}, onInferenceParamsChange, + fieldMetadata, }) => ( @@ -60,6 +65,7 @@ export const ParamsDropdown: FC = ({ value={inferenceParams} onChange={onInferenceParamsChange ?? (() => {})} disabled={disabled} + fieldMetadata={fieldMetadata} /> diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx new file mode 100644 index 00000000000..2c6ff8864a3 --- /dev/null +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ModelSettingsSection } from '@studio/routes/AnonymizerBuilderRoute/components/ModelSettingsSection'; +import { + activeRolesForStrategy, + STRATEGY_SUBSTITUTE, +} from '@studio/routes/AnonymizerBuilderRoute/constants'; +import { + AnonymizerFormData, + getAnonymizerFormDefaults, +} from '@studio/routes/AnonymizerBuilderRoute/schema'; +import { render, screen } from '@testing-library/react'; +import { FC, ReactNode } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +vi.mock('@studio/routes/AnonymizerBuilderRoute/useAnonymizerModels', () => ({ + useAnonymizerModels: () => ({ + models: [], + items: [{ label: 'gpt-oss-120b', value: 'gpt-oss-120b' }], + isLoading: false, + applyModel: vi.fn(), + }), +})); + +const TestWrapper: FC<{ children: ReactNode }> = ({ children }) => { + const methods = useForm({ defaultValues: getAnonymizerFormDefaults() }); + return {children}; +}; + +describe('ModelSettingsSection', () => { + it('offers sampling params for every role except the GLiNER detector', async () => { + render( + + + + ); + + const triggers = await screen.findAllByTestId('params-dropdown-trigger'); + expect(triggers).toHaveLength(activeRolesForStrategy(STRATEGY_SUBSTITUTE).length - 1); + expect(screen.getByText(/GLiNER detection takes entity labels/)).toBeInTheDocument(); + }); +}); diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx index ed8f1121cea..663c00f5484 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx @@ -7,8 +7,10 @@ import type { InferenceParams } from '@nemo/sdk/generated/platform/schema'; import { Divider, Flex, Stack, Text } from '@nvidia/foundations-react-core'; import { activeRolesForStrategy, + ANONYMIZER_PARAM_METADATA, GLINER_ROLE, ROLE_LABELS, + supportsSamplingParams, } from '@studio/routes/AnonymizerBuilderRoute/constants'; import type { AnonymizerFormData } from '@studio/routes/AnonymizerBuilderRoute/schema'; import { useAnonymizerModels } from '@studio/routes/AnonymizerBuilderRoute/useAnonymizerModels'; @@ -63,15 +65,24 @@ export const ModelSettingsSection: FC = () => { }} /> - setOpenParamsRole(next ? role : null)} - inferenceParams={roleModelsValue?.[role]?.params as Partial} - onInferenceParamsChange={(params) => - setValue(`roleModels.${role}.params`, params as Record) - } - /> + {supportsSamplingParams(role) && ( + setOpenParamsRole(next ? role : null)} + inferenceParams={roleModelsValue?.[role]?.params as Partial} + onInferenceParamsChange={(params) => + setValue(`roleModels.${role}.params`, params as Record) + } + fieldMetadata={ANONYMIZER_PARAM_METADATA} + /> + )} + {!supportsSamplingParams(role) && ( + + GLiNER detection takes entity labels and a confidence threshold, so temperature, max + tokens, and top P do not apply. + + )} ))} diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts b/web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts index 3135bb0602c..397557beb04 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts @@ -240,6 +240,17 @@ export const MAX_COLUMN_INTROSPECTION_BYTES = 50 * 1024 * 1024; export const DEFAULT_MODEL_TIMEOUT_SECONDS = 500; export const DEFAULT_MODEL_MAX_TOKENS = 16384; +export const MAX_MODEL_MAX_TOKENS = 32768; + +/** Mirrors the LLM entries in the Anonymizer library's default `models.yaml`. */ +export const DEFAULT_MODEL_TEMPERATURE = 0.3; +export const DEFAULT_MODEL_TOP_P = 0.95; + +export const ANONYMIZER_PARAM_METADATA = { + temperature: { default: DEFAULT_MODEL_TEMPERATURE }, + top_p: { default: DEFAULT_MODEL_TOP_P }, + max_tokens: { default: DEFAULT_MODEL_MAX_TOKENS, max: MAX_MODEL_MAX_TOKENS }, +} as const; export const DETECTION_ROLES = [ 'entity_detector', @@ -275,6 +286,11 @@ export const ROLE_LABELS: Record = { export const GLINER_ROLE = 'entity_detector'; +export const SAMPLING_PARAM_KEYS = ['temperature', 'top_p', 'max_tokens'] as const; + +/** GLiNER is a token-classification NIM: it takes labels and a threshold, not sampling params. */ +export const supportsSamplingParams = (role: string): boolean => role !== GLINER_ROLE; + export const activeRolesForStrategy = (strategy: Strategy): string[] => { // rewrite reuses the replacement generator, so the backend validates that role too if (strategy === STRATEGY_REWRITE) return [...DETECTION_ROLES, ...REWRITE_ROLES, REPLACE_ROLE]; diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts index 959955222a3..b7f97de8697 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts @@ -3,6 +3,7 @@ import { DETECTION_ROLES, + GLINER_ROLE, MAX_PREVIEW_ROWS, REPLACE_ROLE, REWRITE_ROLES, @@ -131,18 +132,38 @@ describe('buildAnonymizerJobRequest', () => { expect(req.spec.data.data_summary).toBeUndefined(); }); - it('deduplicates identical role models into a single model_config with a default timeout', () => { + it('deduplicates identical role models into a single model_config per param set', () => { const req = buildAnonymizerJobRequest(form({ strategy: 'substitute' })); expect(req.spec.model_configs).toEqual([ { alias: 'model-1', model: 'openai/gpt-oss-120b', provider: 'default/nvidia', - inference_parameters: { timeout: 500, max_tokens: 16384 }, + inference_parameters: { timeout: 500 }, + }, + { + alias: 'model-2', + model: 'openai/gpt-oss-120b', + provider: 'default/nvidia', + inference_parameters: { timeout: 500, max_tokens: 16384, temperature: 0.3, top_p: 0.95 }, }, ]); }); + it('sends no sampling params to the GLiNER detector, even when the form holds them', () => { + const models = roleModels('openai/gpt-oss-120b', 'default/nvidia'); + models[GLINER_ROLE] = { + modelId: 'gliner', + model: 'nvidia/gliner-pii', + provider: 'default/nvidia', + params: { temperature: 0.9, top_p: 0.5, max_tokens: 2048, max_parallel_requests: 1 }, + }; + const req = buildAnonymizerJobRequest(form({ strategy: 'substitute', roleModels: models })); + const detectorAlias = req.spec.selected_models?.detection?.[GLINER_ROLE]; + const detector = req.spec.model_configs?.find((config) => config.alias === detectorAlias); + expect(detector?.inference_parameters).toEqual({ timeout: 500, max_parallel_requests: 1 }); + }); + it('emits one model_config per unique model+provider', () => { const models = roleModels('openai/gpt-oss-120b', 'default/nvidia'); models[DETECTION_ROLES[0]] = { @@ -160,17 +181,17 @@ describe('buildAnonymizerJobRequest', () => { it('maps detection + replace roles for substitute, detection + rewrite for rewrite', () => { const sub = buildAnonymizerJobRequest(form({ strategy: 'substitute' })).spec.selected_models; expect(sub?.detection?.entity_detector).toBe('model-1'); - expect(sub?.replace?.replacement_generator).toBe('model-1'); + expect(sub?.replace?.replacement_generator).toBe('model-2'); expect(sub?.rewrite).toBeUndefined(); const rew = buildAnonymizerJobRequest(form({ strategy: 'rewrite' })).spec.selected_models; expect(rew?.detection?.entity_detector).toBe('model-1'); - expect(rew?.rewrite?.rewriter).toBe('model-1'); + expect(rew?.rewrite?.rewriter).toBe('model-2'); }); it('maps the replacement generator for rewrite so the backend alias check passes', () => { const rew = buildAnonymizerJobRequest(form({ strategy: 'rewrite' })).spec.selected_models; - expect(rew?.replace?.[REPLACE_ROLE]).toBe('model-1'); + expect(rew?.replace?.[REPLACE_ROLE]).toBe('model-2'); }); it('maps only detection roles for redact/annotate/hash', () => { @@ -242,21 +263,21 @@ describe('buildAnonymizerJobRequest', () => { it('attaches inference_parameters and splits configs when params differ', () => { const models = roleModels('openai/gpt-oss-120b', 'default/nvidia'); - models[DETECTION_ROLES[0]] = { + models['entity_validator'] = { modelId: 'gpt', model: 'openai/gpt-oss-120b', provider: 'default/nvidia', params: { temperature: 0.1 }, }; const req = buildAnonymizerJobRequest(form({ strategy: 'substitute', roleModels: models })); - expect(req.spec.model_configs).toHaveLength(2); - const withTemp = req.spec.model_configs?.find( - (c) => (c.inference_parameters as { temperature?: number })?.temperature != null - ); - expect(withTemp?.inference_parameters).toEqual({ + expect(req.spec.model_configs).toHaveLength(3); + const validatorAlias = req.spec.selected_models?.detection?.['entity_validator']; + const validator = req.spec.model_configs?.find((config) => config.alias === validatorAlias); + expect(validator?.inference_parameters).toEqual({ timeout: 500, max_tokens: 16384, temperature: 0.1, + top_p: 0.95, }); }); }); diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts index 2a101e7f18c..5a5ce13212a 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts @@ -15,7 +15,9 @@ import { ANNOTATE_DEFAULT_TEMPLATE, DETECTION_ROLES, DEFAULT_MODEL_MAX_TOKENS, + DEFAULT_MODEL_TEMPERATURE, DEFAULT_MODEL_TIMEOUT_SECONDS, + DEFAULT_MODEL_TOP_P, DEFAULT_PREVIEW_ROWS, MAX_PREVIEW_ROWS, ENTITY_MODE_CUSTOM, @@ -33,11 +35,13 @@ import { REWRITE_STRATEGY, RISK_TOLERANCE_DEFAULT, RISK_TOLERANCE_ORDER, + SAMPLING_PARAM_KEYS, SOURCE_TYPE_DATASET, STRATEGY_ANNOTATE, STRATEGY_HASH, STRATEGY_REDACT, STRATEGY_SUBSTITUTE, + supportsSamplingParams, } from '@studio/routes/AnonymizerBuilderRoute/constants'; import { trimToUndefined } from '@studio/util/strings'; import { z } from 'zod'; @@ -207,6 +211,30 @@ const buildDetectConfig = ( return { entity_labels: labels }; }; +/** + * Sampling params are dropped for the GLiNER detector rather than merely hidden, so a role that + * once held them (or a strategy switch) can't leave them on a model that ignores them. + */ +const buildRoleParams = ( + role: string, + overrides: Record = {} +): Record => { + if (!supportsSamplingParams(role)) { + const kept = Object.entries(overrides).filter( + ([key]) => !SAMPLING_PARAM_KEYS.includes(key as (typeof SAMPLING_PARAM_KEYS)[number]) + ); + return { timeout: DEFAULT_MODEL_TIMEOUT_SECONDS, ...Object.fromEntries(kept) }; + } + + return { + timeout: DEFAULT_MODEL_TIMEOUT_SECONDS, + max_tokens: DEFAULT_MODEL_MAX_TOKENS, + temperature: DEFAULT_MODEL_TEMPERATURE, + top_p: DEFAULT_MODEL_TOP_P, + ...overrides, + }; +}; + export const buildAnonymizerJobRequest = ( form: AnonymizerFormData, defaultEntityLabels: string[] = [] @@ -229,11 +257,7 @@ export const buildAnonymizerJobRequest = ( const roleModel = form.roleModels[role]; const model = roleModel?.model.trim() ?? ''; const provider = roleModel?.provider.trim() ?? ''; - const params = { - timeout: DEFAULT_MODEL_TIMEOUT_SECONDS, - max_tokens: DEFAULT_MODEL_MAX_TOKENS, - ...(roleModel?.params ?? {}), - }; + const params = buildRoleParams(role, roleModel?.params); const key = `${provider}::${model}::${JSON.stringify(params)}`; let alias = aliasByModel.get(key); if (!alias) { From b57ea45d79fc79906b59c028ae9858a1611e1f74 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 11 Aug 2026 15:44:31 -0700 Subject: [PATCH 2/5] fix(studio): drop the GLiNER params explainer line The absent dropdown reads clearly enough on its own. Signed-off-by: mschwab --- .../components/ModelSettingsSection.test.tsx | 1 - .../components/ModelSettingsSection.tsx | 6 ------ 2 files changed, 7 deletions(-) diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx index 2c6ff8864a3..246422ddc81 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx @@ -38,6 +38,5 @@ describe('ModelSettingsSection', () => { const triggers = await screen.findAllByTestId('params-dropdown-trigger'); expect(triggers).toHaveLength(activeRolesForStrategy(STRATEGY_SUBSTITUTE).length - 1); - expect(screen.getByText(/GLiNER detection takes entity labels/)).toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx index 663c00f5484..42986fdedcc 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx @@ -77,12 +77,6 @@ export const ModelSettingsSection: FC = () => { /> )} - {!supportsSamplingParams(role) && ( - - GLiNER detection takes entity labels and a confidence threshold, so temperature, max - tokens, and top P do not apply. - - )} ))} From b4781833119deaff1d1965f0e703f009f6379105 Mon Sep 17 00:00:00 2001 From: mschwab Date: Tue, 11 Aug 2026 16:49:02 -0700 Subject: [PATCH 3/5] test(studio): assert which role lost the params dropdown A bare trigger count passes even if the wrong role drops its controls. Signed-off-by: mschwab --- .../components/ModelSettingsSection.test.tsx | 14 +++++++++++++- .../components/ModelSettingsSection.tsx | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx index 246422ddc81..34aefbce6dd 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx @@ -4,13 +4,14 @@ import { ModelSettingsSection } from '@studio/routes/AnonymizerBuilderRoute/components/ModelSettingsSection'; import { activeRolesForStrategy, + GLINER_ROLE, STRATEGY_SUBSTITUTE, } from '@studio/routes/AnonymizerBuilderRoute/constants'; import { AnonymizerFormData, getAnonymizerFormDefaults, } from '@studio/routes/AnonymizerBuilderRoute/schema'; -import { render, screen } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import { FC, ReactNode } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; @@ -38,5 +39,16 @@ describe('ModelSettingsSection', () => { const triggers = await screen.findAllByTestId('params-dropdown-trigger'); expect(triggers).toHaveLength(activeRolesForStrategy(STRATEGY_SUBSTITUTE).length - 1); + + expect( + within(screen.getByTestId(`role-settings-${GLINER_ROLE}`)).queryByTestId( + 'params-dropdown-trigger' + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByTestId('role-settings-entity_validator')).getByTestId( + 'params-dropdown-trigger' + ) + ).toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx index 42986fdedcc..bf288c44732 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.tsx @@ -37,7 +37,7 @@ export const ModelSettingsSection: FC = () => { return ( {roles.map((role, index) => ( - + {index > 0 && } {ROLE_LABELS[role] ?? role} From a131d857d9e2f7136a4cbf92e32c02d53ae03b9f Mon Sep 17 00:00:00 2001 From: mschwab Date: Wed, 12 Aug 2026 11:45:05 -0700 Subject: [PATCH 4/5] refactor(studio): reuse the shared inference param field list SAMPLING_PARAM_KEYS in the anonymizer constants duplicated the private FIELDS const in InferenceParameters.tsx, which already backs InferenceParameterField. Export it as INFERENCE_PARAMETER_FIELDS and drop the anonymizer copy, so the set of sampling params GLiNER cannot take lives next to the controls that render them. Signed-off-by: mschwab --- .../components/ModelSelectV2/InferenceParameters.tsx | 12 +++++++----- .../src/routes/AnonymizerBuilderRoute/constants.ts | 2 -- .../src/routes/AnonymizerBuilderRoute/schema.ts | 7 +++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx b/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx index 2a63c253df3..ecec09f5b91 100644 --- a/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx +++ b/web/packages/common/src/components/ModelSelectV2/InferenceParameters.tsx @@ -8,11 +8,13 @@ import type { InferenceParams } from '@nemo/sdk/generated/platform/schema'; import { Stack } from '@nvidia/foundations-react-core'; import { type FC } from 'react'; -const FIELDS = ['temperature', 'max_tokens', 'top_p'] as const satisfies Array< - keyof typeof INFERENCE_HYPERPARAMETER_FIELD_METADATA ->; +export const INFERENCE_PARAMETER_FIELDS = [ + 'temperature', + 'max_tokens', + 'top_p', +] as const satisfies Array; -export type InferenceParameterField = (typeof FIELDS)[number]; +export type InferenceParameterField = (typeof INFERENCE_PARAMETER_FIELDS)[number]; type FieldMetadata = HyperparameterFieldMetadata< Record @@ -33,7 +35,7 @@ export const InferenceParameters: FC = ({ fieldMetadata, }) => ( - {FIELDS.map((key) => { + {INFERENCE_PARAMETER_FIELDS.map((key) => { const param = { ...INFERENCE_HYPERPARAMETER_FIELD_METADATA[key], ...fieldMetadata?.[key] }; return ( = { export const GLINER_ROLE = 'entity_detector'; -export const SAMPLING_PARAM_KEYS = ['temperature', 'top_p', 'max_tokens'] as const; - /** GLiNER is a token-classification NIM: it takes labels and a threshold, not sampling params. */ export const supportsSamplingParams = (role: string): boolean => role !== GLINER_ROLE; diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts index 5a5ce13212a..53dcfa2a4ed 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts @@ -1,6 +1,10 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { + INFERENCE_PARAMETER_FIELDS, + type InferenceParameterField, +} from '@nemo/common/src/components/ModelSelectV2/InferenceParameters'; import { generateDefaultName } from '@nemo/common/src/utils/generateDefaultName'; import type { AnonymizerConfig, @@ -35,7 +39,6 @@ import { REWRITE_STRATEGY, RISK_TOLERANCE_DEFAULT, RISK_TOLERANCE_ORDER, - SAMPLING_PARAM_KEYS, SOURCE_TYPE_DATASET, STRATEGY_ANNOTATE, STRATEGY_HASH, @@ -221,7 +224,7 @@ const buildRoleParams = ( ): Record => { if (!supportsSamplingParams(role)) { const kept = Object.entries(overrides).filter( - ([key]) => !SAMPLING_PARAM_KEYS.includes(key as (typeof SAMPLING_PARAM_KEYS)[number]) + ([key]) => !INFERENCE_PARAMETER_FIELDS.includes(key as InferenceParameterField) ); return { timeout: DEFAULT_MODEL_TIMEOUT_SECONDS, ...Object.fromEntries(kept) }; } From bdd4915be8030c3da5573126779b2264f8563b7d Mon Sep 17 00:00:00 2001 From: mschwab Date: Wed, 12 Aug 2026 11:45:20 -0700 Subject: [PATCH 5/5] test(studio): use nvidia mock models and type-only imports Swap the anonymizer test mocks from openai/gpt-oss-120b to nvidia/nemotron-3-nano-30b-a3b, and import AnonymizerFormData, FC and ReactNode with import type. Signed-off-by: mschwab --- .../components/ModelSettingsSection.test.tsx | 6 +++--- .../AnonymizerBuilderRoute/schema.test.ts | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx index 34aefbce6dd..5b64a93d553 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/components/ModelSettingsSection.test.tsx @@ -8,17 +8,17 @@ import { STRATEGY_SUBSTITUTE, } from '@studio/routes/AnonymizerBuilderRoute/constants'; import { - AnonymizerFormData, getAnonymizerFormDefaults, + type AnonymizerFormData, } from '@studio/routes/AnonymizerBuilderRoute/schema'; import { render, screen, within } from '@testing-library/react'; -import { FC, ReactNode } from 'react'; +import { type FC, type ReactNode } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; vi.mock('@studio/routes/AnonymizerBuilderRoute/useAnonymizerModels', () => ({ useAnonymizerModels: () => ({ models: [], - items: [{ label: 'gpt-oss-120b', value: 'gpt-oss-120b' }], + items: [{ label: 'nemotron-3-nano-30b-a3b', value: 'nvidia/nemotron-3-nano-30b-a3b' }], isLoading: false, applyModel: vi.fn(), }), diff --git a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts index b7f97de8697..6a2aa8aad84 100644 --- a/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts +++ b/web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts @@ -10,10 +10,10 @@ import { ROLE_LABELS, } from '@studio/routes/AnonymizerBuilderRoute/constants'; import { - AnonymizerFormData, anonymizerFormSchema, buildAnonymizerJobRequest, getAnonymizerFormDefaults, + type AnonymizerFormData, } from '@studio/routes/AnonymizerBuilderRoute/schema'; const ALL_ROLES = [...DETECTION_ROLES, REPLACE_ROLE, ...REWRITE_ROLES]; @@ -25,7 +25,7 @@ const roleModels = (model: string, provider: string): AnonymizerFormData['roleMo const form = (overrides: Partial = {}): AnonymizerFormData => ({ ...getAnonymizerFormDefaults(), source: 'https://example.com/data.csv', - roleModels: roleModels('openai/gpt-oss-120b', 'default/nvidia'), + roleModels: roleModels('nvidia/nemotron-3-nano-30b-a3b', 'default/nvidia'), ...overrides, }); @@ -137,13 +137,13 @@ describe('buildAnonymizerJobRequest', () => { expect(req.spec.model_configs).toEqual([ { alias: 'model-1', - model: 'openai/gpt-oss-120b', + model: 'nvidia/nemotron-3-nano-30b-a3b', provider: 'default/nvidia', inference_parameters: { timeout: 500 }, }, { alias: 'model-2', - model: 'openai/gpt-oss-120b', + model: 'nvidia/nemotron-3-nano-30b-a3b', provider: 'default/nvidia', inference_parameters: { timeout: 500, max_tokens: 16384, temperature: 0.3, top_p: 0.95 }, }, @@ -151,7 +151,7 @@ describe('buildAnonymizerJobRequest', () => { }); it('sends no sampling params to the GLiNER detector, even when the form holds them', () => { - const models = roleModels('openai/gpt-oss-120b', 'default/nvidia'); + const models = roleModels('nvidia/nemotron-3-nano-30b-a3b', 'default/nvidia'); models[GLINER_ROLE] = { modelId: 'gliner', model: 'nvidia/gliner-pii', @@ -165,7 +165,7 @@ describe('buildAnonymizerJobRequest', () => { }); it('emits one model_config per unique model+provider', () => { - const models = roleModels('openai/gpt-oss-120b', 'default/nvidia'); + const models = roleModels('nvidia/nemotron-3-nano-30b-a3b', 'default/nvidia'); models[DETECTION_ROLES[0]] = { modelId: 'gliner', model: 'nvidia/gliner-pii', @@ -262,10 +262,10 @@ describe('buildAnonymizerJobRequest', () => { }); it('attaches inference_parameters and splits configs when params differ', () => { - const models = roleModels('openai/gpt-oss-120b', 'default/nvidia'); + const models = roleModels('nvidia/nemotron-3-nano-30b-a3b', 'default/nvidia'); models['entity_validator'] = { - modelId: 'gpt', - model: 'openai/gpt-oss-120b', + modelId: 'nemotron', + model: 'nvidia/nemotron-3-nano-30b-a3b', provider: 'default/nvidia', params: { temperature: 0.1 }, }; @@ -287,7 +287,7 @@ describe('anonymizerFormSchema', () => { anonymizerFormSchema.safeParse({ ...getAnonymizerFormDefaults(), source: 'https://example.com/data.csv', - roleModels: roleModels('openai/gpt-oss-120b', 'default/nvidia'), + roleModels: roleModels('nvidia/nemotron-3-nano-30b-a3b', 'default/nvidia'), ...overrides, });